// Copyright 2015 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! An internal use crate to looking for windows-specific tools: //! 1. On Windows host, probe the Windows Registry if needed; //! 2. On non-Windows host, check specified environment variables. #![allow(clippy::upper_case_acronyms)] use std::{ env, ffi::{OsStr, OsString}, ops::Deref, path::PathBuf, process::Command, sync::Arc, }; use crate::Tool; /// The target provided by the user. #[derive(Copy, Clone, PartialEq, Eq)] enum TargetArch { X86, X64, Arm, Arm64, Arm64ec, } impl TargetArch { /// Parse the `TargetArch` from a str. Returns `None` if the arch is unrecognized. fn new(arch: &str) -> Option { // NOTE: Keep up to date with docs in [`find`]. match arch { "x64" | "x86_64" => Some(Self::X64), "arm64" | "aarch64" => Some(Self::Arm64), "arm64ec" => Some(Self::Arm64ec), "x86" | "i686" | "i586" => Some(Self::X86), "arm" | "thumbv7a" => Some(Self::Arm), _ => None, } } #[cfg(windows)] /// Gets the Visual Studio name for the architecture. fn as_vs_arch(&self) -> &'static str { match self { Self::X64 => "x64", Self::Arm64 | Self::Arm64ec => "arm64", Self::X86 => "x86", Self::Arm => "arm", } } } #[derive(Debug, Clone)] #[non_exhaustive] pub enum Env { Owned(OsString), Arced(Arc), } impl AsRef for Env { fn as_ref(&self) -> &OsStr { self.deref() } } impl Deref for Env { type Target = OsStr; fn deref(&self) -> &Self::Target { match self { Env::Owned(os_str) => os_str, Env::Arced(os_str) => os_str, } } } impl From for PathBuf { fn from(env: Env) -> Self { match env { Env::Owned(os_str) => PathBuf::from(os_str), Env::Arced(os_str) => PathBuf::from(os_str.deref()), } } } pub trait EnvGetter { fn get_env(&self, name: &'static str) -> Option; } struct StdEnvGetter; impl EnvGetter for StdEnvGetter { #[allow(clippy::disallowed_methods)] fn get_env(&self, name: &'static str) -> Option { env::var_os(name).map(Env::Owned) } } /// Attempts to find a tool within an MSVC installation using the Windows /// registry as a point to search from. /// /// The `arch_or_target` argument is the architecture or the Rust target name /// that the tool should work for (e.g. compile or link for). The supported /// architecture names are: /// - `"x64"` or `"x86_64"` /// - `"arm64"` or `"aarch64"` /// - `"arm64ec"` /// - `"x86"`, `"i586"` or `"i686"` /// - `"arm"` or `"thumbv7a"` /// /// The `tool` argument is the tool to find. Supported tools include: /// - MSVC tools: `cl.exe`, `link.exe`, `lib.exe`, etc. /// - `MSBuild`: `msbuild.exe` /// - Visual Studio IDE: `devenv.exe` /// - Clang/LLVM tools: `clang.exe`, `clang++.exe`, `clang-*.exe`, `llvm-*.exe`, `lld.exe`, etc. /// /// This function will return `None` if the tool could not be found, or it will /// return `Some(cmd)` which represents a command that's ready to execute the /// tool with the appropriate environment variables set. /// /// Note that this function always returns `None` for non-MSVC targets (if a /// full target name was specified). pub fn find(arch_or_target: &str, tool: &str) -> Option { find_tool(arch_or_target, tool).map(|c| c.to_command()) } /// Similar to the `find` function above, this function will attempt the same /// operation (finding a MSVC tool in a local install) but instead returns a /// `Tool` which may be introspected. pub fn find_tool(arch_or_target: &str, tool: &str) -> Option { let full_arch = if let Some((full_arch, rest)) = arch_or_target.split_once("-") { // The logic is all tailored for MSVC, if the target is not that then // bail out early. if !rest.contains("msvc") { return None; } full_arch } else { arch_or_target }; find_tool_with_env(full_arch, tool, &StdEnvGetter) } pub fn find_tool_with_env(full_arch: &str, tool: &str, env_getter: &dyn EnvGetter) -> Option { // We only need the arch. let target = TargetArch::new(full_arch)?; // Looks like msbuild isn't located in the same location as other tools like // cl.exe and lib.exe. if tool.contains("msbuild") { return impl_::find_msbuild(target, env_getter); } // Looks like devenv isn't located in the same location as other tools like // cl.exe and lib.exe. if tool.contains("devenv") { return impl_::find_devenv(target, env_getter); } // Clang/LLVM isn't located in the same location as other tools like // cl.exe and lib.exe. if ["clang", "lldb", "llvm", "ld", "lld"] .iter() .any(|&t| tool.contains(t)) { return impl_::find_llvm_tool(tool, target, env_getter); } // Ok, if we're here, now comes the fun part of the probing. Default shells // or shells like MSYS aren't really configured to execute `cl.exe` and the // various compiler tools shipped as part of Visual Studio. Here we try to // first find the relevant tool, then we also have to be sure to fill in // environment variables like `LIB`, `INCLUDE`, and `PATH` to ensure that // the tool is actually usable. impl_::find_msvc_environment(tool, target, env_getter) .or_else(|| impl_::find_msvc_15plus(tool, target, env_getter)) .or_else(|| impl_::find_msvc_14(tool, target, env_getter)) } /// A version of Visual Studio #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[non_exhaustive] pub enum VsVers { /// Visual Studio 12 (2013) #[deprecated( note = "Visual Studio 12 is no longer supported. cc will never return this value." )] Vs12, /// Visual Studio 14 (2015) Vs14, /// Visual Studio 15 (2017) Vs15, /// Visual Studio 16 (2019) Vs16, /// Visual Studio 17 (2022) Vs17, } /// Find the most recent installed version of Visual Studio /// /// This is used by the cmake crate to figure out the correct /// generator. #[allow(clippy::disallowed_methods)] pub fn find_vs_version() -> Result { fn has_msbuild_version(version: &str) -> bool { impl_::has_msbuild_version(version, &StdEnvGetter) } match std::env::var("VisualStudioVersion") { Ok(version) => match &version[..] { "17.0" => Ok(VsVers::Vs17), "16.0" => Ok(VsVers::Vs16), "15.0" => Ok(VsVers::Vs15), "14.0" => Ok(VsVers::Vs14), vers => Err(format!( "\n\n\ unsupported or unknown VisualStudio version: {vers}\n\ if another version is installed consider running \ the appropriate vcvars script before building this \ crate\n\ " )), }, _ => { // Check for the presence of a specific registry key // that indicates visual studio is installed. if has_msbuild_version("17.0") { Ok(VsVers::Vs17) } else if has_msbuild_version("16.0") { Ok(VsVers::Vs16) } else if has_msbuild_version("15.0") { Ok(VsVers::Vs15) } else if has_msbuild_version("14.0") { Ok(VsVers::Vs14) } else { Err("\n\n\ couldn't determine visual studio generator\n\ if VisualStudio is installed, however, consider \ running the appropriate vcvars script before building \ this crate\n\ " .to_string()) } } } } /// To find the Universal CRT we look in a specific registry key for where /// all the Universal CRTs are located and then sort them asciibetically to /// find the newest version. While this sort of sorting isn't ideal, it is /// what vcvars does so that's good enough for us. /// /// Returns a pair of (root, version) for the ucrt dir if found pub fn get_ucrt_dir() -> Option<(PathBuf, String)> { impl_::get_ucrt_dir() } /// Windows Implementation. #[cfg(windows)] mod impl_ { use crate::com; use crate::registry::{RegistryKey, LOCAL_MACHINE}; use crate::setup_config::SetupConfiguration; use crate::vs_instances::{VsInstances, VswhereInstance}; use crate::windows_sys::{ GetMachineTypeAttributes, GetProcAddress, LoadLibraryA, UserEnabled, HMODULE, IMAGE_FILE_MACHINE_AMD64, MACHINE_ATTRIBUTES, S_OK, }; use std::convert::TryFrom; use std::env; use std::ffi::OsString; use std::fs::File; use std::io::Read; use std::iter; use std::mem; use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Once; use super::{EnvGetter, TargetArch}; use crate::Tool; struct MsvcTool { tool: PathBuf, libs: Vec, path: Vec, include: Vec, } #[derive(Default)] struct SdkInfo { libs: Vec, path: Vec, include: Vec, } struct LibraryHandle(HMODULE); impl LibraryHandle { fn new(name: &[u8]) -> Option { let handle = unsafe { LoadLibraryA(name.as_ptr() as _) }; (!handle.is_null()).then_some(Self(handle)) } /// Get a function pointer to a function in the library. /// # SAFETY /// /// The caller must ensure that the function signature matches the actual function. /// The easiest way to do this is to add an entry to `windows_sys_no_link.list` and use the /// generated function for `func_signature`. /// /// The function returned cannot be used after the handle is dropped. unsafe fn get_proc_address(&self, name: &[u8]) -> Option { let symbol = GetProcAddress(self.0, name.as_ptr() as _); symbol.map(|symbol| mem::transmute_copy(&symbol)) } } type GetMachineTypeAttributesFuncType = unsafe extern "system" fn(u16, *mut MACHINE_ATTRIBUTES) -> i32; const _: () = { // Ensure that our hand-written signature matches the actual function signature. // We can't use `GetMachineTypeAttributes` outside of a const scope otherwise we'll end up statically linking to // it, which will fail to load on older versions of Windows. let _: GetMachineTypeAttributesFuncType = GetMachineTypeAttributes; }; fn is_amd64_emulation_supported_inner() -> Option { // GetMachineTypeAttributes is only available on Win11 22000+, so dynamically load it. let kernel32 = LibraryHandle::new(b"kernel32.dll\0")?; // SAFETY: GetMachineTypeAttributesFuncType is checked to match the real function signature. let get_machine_type_attributes = unsafe { kernel32 .get_proc_address::(b"GetMachineTypeAttributes\0") }?; let mut attributes = Default::default(); if unsafe { get_machine_type_attributes(IMAGE_FILE_MACHINE_AMD64, &mut attributes) } == S_OK { Some((attributes & UserEnabled) != 0) } else { Some(false) } } fn is_amd64_emulation_supported() -> bool { // TODO: Replace with a OnceLock once MSRV is 1.70. static LOAD_VALUE: Once = Once::new(); static IS_SUPPORTED: AtomicBool = AtomicBool::new(false); // Using Relaxed ordering since the Once is providing synchronization. LOAD_VALUE.call_once(|| { IS_SUPPORTED.store( is_amd64_emulation_supported_inner().unwrap_or(false), Ordering::Relaxed, ); }); IS_SUPPORTED.load(Ordering::Relaxed) } impl MsvcTool { fn new(tool: PathBuf) -> MsvcTool { MsvcTool { tool, libs: Vec::new(), path: Vec::new(), include: Vec::new(), } } fn add_sdk(&mut self, sdk_info: SdkInfo) { self.libs.extend(sdk_info.libs); self.path.extend(sdk_info.path); self.include.extend(sdk_info.include); } fn into_tool(self, env_getter: &dyn EnvGetter) -> Tool { let MsvcTool { tool, libs, path, include, } = self; let mut tool = Tool { tool, is_clang_cl: false, env: Vec::new(), }; add_env(&mut tool, "LIB", libs, env_getter); add_env(&mut tool, "PATH", path, env_getter); add_env(&mut tool, "INCLUDE", include, env_getter); tool } } impl SdkInfo { fn find_tool(&self, tool: &str) -> Option { self.path.iter().map(|p| p.join(tool)).find(|p| p.exists()) } } /// Checks to see if the target's arch matches the VS environment. Returns `None` if the /// environment is unknown. fn is_vscmd_target(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { is_vscmd_target_env(target, env_getter).or_else(|| is_vscmd_target_cl(target, env_getter)) } /// Checks to see if the `VSCMD_ARG_TGT_ARCH` environment variable matches the /// given target's arch. Returns `None` if the variable does not exist. fn is_vscmd_target_env(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { let vscmd_arch = env_getter.get_env("VSCMD_ARG_TGT_ARCH")?; Some(target.as_vs_arch() == vscmd_arch.as_ref()) } /// Checks if the cl.exe target matches the given target's arch. Returns `None` if anything /// fails. fn is_vscmd_target_cl(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { let cmd_target = vscmd_target_cl(env_getter)?; Some(target.as_vs_arch() == cmd_target) } /// Detect the target architecture of `cl.exe` in the current path, and return `None` if this /// fails for any reason. fn vscmd_target_cl(env_getter: &dyn EnvGetter) -> Option<&'static str> { let cl_exe = env_getter.get_env("PATH").and_then(|path| { env::split_paths(&path) .map(|p| p.join("cl.exe")) .find(|p| p.exists()) })?; let mut cl = Command::new(cl_exe); cl.stderr(std::process::Stdio::piped()) .stdout(std::process::Stdio::null()); let out = cl.output().ok()?; let cl_arch = out .stderr .split(|&b| b == b'\n' || b == b'\r') .next()? .rsplit(|&b| b == b' ') .next()?; match cl_arch { b"x64" => Some("x64"), b"x86" => Some("x86"), b"ARM64" => Some("arm64"), b"ARM" => Some("arm"), _ => None, } } /// Attempt to find the tool using environment variables set by vcvars. pub(super) fn find_msvc_environment( tool: &str, target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { // Early return if the environment isn't one that is known to have compiler toolsets in PATH // `VCINSTALLDIR` is set from vcvarsall.bat (developer command prompt) // `VSTEL_MSBuildProjectFullPath` is set by msbuild when invoking custom build steps // NOTE: `VisualStudioDir` used to be used but this isn't set when invoking msbuild from the commandline if env_getter.get_env("VCINSTALLDIR").is_none() && env_getter.get_env("VSTEL_MSBuildProjectFullPath").is_none() { return None; } // If the vscmd target differs from the requested target then // attempt to get the tool using the VS install directory. if is_vscmd_target(target, env_getter) == Some(false) { // We will only get here with versions 15+. let vs_install_dir: PathBuf = env_getter.get_env("VSINSTALLDIR")?.into(); tool_from_vs15plus_instance(tool, target, &vs_install_dir, env_getter) } else { // Fallback to simply using the current environment. env_getter .get_env("PATH") .and_then(|path| { env::split_paths(&path) .map(|p| p.join(tool)) .find(|p| p.exists()) }) .map(|path| Tool { tool: path, is_clang_cl: false, env: Vec::new(), }) } } fn find_msbuild_vs17(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "17", env_getter) } #[allow(bare_trait_objects)] fn vs16plus_instances( target: TargetArch, version: &'static str, env_getter: &dyn EnvGetter, ) -> Box> { let instances = if let Some(instances) = vs15plus_instances(target, env_getter) { instances } else { return Box::new(iter::empty()); }; Box::new(instances.into_iter().filter_map(move |instance| { let installation_name = instance.installation_name()?; if installation_name.starts_with(&format!("VisualStudio/{}.", version)) || installation_name.starts_with(&format!("VisualStudioPreview/{}.", version)) { Some(instance.installation_path()?) } else { None } })) } fn find_tool_in_vs16plus_path( tool: &str, target: TargetArch, version: &'static str, env_getter: &dyn EnvGetter, ) -> Option { vs16plus_instances(target, version, env_getter) .filter_map(|path| { let path = path.join(tool); if !path.is_file() { return None; } let mut tool = Tool { tool: path, is_clang_cl: false, env: Vec::new(), }; if target == TargetArch::X64 { tool.env.push(("Platform".into(), "X64".into())); } if matches!(target, TargetArch::Arm64 | TargetArch::Arm64ec) { tool.env.push(("Platform".into(), "ARM64".into())); } Some(tool) }) .next() } fn find_msbuild_vs16(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "16", env_getter) } pub(super) fn find_llvm_tool( tool: &str, target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { find_llvm_tool_vs17(tool, target, env_getter) } fn find_llvm_tool_vs17( tool: &str, target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { vs16plus_instances(target, "17", env_getter) .filter_map(|mut base_path| { base_path.push(r"VC\Tools\LLVM"); let host_folder = match host_arch() { // The default LLVM bin folder is x86, and there's separate subfolders // for the x64 and ARM64 host tools. X86 => "", X86_64 => "x64", AARCH64 => "ARM64", _ => return None, }; if host_folder != "" { // E.g. C:\...\VC\Tools\LLVM\x64 base_path.push(host_folder); } // E.g. C:\...\VC\Tools\LLVM\x64\bin\clang.exe base_path.push("bin"); base_path.push(tool); let is_clang_cl = tool.contains("clang-cl"); base_path.is_file().then(|| Tool { tool: base_path, is_clang_cl, env: Vec::new(), }) }) .next() } // In MSVC 15 (2017) MS once again changed the scheme for locating // the tooling. Now we must go through some COM interfaces, which // is super fun for Rust. // // Note that much of this logic can be found [online] wrt paths, COM, etc. // // [online]: https://blogs.msdn.microsoft.com/vcblog/2017/03/06/finding-the-visual-c-compiler-tools-in-visual-studio-2017/ // // Returns MSVC 15+ instances (15, 16 right now), the order should be consider undefined. // // However, on ARM64 this method doesn't work because VS Installer fails to register COM component on ARM64. // Hence, as the last resort we try to use vswhere.exe to list available instances. fn vs15plus_instances(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { vs15plus_instances_using_com() .or_else(|| vs15plus_instances_using_vswhere(target, env_getter)) } fn vs15plus_instances_using_com() -> Option { com::initialize().ok()?; let config = SetupConfiguration::new().ok()?; let enum_setup_instances = config.enum_all_instances().ok()?; Some(VsInstances::ComBased(enum_setup_instances)) } fn vs15plus_instances_using_vswhere( target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { let program_files_path = env_getter .get_env("ProgramFiles(x86)") .or_else(|| env_getter.get_env("ProgramFiles"))?; let program_files_path = Path::new(program_files_path.as_ref()); let vswhere_path = program_files_path.join(r"Microsoft Visual Studio\Installer\vswhere.exe"); if !vswhere_path.exists() { return None; } let tools_arch = match target { TargetArch::X86 | TargetArch::X64 => Some("x86.x64"), TargetArch::Arm => Some("ARM"), TargetArch::Arm64 | TargetArch::Arm64ec => Some("ARM64"), }; let vswhere_output = Command::new(vswhere_path) .args([ "-latest", "-products", "*", "-requires", &format!("Microsoft.VisualStudio.Component.VC.Tools.{}", tools_arch?), "-format", "text", "-nologo", ]) .stderr(std::process::Stdio::inherit()) .output() .ok()?; let vs_instances = VsInstances::VswhereBased(VswhereInstance::try_from(&vswhere_output.stdout).ok()?); Some(vs_instances) } // Inspired from official microsoft/vswhere ParseVersionString // i.e. at most four u16 numbers separated by '.' fn parse_version(version: &str) -> Option<[u16; 4]> { let mut iter = version.split('.').map(u16::from_str).fuse(); let mut get_next_number = move || match iter.next() { Some(Ok(version_part)) => Some(version_part), Some(Err(_)) => None, None => Some(0), }; Some([ get_next_number()?, get_next_number()?, get_next_number()?, get_next_number()?, ]) } pub(super) fn find_msvc_15plus( tool: &str, target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { let iter = vs15plus_instances(target, env_getter)?; iter.into_iter() .filter_map(|instance| { let version = parse_version(&instance.installation_version()?)?; let instance_path = instance.installation_path()?; let tool = tool_from_vs15plus_instance(tool, target, &instance_path, env_getter)?; Some((version, tool)) }) .max_by(|(a_version, _), (b_version, _)| a_version.cmp(b_version)) .map(|(_version, tool)| tool) } // While the paths to Visual Studio 2017's devenv and MSBuild could // potentially be retrieved from the registry, finding them via // SetupConfiguration has shown to be [more reliable], and is preferred // according to Microsoft. To help head off potential regressions though, // we keep the registry method as a fallback option. // // [more reliable]: https://github.com/rust-lang/cc-rs/pull/331 fn find_tool_in_vs15_path( tool: &str, target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { let mut path = match vs15plus_instances(target, env_getter) { Some(instances) => instances .into_iter() .filter_map(|instance| instance.installation_path()) .map(|path| path.join(tool)) .find(|path| path.is_file()), None => None, }; if path.is_none() { let key = r"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\SxS\VS7"; path = LOCAL_MACHINE .open(key.as_ref()) .ok() .and_then(|key| key.query_str("15.0").ok()) .map(|path| PathBuf::from(path).join(tool)) .and_then(|path| if path.is_file() { Some(path) } else { None }); } path.map(|path| { let mut tool = Tool { tool: path, is_clang_cl: false, env: Vec::new(), }; if target == TargetArch::X64 { tool.env.push(("Platform".into(), "X64".into())); } else if matches!(target, TargetArch::Arm64 | TargetArch::Arm64ec) { tool.env.push(("Platform".into(), "ARM64".into())); } tool }) } fn tool_from_vs15plus_instance( tool: &str, target: TargetArch, instance_path: &Path, env_getter: &dyn EnvGetter, ) -> Option { let (root_path, bin_path, host_dylib_path, lib_path, alt_lib_path, include_path) = vs15plus_vc_paths(target, instance_path, env_getter)?; let sdk_info = get_sdks(target, env_getter)?; let mut tool_path = bin_path.join(tool); if !tool_path.exists() { tool_path = sdk_info.find_tool(tool)?; }; let mut tool = MsvcTool::new(tool_path); tool.path.push(bin_path.clone()); tool.path.push(host_dylib_path); if let Some(alt_lib_path) = alt_lib_path { tool.libs.push(alt_lib_path); } tool.libs.push(lib_path); tool.include.push(include_path); if let Some((atl_lib_path, atl_include_path)) = atl_paths(target, &root_path) { tool.libs.push(atl_lib_path); tool.include.push(atl_include_path); } tool.add_sdk(sdk_info); Some(tool.into_tool(env_getter)) } fn vs15plus_vc_paths( target_arch: TargetArch, instance_path: &Path, env_getter: &dyn EnvGetter, ) -> Option<(PathBuf, PathBuf, PathBuf, PathBuf, Option, PathBuf)> { let version = vs15plus_vc_read_version(instance_path)?; let hosts = match host_arch() { X86 => &["X86"], X86_64 => &["X64"], // Starting with VS 17.4, there is a natively hosted compiler on ARM64: // https://devblogs.microsoft.com/visualstudio/arm64-visual-studio-is-officially-here/ // On older versions of VS, we use x64 if running under emulation is supported, // otherwise use x86. AARCH64 => { if is_amd64_emulation_supported() { &["ARM64", "X64", "X86"][..] } else { &["ARM64", "X86"] } } _ => return None, }; let target_dir = target_arch.as_vs_arch(); // The directory layout here is MSVC/bin/Host$host/$target/ let path = instance_path.join(r"VC\Tools\MSVC").join(version); // We use the first available host architecture that can build for the target let (host_path, host) = hosts.iter().find_map(|&x| { let candidate = path.join("bin").join(format!("Host{}", x)); if candidate.join(target_dir).exists() { Some((candidate, x)) } else { None } })?; // This is the path to the toolchain for a particular target, running // on a given host let bin_path = host_path.join(target_dir); // But! we also need PATH to contain the target directory for the host // architecture, because it contains dlls like mspdb140.dll compiled for // the host architecture. let host_dylib_path = host_path.join(host.to_lowercase()); let lib_fragment = if use_spectre_mitigated_libs(env_getter) { r"lib\spectre" } else { "lib" }; let lib_path = path.join(lib_fragment).join(target_dir); let alt_lib_path = (target_arch == TargetArch::Arm64ec).then(|| path.join(lib_fragment).join("arm64ec")); let include_path = path.join("include"); Some(( path, bin_path, host_dylib_path, lib_path, alt_lib_path, include_path, )) } fn vs15plus_vc_read_version(dir: &Path) -> Option { // Try to open the default version file. let mut version_path: PathBuf = dir.join(r"VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt"); let mut version_file = if let Ok(f) = File::open(&version_path) { f } else { // If the default doesn't exist, search for other version files. // These are in the form Microsoft.VCToolsVersion.v143.default.txt // where `143` is any three decimal digit version number. // This sorts versions by lexical order and selects the highest version. let mut version_file = String::new(); version_path.pop(); for file in version_path.read_dir().ok()? { let name = file.ok()?.file_name(); let name = name.to_str()?; if name.starts_with("Microsoft.VCToolsVersion.v") && name.ends_with(".default.txt") && name > &version_file { version_file.replace_range(.., name); } } if version_file.is_empty() { // If all else fails, manually search for bin directories. let tools_dir: PathBuf = dir.join(r"VC\Tools\MSVC"); return tools_dir .read_dir() .ok()? .filter_map(|file| { let file = file.ok()?; let name = file.file_name().into_string().ok()?; file.path().join("bin").exists().then(|| { let version = parse_version(&name); (name, version) }) }) .max_by(|(_, a), (_, b)| a.cmp(b)) .map(|(version, _)| version); } version_path.push(version_file); File::open(version_path).ok()? }; // Get the version string from the file we found. let mut version = String::new(); version_file.read_to_string(&mut version).ok()?; version.truncate(version.trim_end().len()); Some(version) } fn use_spectre_mitigated_libs(env_getter: &dyn EnvGetter) -> bool { env_getter .get_env("VSCMD_ARG_VCVARS_SPECTRE") .map(|env| env.as_ref() == "spectre") .unwrap_or_default() } fn atl_paths(target: TargetArch, path: &Path) -> Option<(PathBuf, PathBuf)> { let atl_path = path.join("atlmfc"); let sub = target.as_vs_arch(); if atl_path.exists() { Some((atl_path.join("lib").join(sub), atl_path.join("include"))) } else { None } } // For MSVC 14 we need to find the Universal CRT as well as either // the Windows 10 SDK or Windows 8.1 SDK. pub(super) fn find_msvc_14( tool: &str, target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { let vcdir = get_vc_dir("14.0")?; let sdk_info = get_sdks(target, env_getter)?; let mut tool = get_tool(tool, &vcdir, target, &sdk_info)?; tool.add_sdk(sdk_info); Some(tool.into_tool(env_getter)) } fn get_sdks(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { let sub = target.as_vs_arch(); let (ucrt, ucrt_version) = get_ucrt_dir()?; let host = match host_arch() { X86 => "x86", X86_64 => "x64", AARCH64 => "arm64", _ => return None, }; let mut info = SdkInfo::default(); info.path .push(ucrt.join("bin").join(&ucrt_version).join(host)); let ucrt_include = ucrt.join("include").join(&ucrt_version); info.include.push(ucrt_include.join("ucrt")); let ucrt_lib = ucrt.join("lib").join(&ucrt_version); info.libs.push(ucrt_lib.join("ucrt").join(sub)); if let Some((sdk, version)) = get_sdk10_dir(env_getter) { info.path.push(sdk.join("bin").join(host)); let sdk_lib = sdk.join("lib").join(&version); info.libs.push(sdk_lib.join("um").join(sub)); let sdk_include = sdk.join("include").join(&version); info.include.push(sdk_include.join("um")); info.include.push(sdk_include.join("cppwinrt")); info.include.push(sdk_include.join("winrt")); info.include.push(sdk_include.join("shared")); } else if let Some(sdk) = get_sdk81_dir() { info.path.push(sdk.join("bin").join(host)); let sdk_lib = sdk.join("lib").join("winv6.3"); info.libs.push(sdk_lib.join("um").join(sub)); let sdk_include = sdk.join("include"); info.include.push(sdk_include.join("um")); info.include.push(sdk_include.join("winrt")); info.include.push(sdk_include.join("shared")); } Some(info) } fn add_env( tool: &mut Tool, env: &'static str, paths: Vec, env_getter: &dyn EnvGetter, ) { let prev = env_getter.get_env(env); let prev = prev.as_ref().map(AsRef::as_ref).unwrap_or_default(); let prev = env::split_paths(&prev); let new = paths.into_iter().chain(prev); tool.env .push((env.to_string().into(), env::join_paths(new).unwrap())); } // Given a possible MSVC installation directory, we look for the linker and // then add the MSVC library path. fn get_tool( tool: &str, path: &Path, target: TargetArch, sdk_info: &SdkInfo, ) -> Option { bin_subdir(target) .into_iter() .map(|(sub, host)| { ( path.join("bin").join(sub).join(tool), Some(path.join("bin").join(host)), ) }) .filter(|(path, _)| path.is_file()) .chain(iter::once_with(|| Some((sdk_info.find_tool(tool)?, None))).flatten()) .map(|(tool_path, host)| { let mut tool = MsvcTool::new(tool_path); tool.path.extend(host); let sub = vc_lib_subdir(target); tool.libs.push(path.join("lib").join(sub)); tool.include.push(path.join("include")); let atlmfc_path = path.join("atlmfc"); if atlmfc_path.exists() { tool.libs.push(atlmfc_path.join("lib").join(sub)); tool.include.push(atlmfc_path.join("include")); } tool }) .next() } // To find MSVC we look in a specific registry key for the version we are // trying to find. fn get_vc_dir(ver: &str) -> Option { let key = r"SOFTWARE\Microsoft\VisualStudio\SxS\VC7"; let key = LOCAL_MACHINE.open(key.as_ref()).ok()?; let path = key.query_str(ver).ok()?; Some(path.into()) } // To find the Universal CRT we look in a specific registry key for where // all the Universal CRTs are located and then sort them asciibetically to // find the newest version. While this sort of sorting isn't ideal, it is // what vcvars does so that's good enough for us. // // Returns a pair of (root, version) for the ucrt dir if found pub(super) fn get_ucrt_dir() -> Option<(PathBuf, String)> { let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; let key = LOCAL_MACHINE.open(key.as_ref()).ok()?; let root = key.query_str("KitsRoot10").ok()?; let readdir = Path::new(&root).join("lib").read_dir().ok()?; let max_libdir = readdir .filter_map(|dir| dir.ok()) .map(|dir| dir.path()) .filter(|dir| { dir.components() .last() .and_then(|c| c.as_os_str().to_str()) .map(|c| c.starts_with("10.") && dir.join("ucrt").is_dir()) .unwrap_or(false) }) .max()?; let version = max_libdir.components().last().unwrap(); let version = version.as_os_str().to_str().unwrap().to_string(); Some((root.into(), version)) } // Vcvars finds the correct version of the Windows 10 SDK by looking // for the include `um\Windows.h` because sometimes a given version will // only have UCRT bits without the rest of the SDK. Since we only care about // libraries and not includes, we instead look for `um\x64\kernel32.lib`. // Since the 32-bit and 64-bit libraries are always installed together we // only need to bother checking x64, making this code a tiny bit simpler. // Like we do for the Universal CRT, we sort the possibilities // asciibetically to find the newest one as that is what vcvars does. // Before doing that, we check the "WindowsSdkDir" and "WindowsSDKVersion" // environment variables set by vcvars to use the environment sdk version // if one is already configured. fn get_sdk10_dir(env_getter: &dyn EnvGetter) -> Option<(PathBuf, String)> { if let (Some(root), Some(version)) = ( env_getter.get_env("WindowsSdkDir"), env_getter .get_env("WindowsSDKVersion") .as_ref() .and_then(|version| version.as_ref().to_str()), ) { return Some(( PathBuf::from(root), version.trim_end_matches('\\').to_string(), )); } let key = r"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0"; let key = LOCAL_MACHINE.open(key.as_ref()).ok()?; let root = key.query_str("InstallationFolder").ok()?; let readdir = Path::new(&root).join("lib").read_dir().ok()?; let mut dirs = readdir .filter_map(|dir| dir.ok()) .map(|dir| dir.path()) .collect::>(); dirs.sort(); let dir = dirs .into_iter() .rev() .find(|dir| dir.join("um").join("x64").join("kernel32.lib").is_file())?; let version = dir.components().last().unwrap(); let version = version.as_os_str().to_str().unwrap().to_string(); Some((root.into(), version)) } // Interestingly there are several subdirectories, `win7` `win8` and // `winv6.3`. Vcvars seems to only care about `winv6.3` though, so the same // applies to us. Note that if we were targeting kernel mode drivers // instead of user mode applications, we would care. fn get_sdk81_dir() -> Option { let key = r"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.1"; let key = LOCAL_MACHINE.open(key.as_ref()).ok()?; let root = key.query_str("InstallationFolder").ok()?; Some(root.into()) } const PROCESSOR_ARCHITECTURE_INTEL: u16 = 0; const PROCESSOR_ARCHITECTURE_AMD64: u16 = 9; const PROCESSOR_ARCHITECTURE_ARM64: u16 = 12; const X86: u16 = PROCESSOR_ARCHITECTURE_INTEL; const X86_64: u16 = PROCESSOR_ARCHITECTURE_AMD64; const AARCH64: u16 = PROCESSOR_ARCHITECTURE_ARM64; // When choosing the tool to use, we have to choose the one which matches // the target architecture. Otherwise we end up in situations where someone // on 32-bit Windows is trying to cross compile to 64-bit and it tries to // invoke the native 64-bit compiler which won't work. // // For the return value of this function, the first member of the tuple is // the folder of the tool we will be invoking, while the second member is // the folder of the host toolchain for that tool which is essential when // using a cross linker. We return a Vec since on x64 there are often two // linkers that can target the architecture we desire. The 64-bit host // linker is preferred, and hence first, due to 64-bit allowing it more // address space to work with and potentially being faster. fn bin_subdir(target: TargetArch) -> Vec<(&'static str, &'static str)> { match (target, host_arch()) { (TargetArch::X86, X86) => vec![("", "")], (TargetArch::X86, X86_64) => vec![("amd64_x86", "amd64"), ("", "")], (TargetArch::X64, X86) => vec![("x86_amd64", "")], (TargetArch::X64, X86_64) => vec![("amd64", "amd64"), ("x86_amd64", "")], (TargetArch::Arm, X86) => vec![("x86_arm", "")], (TargetArch::Arm, X86_64) => vec![("amd64_arm", "amd64"), ("x86_arm", "")], _ => vec![], } } // MSVC's x86 libraries are not in a subfolder fn vc_lib_subdir(target: TargetArch) -> &'static str { match target { TargetArch::X86 => "", TargetArch::X64 => "amd64", TargetArch::Arm => "arm", TargetArch::Arm64 | TargetArch::Arm64ec => "arm64", } } #[allow(bad_style)] fn host_arch() -> u16 { type DWORD = u32; type WORD = u16; type LPVOID = *mut u8; type DWORD_PTR = usize; #[repr(C)] struct SYSTEM_INFO { wProcessorArchitecture: WORD, _wReserved: WORD, _dwPageSize: DWORD, _lpMinimumApplicationAddress: LPVOID, _lpMaximumApplicationAddress: LPVOID, _dwActiveProcessorMask: DWORD_PTR, _dwNumberOfProcessors: DWORD, _dwProcessorType: DWORD, _dwAllocationGranularity: DWORD, _wProcessorLevel: WORD, _wProcessorRevision: WORD, } extern "system" { fn GetNativeSystemInfo(lpSystemInfo: *mut SYSTEM_INFO); } unsafe { let mut info = mem::zeroed(); GetNativeSystemInfo(&mut info); info.wProcessorArchitecture } } #[cfg(test)] mod tests { use super::*; use std::path::Path; // Import the find function from the module level use crate::find_tools::find; fn host_arch_to_string(host_arch_value: u16) -> &'static str { match host_arch_value { X86 => "x86", X86_64 => "x64", AARCH64 => "arm64", _ => panic!("Unsupported host architecture: {}", host_arch_value), } } #[test] fn test_find_cl_exe() { // Test that we can find cl.exe for common target architectures // and validate the correct host-target combination paths // This should pass on Windows CI with Visual Studio installed let target_architectures = ["x64", "x86", "arm64"]; let mut found_any = false; // Determine the host architecture let host_arch_value = host_arch(); let host_name = host_arch_to_string(host_arch_value); for &target_arch in &target_architectures { if let Some(cmd) = find(target_arch, "cl.exe") { // Verify the command looks valid assert!( !cmd.get_program().is_empty(), "cl.exe program path should not be empty" ); assert!( Path::new(cmd.get_program()).exists(), "cl.exe should exist at: {:?}", cmd.get_program() ); // Verify the path contains the correct host-target combination // Use case-insensitive comparison since VS IDE uses "Hostx64" while Build Tools use "HostX64" let path_str = cmd.get_program().to_string_lossy(); let path_str_lower = path_str.to_lowercase(); let expected_host_target_path = format!("\\bin\\host{host_name}\\{target_arch}"); let expected_host_target_path_unix = expected_host_target_path.replace("\\", "/"); assert!( path_str_lower.contains(&expected_host_target_path) || path_str_lower.contains(&expected_host_target_path_unix), "cl.exe path should contain host-target combination (case-insensitive) '{}' for {} host targeting {}, but found: {}", expected_host_target_path, host_name, target_arch, path_str ); found_any = true; } } assert!(found_any, "Expected to find cl.exe for at least one target architecture (x64, x86, or arm64) on Windows CI with Visual Studio installed"); } #[test] #[cfg(not(disable_clang_cl_tests))] fn test_find_llvm_tools() { // Import StdEnvGetter from the parent module use crate::find_tools::StdEnvGetter; // Test the actual find_llvm_tool function with various LLVM tools // This test assumes CI environment has Visual Studio + Clang installed // We test against x64 target since clang can cross-compile to any target let target_arch = TargetArch::new("x64").expect("Should support x64 architecture"); let llvm_tools = ["clang.exe", "clang++.exe", "lld.exe", "llvm-ar.exe"]; // Determine expected host-specific path based on host architecture let host_arch_value = host_arch(); let expected_host_path = match host_arch_value { X86 => "LLVM\\bin", // x86 host X86_64 => "LLVM\\x64\\bin", // x64 host AARCH64 => "LLVM\\ARM64\\bin", // arm64 host _ => panic!("Unsupported host architecture: {}", host_arch_value), }; let host_name = host_arch_to_string(host_arch_value); let mut found_tools_count = 0; for &tool in &llvm_tools { // Test finding LLVM tools using the standard environment getter let env_getter = StdEnvGetter; let result = find_llvm_tool(tool, target_arch, &env_getter); match result { Some(found_tool) => { found_tools_count += 1; // Verify the found tool has a valid, non-empty path assert!( !found_tool.path().as_os_str().is_empty(), "Found LLVM tool '{}' should have a non-empty path", tool ); // Verify the tool path actually exists on filesystem assert!( found_tool.path().exists(), "LLVM tool '{}' path should exist: {:?}", tool, found_tool.path() ); // Verify the tool path contains the expected tool name let path_str = found_tool.path().to_string_lossy(); assert!( path_str.contains(tool.trim_end_matches(".exe")), "Tool path '{}' should contain tool name '{}'", path_str, tool ); // Verify it's in the correct host-specific VS LLVM directory assert!( path_str.contains(expected_host_path) || path_str.contains(&expected_host_path.replace("\\", "/")), "LLVM tool should be in host-specific VS LLVM directory '{}' for {} host, but found: {}", expected_host_path, host_name, path_str ); } None => {} } } // On CI with VS + Clang installed, we should find at least some LLVM tools assert!( found_tools_count > 0, "Expected to find at least one LLVM tool on CI with Visual Studio + Clang installed for {} host. Found: {}", host_name, found_tools_count ); } } // Given a registry key, look at all the sub keys and find the one which has // the maximal numeric value. // // Returns the name of the maximal key as well as the opened maximal key. fn max_version(key: &RegistryKey) -> Option<(OsString, RegistryKey)> { let mut max_vers = 0; let mut max_key = None; for subkey in key.iter().filter_map(|k| k.ok()) { let val = subkey .to_str() .and_then(|s| s.trim_start_matches('v').replace('.', "").parse().ok()); let val = match val { Some(s) => s, None => continue, }; if val > max_vers { if let Ok(k) = key.open(&subkey) { max_vers = val; max_key = Some((subkey, k)); } } } max_key } #[inline(always)] pub(super) fn has_msbuild_version(version: &str, env_getter: &dyn EnvGetter) -> bool { match version { "17.0" => { find_msbuild_vs17(TargetArch::X64, env_getter).is_some() || find_msbuild_vs17(TargetArch::X86, env_getter).is_some() || find_msbuild_vs17(TargetArch::Arm64, env_getter).is_some() } "16.0" => { find_msbuild_vs16(TargetArch::X64, env_getter).is_some() || find_msbuild_vs16(TargetArch::X86, env_getter).is_some() || find_msbuild_vs16(TargetArch::Arm64, env_getter).is_some() } "15.0" => { find_msbuild_vs15(TargetArch::X64, env_getter).is_some() || find_msbuild_vs15(TargetArch::X86, env_getter).is_some() || find_msbuild_vs15(TargetArch::Arm64, env_getter).is_some() } "14.0" => LOCAL_MACHINE .open(&OsString::from(format!( "SOFTWARE\\Microsoft\\MSBuild\\ToolsVersions\\{}", version ))) .is_ok(), _ => false, } } pub(super) fn find_devenv(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { find_devenv_vs15(target, env_getter) } fn find_devenv_vs15(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { find_tool_in_vs15_path(r"Common7\IDE\devenv.exe", target, env_getter) } // see http://stackoverflow.com/questions/328017/path-to-msbuild pub(super) fn find_msbuild(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { // VS 15 (2017) changed how to locate msbuild if let Some(r) = find_msbuild_vs17(target, env_getter) { Some(r) } else if let Some(r) = find_msbuild_vs16(target, env_getter) { return Some(r); } else if let Some(r) = find_msbuild_vs15(target, env_getter) { return Some(r); } else { find_old_msbuild(target) } } fn find_msbuild_vs15(target: TargetArch, env_getter: &dyn EnvGetter) -> Option { find_tool_in_vs15_path(r"MSBuild\15.0\Bin\MSBuild.exe", target, env_getter) } fn find_old_msbuild(target: TargetArch) -> Option { let key = r"SOFTWARE\Microsoft\MSBuild\ToolsVersions"; LOCAL_MACHINE .open(key.as_ref()) .ok() .and_then(|key| { max_version(&key).and_then(|(_vers, key)| key.query_str("MSBuildToolsPath").ok()) }) .map(|path| { let mut path = PathBuf::from(path); path.push("MSBuild.exe"); let mut tool = Tool { tool: path, is_clang_cl: false, env: Vec::new(), }; if target == TargetArch::X64 { tool.env.push(("Platform".into(), "X64".into())); } tool }) } } /// Non-Windows Implementation. #[cfg(not(windows))] mod impl_ { use std::{env, ffi::OsStr, path::PathBuf}; use super::{EnvGetter, TargetArch}; use crate::Tool; /// Finding msbuild.exe tool under unix system is not currently supported. /// Maybe can check it using an environment variable looks like `MSBUILD_BIN`. #[inline(always)] pub(super) fn find_msbuild(_target: TargetArch, _: &dyn EnvGetter) -> Option { None } // Finding devenv.exe tool under unix system is not currently supported. // Maybe can check it using an environment variable looks like `DEVENV_BIN`. #[inline(always)] pub(super) fn find_devenv(_target: TargetArch, _: &dyn EnvGetter) -> Option { None } // Finding Clang/LLVM-related tools on unix systems is not currently supported. #[inline(always)] pub(super) fn find_llvm_tool( _tool: &str, _target: TargetArch, _: &dyn EnvGetter, ) -> Option { None } /// Attempt to find the tool using environment variables set by vcvars. pub(super) fn find_msvc_environment( tool: &str, _target: TargetArch, env_getter: &dyn EnvGetter, ) -> Option { // Early return if the environment doesn't contain a VC install. let vc_install_dir = env_getter.get_env("VCINSTALLDIR")?; let vs_install_dir = env_getter.get_env("VSINSTALLDIR")?; let get_tool = |install_dir: &OsStr| { env::split_paths(install_dir) .map(|p| p.join(tool)) .find(|p| p.exists()) .map(|path| Tool { tool: path, is_clang_cl: false, env: Vec::new(), }) }; // Take the path of tool for the vc install directory. get_tool(vc_install_dir.as_ref()) // Take the path of tool for the vs install directory. .or_else(|| get_tool(vs_install_dir.as_ref())) // Take the path of tool for the current path environment. .or_else(|| { env_getter .get_env("PATH") .as_ref() .map(|path| path.as_ref()) .and_then(get_tool) }) } #[inline(always)] pub(super) fn find_msvc_15plus( _tool: &str, _target: TargetArch, _: &dyn EnvGetter, ) -> Option { None } // For MSVC 14 we need to find the Universal CRT as well as either // the Windows 10 SDK or Windows 8.1 SDK. #[inline(always)] pub(super) fn find_msvc_14( _tool: &str, _target: TargetArch, _: &dyn EnvGetter, ) -> Option { None } #[inline(always)] pub(super) fn has_msbuild_version(_version: &str, _: &dyn EnvGetter) -> bool { false } #[inline(always)] pub(super) fn get_ucrt_dir() -> Option<(PathBuf, String)> { None } }