find_msvc_tools/
find_tools.rs

1// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution and at
3// http://rust-lang.org/COPYRIGHT.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! An internal use crate to looking for windows-specific tools:
12//! 1. On Windows host, probe the Windows Registry if needed;
13//! 2. On non-Windows host, check specified environment variables.
14
15#![allow(clippy::upper_case_acronyms)]
16
17use std::{
18    env,
19    ffi::{OsStr, OsString},
20    ops::Deref,
21    path::PathBuf,
22    process::Command,
23    sync::Arc,
24};
25
26use crate::Tool;
27
28/// The target provided by the user.
29#[derive(Copy, Clone, PartialEq, Eq)]
30enum TargetArch {
31    X86,
32    X64,
33    Arm,
34    Arm64,
35    Arm64ec,
36}
37impl TargetArch {
38    /// Parse the `TargetArch` from a str. Returns `None` if the arch is unrecognized.
39    fn new(arch: &str) -> Option<Self> {
40        // NOTE: Keep up to date with docs in [`find`].
41        match arch {
42            "x64" | "x86_64" => Some(Self::X64),
43            "arm64" | "aarch64" => Some(Self::Arm64),
44            "arm64ec" => Some(Self::Arm64ec),
45            "x86" | "i686" | "i586" => Some(Self::X86),
46            "arm" | "thumbv7a" => Some(Self::Arm),
47            _ => None,
48        }
49    }
50
51    #[cfg(windows)]
52    /// Gets the Visual Studio name for the architecture.
53    fn as_vs_arch(&self) -> &'static str {
54        match self {
55            Self::X64 => "x64",
56            Self::Arm64 | Self::Arm64ec => "arm64",
57            Self::X86 => "x86",
58            Self::Arm => "arm",
59        }
60    }
61}
62
63#[derive(Debug, Clone)]
64#[non_exhaustive]
65pub enum Env {
66    Owned(OsString),
67    Arced(Arc<OsStr>),
68}
69
70impl AsRef<OsStr> for Env {
71    fn as_ref(&self) -> &OsStr {
72        self.deref()
73    }
74}
75
76impl Deref for Env {
77    type Target = OsStr;
78
79    fn deref(&self) -> &Self::Target {
80        match self {
81            Env::Owned(os_str) => os_str,
82            Env::Arced(os_str) => os_str,
83        }
84    }
85}
86
87impl From<Env> for PathBuf {
88    fn from(env: Env) -> Self {
89        match env {
90            Env::Owned(os_str) => PathBuf::from(os_str),
91            Env::Arced(os_str) => PathBuf::from(os_str.deref()),
92        }
93    }
94}
95
96pub trait EnvGetter {
97    fn get_env(&self, name: &'static str) -> Option<Env>;
98}
99
100struct StdEnvGetter;
101
102impl EnvGetter for StdEnvGetter {
103    #[allow(clippy::disallowed_methods)]
104    fn get_env(&self, name: &'static str) -> Option<Env> {
105        env::var_os(name).map(Env::Owned)
106    }
107}
108
109/// Attempts to find a tool within an MSVC installation using the Windows
110/// registry as a point to search from.
111///
112/// The `arch_or_target` argument is the architecture or the Rust target name
113/// that the tool should work for (e.g. compile or link for). The supported
114/// architecture names are:
115/// - `"x64"` or `"x86_64"`
116/// - `"arm64"` or `"aarch64"`
117/// - `"arm64ec"`
118/// - `"x86"`, `"i586"` or `"i686"`
119/// - `"arm"` or `"thumbv7a"`
120///
121/// The `tool` argument is the tool to find. Supported tools include:
122/// - MSVC tools: `cl.exe`, `link.exe`, `lib.exe`, etc.
123/// - `MSBuild`: `msbuild.exe`
124/// - Visual Studio IDE: `devenv.exe`
125/// - Clang/LLVM tools: `clang.exe`, `clang++.exe`, `clang-*.exe`, `llvm-*.exe`, `lld.exe`, etc.
126///
127/// This function will return `None` if the tool could not be found, or it will
128/// return `Some(cmd)` which represents a command that's ready to execute the
129/// tool with the appropriate environment variables set.
130///
131/// To find MSVC tools, this function will first attempt to detect if we are
132/// running in the context of a developer command prompt, and then use the tools
133/// as found in the current `PATH`. If that fails, it will attempt to locate
134/// the newest MSVC toolset in the newest installed version of Visual Studio.
135/// To limit the search to a specific version of the MSVC toolset, set the
136/// VCToolsVersion environment variable to the desired version (e.g. "14.44.35207").
137///
138/// Note that this function always returns `None` for non-MSVC targets (if a
139/// full target name was specified).
140pub fn find(arch_or_target: &str, tool: &str) -> Option<Command> {
141    find_tool(arch_or_target, tool).map(|c| c.to_command())
142}
143
144/// Similar to the `find` function above, this function will attempt the same
145/// operation (finding a MSVC tool in a local install) but instead returns a
146/// `Tool` which may be introspected.
147pub fn find_tool(arch_or_target: &str, tool: &str) -> Option<Tool> {
148    let full_arch = if let Some((full_arch, rest)) = arch_or_target.split_once("-") {
149        // The logic is all tailored for MSVC, if the target is not that then
150        // bail out early.
151        if !rest.contains("msvc") {
152            return None;
153        }
154        full_arch
155    } else {
156        arch_or_target
157    };
158    find_tool_with_env(full_arch, tool, &StdEnvGetter)
159}
160
161pub fn find_tool_with_env(full_arch: &str, tool: &str, env_getter: &dyn EnvGetter) -> Option<Tool> {
162    // We only need the arch.
163    let target = TargetArch::new(full_arch)?;
164
165    // Looks like msbuild isn't located in the same location as other tools like
166    // cl.exe and lib.exe.
167    if tool.contains("msbuild") {
168        return impl_::find_msbuild(target, env_getter);
169    }
170
171    // Looks like devenv isn't located in the same location as other tools like
172    // cl.exe and lib.exe.
173    if tool.contains("devenv") {
174        return impl_::find_devenv(target, env_getter);
175    }
176
177    // Clang/LLVM isn't located in the same location as other tools like
178    // cl.exe and lib.exe.
179    if ["clang", "lldb", "llvm", "ld", "lld"]
180        .iter()
181        .any(|&t| tool.contains(t))
182    {
183        return impl_::find_llvm_tool(tool, target, env_getter);
184    }
185
186    // Ok, if we're here, now comes the fun part of the probing. Default shells
187    // or shells like MSYS aren't really configured to execute `cl.exe` and the
188    // various compiler tools shipped as part of Visual Studio. Here we try to
189    // first find the relevant tool, then we also have to be sure to fill in
190    // environment variables like `LIB`, `INCLUDE`, and `PATH` to ensure that
191    // the tool is actually usable.
192
193    impl_::find_msvc_environment(tool, target, env_getter)
194        .or_else(|| impl_::find_msvc_15plus(tool, target, env_getter))
195        .or_else(|| impl_::find_msvc_14(tool, target, env_getter))
196}
197
198/// A version of Visual Studio
199#[derive(Debug, PartialEq, Eq, Copy, Clone)]
200#[non_exhaustive]
201pub enum VsVers {
202    /// Visual Studio 12 (2013)
203    #[deprecated(
204        note = "Visual Studio 12 is no longer supported. cc will never return this value."
205    )]
206    Vs12,
207    /// Visual Studio 14 (2015)
208    Vs14,
209    /// Visual Studio 15 (2017)
210    Vs15,
211    /// Visual Studio 16 (2019)
212    Vs16,
213    /// Visual Studio 17 (2022)
214    Vs17,
215    /// Visual Studio 18 (2026)
216    Vs18,
217}
218
219/// Find the most recent installed version of Visual Studio
220///
221/// This is used by the cmake crate to figure out the correct
222/// generator.
223#[allow(clippy::disallowed_methods)]
224pub fn find_vs_version() -> Result<VsVers, String> {
225    fn has_msbuild_version(version: &str) -> bool {
226        impl_::has_msbuild_version(version, &StdEnvGetter)
227    }
228
229    match std::env::var("VisualStudioVersion") {
230        Ok(version) => match &version[..] {
231            "18.0" => Ok(VsVers::Vs18),
232            "17.0" => Ok(VsVers::Vs17),
233            "16.0" => Ok(VsVers::Vs16),
234            "15.0" => Ok(VsVers::Vs15),
235            "14.0" => Ok(VsVers::Vs14),
236            vers => Err(format!(
237                "\n\n\
238                 unsupported or unknown VisualStudio version: {vers}\n\
239                 if another version is installed consider running \
240                 the appropriate vcvars script before building this \
241                 crate\n\
242                 "
243            )),
244        },
245        _ => {
246            // Check for the presence of a specific registry key
247            // that indicates visual studio is installed.
248            if has_msbuild_version("18.0") {
249                Ok(VsVers::Vs18)
250            } else if has_msbuild_version("17.0") {
251                Ok(VsVers::Vs17)
252            } else if has_msbuild_version("16.0") {
253                Ok(VsVers::Vs16)
254            } else if has_msbuild_version("15.0") {
255                Ok(VsVers::Vs15)
256            } else if has_msbuild_version("14.0") {
257                Ok(VsVers::Vs14)
258            } else {
259                Err("\n\n\
260                     couldn't determine visual studio generator\n\
261                     if VisualStudio is installed, however, consider \
262                     running the appropriate vcvars script before building \
263                     this crate\n\
264                     "
265                .to_string())
266            }
267        }
268    }
269}
270
271/// To find the Universal CRT we look in a specific registry key for where
272/// all the Universal CRTs are located and then sort them asciibetically to
273/// find the newest version. While this sort of sorting isn't ideal,  it is
274/// what vcvars does so that's good enough for us.
275///
276/// Returns a pair of (root, version) for the ucrt dir if found
277pub fn get_ucrt_dir() -> Option<(PathBuf, String)> {
278    impl_::get_ucrt_dir()
279}
280
281/// Windows Implementation.
282#[cfg(windows)]
283mod impl_ {
284    use crate::com;
285    use crate::registry::{RegistryKey, LOCAL_MACHINE};
286    use crate::setup_config::SetupConfiguration;
287    use crate::vs_instances::{VsInstances, VswhereInstance};
288    use crate::windows_sys::{
289        GetMachineTypeAttributes, GetProcAddress, LoadLibraryA, UserEnabled, HMODULE,
290        IMAGE_FILE_MACHINE_AMD64, MACHINE_ATTRIBUTES, S_OK,
291    };
292    use std::convert::TryFrom;
293    use std::env;
294    use std::ffi::OsString;
295    use std::fs::File;
296    use std::io::Read;
297    use std::iter;
298    use std::mem;
299    use std::path::{Path, PathBuf};
300    use std::process::Command;
301    use std::str::FromStr;
302    use std::sync::atomic::{AtomicBool, Ordering};
303    use std::sync::Once;
304
305    use super::{EnvGetter, TargetArch};
306    use crate::Tool;
307
308    struct MsvcTool {
309        tool: PathBuf,
310        libs: Vec<PathBuf>,
311        path: Vec<PathBuf>,
312        include: Vec<PathBuf>,
313    }
314
315    #[derive(Default)]
316    struct SdkInfo {
317        libs: Vec<PathBuf>,
318        path: Vec<PathBuf>,
319        include: Vec<PathBuf>,
320    }
321
322    struct LibraryHandle(HMODULE);
323
324    impl LibraryHandle {
325        fn new(name: &[u8]) -> Option<Self> {
326            let handle = unsafe { LoadLibraryA(name.as_ptr() as _) };
327            (!handle.is_null()).then_some(Self(handle))
328        }
329
330        /// Get a function pointer to a function in the library.
331        /// # SAFETY
332        ///
333        /// The caller must ensure that the function signature matches the actual function.
334        /// The easiest way to do this is to add an entry to `windows_sys_no_link.list` and use the
335        /// generated function for `func_signature`.
336        ///
337        /// The function returned cannot be used after the handle is dropped.
338        unsafe fn get_proc_address<F>(&self, name: &[u8]) -> Option<F> {
339            let symbol = GetProcAddress(self.0, name.as_ptr() as _);
340            symbol.map(|symbol| mem::transmute_copy(&symbol))
341        }
342    }
343
344    type GetMachineTypeAttributesFuncType =
345        unsafe extern "system" fn(u16, *mut MACHINE_ATTRIBUTES) -> i32;
346    const _: () = {
347        // Ensure that our hand-written signature matches the actual function signature.
348        // We can't use `GetMachineTypeAttributes` outside of a const scope otherwise we'll end up statically linking to
349        // it, which will fail to load on older versions of Windows.
350        let _: GetMachineTypeAttributesFuncType = GetMachineTypeAttributes;
351    };
352
353    fn is_amd64_emulation_supported_inner() -> Option<bool> {
354        // GetMachineTypeAttributes is only available on Win11 22000+, so dynamically load it.
355        let kernel32 = LibraryHandle::new(b"kernel32.dll\0")?;
356        // SAFETY: GetMachineTypeAttributesFuncType is checked to match the real function signature.
357        let get_machine_type_attributes = unsafe {
358            kernel32
359                .get_proc_address::<GetMachineTypeAttributesFuncType>(b"GetMachineTypeAttributes\0")
360        }?;
361        let mut attributes = Default::default();
362        if unsafe { get_machine_type_attributes(IMAGE_FILE_MACHINE_AMD64, &mut attributes) } == S_OK
363        {
364            Some((attributes & UserEnabled) != 0)
365        } else {
366            Some(false)
367        }
368    }
369
370    fn is_amd64_emulation_supported() -> bool {
371        // TODO: Replace with a OnceLock once MSRV is 1.70.
372        static LOAD_VALUE: Once = Once::new();
373        static IS_SUPPORTED: AtomicBool = AtomicBool::new(false);
374
375        // Using Relaxed ordering since the Once is providing synchronization.
376        LOAD_VALUE.call_once(|| {
377            IS_SUPPORTED.store(
378                is_amd64_emulation_supported_inner().unwrap_or(false),
379                Ordering::Relaxed,
380            );
381        });
382        IS_SUPPORTED.load(Ordering::Relaxed)
383    }
384
385    impl MsvcTool {
386        fn new(tool: PathBuf) -> MsvcTool {
387            MsvcTool {
388                tool,
389                libs: Vec::new(),
390                path: Vec::new(),
391                include: Vec::new(),
392            }
393        }
394
395        fn add_sdk(&mut self, sdk_info: SdkInfo) {
396            self.libs.extend(sdk_info.libs);
397            self.path.extend(sdk_info.path);
398            self.include.extend(sdk_info.include);
399        }
400
401        fn into_tool(self, env_getter: &dyn EnvGetter) -> Tool {
402            let MsvcTool {
403                tool,
404                libs,
405                path,
406                include,
407            } = self;
408            let mut tool = Tool {
409                tool,
410                is_clang_cl: false,
411                env: Vec::new(),
412            };
413            add_env(&mut tool, "LIB", libs, env_getter);
414            add_env(&mut tool, "PATH", path, env_getter);
415            add_env(&mut tool, "INCLUDE", include, env_getter);
416            tool
417        }
418    }
419
420    impl SdkInfo {
421        fn find_tool(&self, tool: &str) -> Option<PathBuf> {
422            self.path.iter().map(|p| p.join(tool)).find(|p| p.exists())
423        }
424    }
425
426    /// Checks to see if the target's arch matches the VS environment. Returns `None` if the
427    /// environment is unknown.
428    fn is_vscmd_target(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<bool> {
429        is_vscmd_target_env(target, env_getter).or_else(|| is_vscmd_target_cl(target, env_getter))
430    }
431
432    /// Checks to see if the `VSCMD_ARG_TGT_ARCH` environment variable matches the
433    /// given target's arch. Returns `None` if the variable does not exist.
434    fn is_vscmd_target_env(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<bool> {
435        let vscmd_arch = env_getter.get_env("VSCMD_ARG_TGT_ARCH")?;
436        Some(target.as_vs_arch() == vscmd_arch.as_ref())
437    }
438
439    /// Checks if the cl.exe target matches the given target's arch. Returns `None` if anything
440    /// fails.
441    fn is_vscmd_target_cl(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<bool> {
442        let cmd_target = vscmd_target_cl(env_getter)?;
443        Some(target.as_vs_arch() == cmd_target)
444    }
445
446    /// Detect the target architecture of `cl.exe` in the current path, and return `None` if this
447    /// fails for any reason.
448    fn vscmd_target_cl(env_getter: &dyn EnvGetter) -> Option<&'static str> {
449        let cl_exe = env_getter.get_env("PATH").and_then(|path| {
450            env::split_paths(&path)
451                .map(|p| p.join("cl.exe"))
452                .find(|p| p.exists())
453        })?;
454        let mut cl = Command::new(cl_exe);
455        cl.stderr(std::process::Stdio::piped())
456            .stdout(std::process::Stdio::null());
457
458        let out = cl.output().ok()?;
459        let cl_arch = out
460            .stderr
461            .split(|&b| b == b'\n' || b == b'\r')
462            .next()?
463            .rsplit(|&b| b == b' ')
464            .next()?;
465
466        match cl_arch {
467            b"x64" => Some("x64"),
468            b"x86" => Some("x86"),
469            b"ARM64" => Some("arm64"),
470            b"ARM" => Some("arm"),
471            _ => None,
472        }
473    }
474
475    /// Attempt to find the tool using environment variables set by vcvars.
476    pub(super) fn find_msvc_environment(
477        tool: &str,
478        target: TargetArch,
479        env_getter: &dyn EnvGetter,
480    ) -> Option<Tool> {
481        // Early return if the environment isn't one that is known to have compiler toolsets in PATH
482        // `VCINSTALLDIR` is set from vcvarsall.bat (developer command prompt)
483        // `VSTEL_MSBuildProjectFullPath` is set by msbuild when invoking custom build steps
484        // NOTE: `VisualStudioDir` used to be used but this isn't set when invoking msbuild from the commandline
485        if env_getter.get_env("VCINSTALLDIR").is_none()
486            && env_getter.get_env("VSTEL_MSBuildProjectFullPath").is_none()
487        {
488            return None;
489        }
490
491        // If the vscmd target differs from the requested target then
492        // attempt to get the tool using the VS install directory.
493        if is_vscmd_target(target, env_getter) == Some(false) {
494            // We will only get here with versions 15+.
495            let vs_install_dir: PathBuf = env_getter.get_env("VSINSTALLDIR")?.into();
496            tool_from_vs15plus_instance(tool, target, &vs_install_dir, env_getter)
497        } else {
498            // Fallback to simply using the current environment.
499            env_getter
500                .get_env("PATH")
501                .and_then(|path| {
502                    env::split_paths(&path)
503                        .map(|p| p.join(tool))
504                        .find(|p| p.exists())
505                })
506                .map(|path| Tool {
507                    tool: path,
508                    is_clang_cl: false,
509                    env: Vec::new(),
510                })
511        }
512    }
513
514    fn find_msbuild_vs18(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
515        find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "18", env_getter)
516    }
517
518    fn find_msbuild_vs17(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
519        find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "17", env_getter)
520    }
521
522    #[allow(bare_trait_objects)]
523    fn vs16plus_instances(
524        target: TargetArch,
525        version: &'static str,
526        env_getter: &dyn EnvGetter,
527    ) -> Box<Iterator<Item = PathBuf>> {
528        let instances = if let Some(instances) = vs15plus_instances(target, env_getter) {
529            instances
530        } else {
531            return Box::new(iter::empty());
532        };
533        Box::new(instances.into_iter().filter_map(move |instance| {
534            let installation_name = instance.installation_name()?;
535            if installation_name.starts_with(&format!("VisualStudio/{}.", version))
536                || installation_name.starts_with(&format!("VisualStudioPreview/{}.", version))
537            {
538                Some(instance.installation_path()?)
539            } else {
540                None
541            }
542        }))
543    }
544
545    fn find_tool_in_vs16plus_path(
546        tool: &str,
547        target: TargetArch,
548        version: &'static str,
549        env_getter: &dyn EnvGetter,
550    ) -> Option<Tool> {
551        vs16plus_instances(target, version, env_getter)
552            .filter_map(|path| {
553                let path = path.join(tool);
554                if !path.is_file() {
555                    return None;
556                }
557                let mut tool = Tool {
558                    tool: path,
559                    is_clang_cl: false,
560                    env: Vec::new(),
561                };
562                if target == TargetArch::X64 {
563                    tool.env.push(("Platform".into(), "X64".into()));
564                }
565                if matches!(target, TargetArch::Arm64 | TargetArch::Arm64ec) {
566                    tool.env.push(("Platform".into(), "ARM64".into()));
567                }
568                Some(tool)
569            })
570            .next()
571    }
572
573    fn find_msbuild_vs16(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
574        find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "16", env_getter)
575    }
576
577    pub(super) fn find_llvm_tool(
578        tool: &str,
579        target: TargetArch,
580        env_getter: &dyn EnvGetter,
581    ) -> Option<Tool> {
582        find_llvm_tool_vs17plus(tool, target, env_getter, "18")
583            .or_else(|| find_llvm_tool_vs17plus(tool, target, env_getter, "17"))
584    }
585
586    fn find_llvm_tool_vs17plus(
587        tool: &str,
588        target: TargetArch,
589        env_getter: &dyn EnvGetter,
590        version: &'static str,
591    ) -> Option<Tool> {
592        vs16plus_instances(target, version, env_getter)
593            .filter_map(|mut base_path| {
594                base_path.push(r"VC\Tools\LLVM");
595                let host_folder = match host_arch() {
596                    // The default LLVM bin folder is x86, and there's separate subfolders
597                    // for the x64 and ARM64 host tools.
598                    X86 => "",
599                    X86_64 => "x64",
600                    AARCH64 => "ARM64",
601                    _ => return None,
602                };
603                if host_folder != "" {
604                    // E.g. C:\...\VC\Tools\LLVM\x64
605                    base_path.push(host_folder);
606                }
607                // E.g. C:\...\VC\Tools\LLVM\x64\bin\clang.exe
608                base_path.push("bin");
609                base_path.push(tool);
610                let is_clang_cl = tool.contains("clang-cl");
611                base_path.is_file().then(|| Tool {
612                    tool: base_path,
613                    is_clang_cl,
614                    env: Vec::new(),
615                })
616            })
617            .next()
618    }
619
620    // In MSVC 15 (2017) MS once again changed the scheme for locating
621    // the tooling.  Now we must go through some COM interfaces, which
622    // is super fun for Rust.
623    //
624    // Note that much of this logic can be found [online] wrt paths, COM, etc.
625    //
626    // [online]: https://blogs.msdn.microsoft.com/vcblog/2017/03/06/finding-the-visual-c-compiler-tools-in-visual-studio-2017/
627    //
628    // Returns MSVC 15+ instances (15, 16 right now), the order should be consider undefined.
629    //
630    // However, on ARM64 this method doesn't work because VS Installer fails to register COM component on ARM64.
631    // Hence, as the last resort we try to use vswhere.exe to list available instances.
632    fn vs15plus_instances(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<VsInstances> {
633        vs15plus_instances_using_com()
634            .or_else(|| vs15plus_instances_using_vswhere(target, env_getter))
635    }
636
637    fn vs15plus_instances_using_com() -> Option<VsInstances> {
638        com::initialize().ok()?;
639
640        let config = SetupConfiguration::new().ok()?;
641        let enum_setup_instances = config.enum_all_instances().ok()?;
642
643        Some(VsInstances::ComBased(enum_setup_instances))
644    }
645
646    fn vs15plus_instances_using_vswhere(
647        target: TargetArch,
648        env_getter: &dyn EnvGetter,
649    ) -> Option<VsInstances> {
650        let program_files_path = env_getter
651            .get_env("ProgramFiles(x86)")
652            .or_else(|| env_getter.get_env("ProgramFiles"))?;
653
654        let program_files_path = Path::new(program_files_path.as_ref());
655
656        let vswhere_path =
657            program_files_path.join(r"Microsoft Visual Studio\Installer\vswhere.exe");
658
659        if !vswhere_path.exists() {
660            return None;
661        }
662
663        let tools_arch = match target {
664            TargetArch::X86 | TargetArch::X64 => Some("x86.x64"),
665            TargetArch::Arm => Some("ARM"),
666            TargetArch::Arm64 | TargetArch::Arm64ec => Some("ARM64"),
667        };
668
669        let vswhere_output = Command::new(vswhere_path)
670            .args([
671                "-latest",
672                "-products",
673                "*",
674                "-requires",
675                &format!("Microsoft.VisualStudio.Component.VC.Tools.{}", tools_arch?),
676                "-format",
677                "text",
678                "-nologo",
679            ])
680            .stderr(std::process::Stdio::inherit())
681            .output()
682            .ok()?;
683
684        let vs_instances =
685            VsInstances::VswhereBased(VswhereInstance::try_from(&vswhere_output.stdout).ok()?);
686
687        Some(vs_instances)
688    }
689
690    // Inspired from official microsoft/vswhere ParseVersionString
691    // i.e. at most four u16 numbers separated by '.'
692    fn parse_version(version: &str) -> Option<[u16; 4]> {
693        let mut iter = version.split('.').map(u16::from_str).fuse();
694        let mut get_next_number = move || match iter.next() {
695            Some(Ok(version_part)) => Some(version_part),
696            Some(Err(_)) => None,
697            None => Some(0),
698        };
699        Some([
700            get_next_number()?,
701            get_next_number()?,
702            get_next_number()?,
703            get_next_number()?,
704        ])
705    }
706
707    pub(super) fn find_msvc_15plus(
708        tool: &str,
709        target: TargetArch,
710        env_getter: &dyn EnvGetter,
711    ) -> Option<Tool> {
712        let iter = vs15plus_instances(target, env_getter)?;
713        iter.into_iter()
714            .filter_map(|instance| {
715                let version = parse_version(&instance.installation_version()?)?;
716                let instance_path = instance.installation_path()?;
717                let tool = tool_from_vs15plus_instance(tool, target, &instance_path, env_getter)?;
718                Some((version, tool))
719            })
720            .max_by(|(a_version, _), (b_version, _)| a_version.cmp(b_version))
721            .map(|(_version, tool)| tool)
722    }
723
724    // While the paths to Visual Studio 2017's devenv and MSBuild could
725    // potentially be retrieved from the registry, finding them via
726    // SetupConfiguration has shown to be [more reliable], and is preferred
727    // according to Microsoft. To help head off potential regressions though,
728    // we keep the registry method as a fallback option.
729    //
730    // [more reliable]: https://github.com/rust-lang/cc-rs/pull/331
731    fn find_tool_in_vs15_path(
732        tool: &str,
733        target: TargetArch,
734        env_getter: &dyn EnvGetter,
735    ) -> Option<Tool> {
736        let mut path = match vs15plus_instances(target, env_getter) {
737            Some(instances) => instances
738                .into_iter()
739                .filter_map(|instance| instance.installation_path())
740                .map(|path| path.join(tool))
741                .find(|path| path.is_file()),
742            None => None,
743        };
744
745        if path.is_none() {
746            let key = r"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\SxS\VS7";
747            path = LOCAL_MACHINE
748                .open(key.as_ref())
749                .ok()
750                .and_then(|key| key.query_str("15.0").ok())
751                .map(|path| PathBuf::from(path).join(tool))
752                .and_then(|path| if path.is_file() { Some(path) } else { None });
753        }
754
755        path.map(|path| {
756            let mut tool = Tool {
757                tool: path,
758                is_clang_cl: false,
759                env: Vec::new(),
760            };
761            if target == TargetArch::X64 {
762                tool.env.push(("Platform".into(), "X64".into()));
763            } else if matches!(target, TargetArch::Arm64 | TargetArch::Arm64ec) {
764                tool.env.push(("Platform".into(), "ARM64".into()));
765            }
766            tool
767        })
768    }
769
770    fn tool_from_vs15plus_instance(
771        tool: &str,
772        target: TargetArch,
773        instance_path: &Path,
774        env_getter: &dyn EnvGetter,
775    ) -> Option<Tool> {
776        let (root_path, bin_path, host_dylib_path, lib_path, alt_lib_path, include_path) =
777            vs15plus_vc_paths(target, instance_path, env_getter)?;
778        let sdk_info = get_sdks(target, env_getter)?;
779        let mut tool_path = bin_path.join(tool);
780        if !tool_path.exists() {
781            tool_path = sdk_info.find_tool(tool)?;
782        };
783
784        let mut tool = MsvcTool::new(tool_path);
785        tool.path.push(bin_path.clone());
786        tool.path.push(host_dylib_path);
787        if let Some(alt_lib_path) = alt_lib_path {
788            tool.libs.push(alt_lib_path);
789        }
790        tool.libs.push(lib_path);
791        tool.include.push(include_path);
792
793        if let Some((atl_lib_path, atl_include_path)) = atl_paths(target, &root_path) {
794            tool.libs.push(atl_lib_path);
795            tool.include.push(atl_include_path);
796        }
797
798        tool.add_sdk(sdk_info);
799
800        Some(tool.into_tool(env_getter))
801    }
802
803    fn vs15plus_vc_paths(
804        target_arch: TargetArch,
805        instance_path: &Path,
806        env_getter: &dyn EnvGetter,
807    ) -> Option<(PathBuf, PathBuf, PathBuf, PathBuf, Option<PathBuf>, PathBuf)> {
808        let version = vs15plus_vc_read_version(instance_path, env_getter)?;
809
810        let hosts = match host_arch() {
811            X86 => &["X86"],
812            X86_64 => &["X64"],
813            // Starting with VS 17.4, there is a natively hosted compiler on ARM64:
814            // https://devblogs.microsoft.com/visualstudio/arm64-visual-studio-is-officially-here/
815            // On older versions of VS, we use x64 if running under emulation is supported,
816            // otherwise use x86.
817            AARCH64 => {
818                if is_amd64_emulation_supported() {
819                    &["ARM64", "X64", "X86"][..]
820                } else {
821                    &["ARM64", "X86"]
822                }
823            }
824            _ => return None,
825        };
826        let target_dir = target_arch.as_vs_arch();
827        // The directory layout here is MSVC/bin/Host$host/$target/
828        let path = instance_path.join(r"VC\Tools\MSVC").join(version);
829        // We use the first available host architecture that can build for the target
830        let (host_path, host) = hosts.iter().find_map(|&x| {
831            let candidate = path.join("bin").join(format!("Host{}", x));
832            if candidate.join(target_dir).exists() {
833                Some((candidate, x))
834            } else {
835                None
836            }
837        })?;
838        // This is the path to the toolchain for a particular target, running
839        // on a given host
840        let bin_path = host_path.join(target_dir);
841        // But! we also need PATH to contain the target directory for the host
842        // architecture, because it contains dlls like mspdb140.dll compiled for
843        // the host architecture.
844        let host_dylib_path = host_path.join(host.to_lowercase());
845        let lib_fragment = if use_spectre_mitigated_libs(env_getter) {
846            r"lib\spectre"
847        } else {
848            "lib"
849        };
850        let lib_path = path.join(lib_fragment).join(target_dir);
851        let alt_lib_path =
852            (target_arch == TargetArch::Arm64ec).then(|| path.join(lib_fragment).join("arm64ec"));
853        let include_path = path.join("include");
854        Some((
855            path,
856            bin_path,
857            host_dylib_path,
858            lib_path,
859            alt_lib_path,
860            include_path,
861        ))
862    }
863
864    fn vs15plus_vc_read_version(dir: &Path, env_getter: &dyn EnvGetter) -> Option<String> {
865        if let Some(version) = env_getter.get_env("VCToolsVersion") {
866            // Restrict the search to a specific msvc version; if it doesn't exist then
867            // our caller will fail to find the tool for this instance and move on.
868            return version.to_str().map(ToString::to_string);
869        }
870
871        // Try to open the default version file.
872        let mut version_path: PathBuf =
873            dir.join(r"VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt");
874        let mut version_file = if let Ok(f) = File::open(&version_path) {
875            f
876        } else {
877            // If the default doesn't exist, search for other version files.
878            // These are in the form Microsoft.VCToolsVersion.v143.default.txt
879            // where `143` is any three decimal digit version number.
880            // This sorts versions by lexical order and selects the highest version.
881            let mut version_file = String::new();
882            version_path.pop();
883            for file in version_path.read_dir().ok()? {
884                let name = file.ok()?.file_name();
885                let name = name.to_str()?;
886                if name.starts_with("Microsoft.VCToolsVersion.v")
887                    && name.ends_with(".default.txt")
888                    && name > &version_file
889                {
890                    version_file.replace_range(.., name);
891                }
892            }
893            if version_file.is_empty() {
894                // If all else fails, manually search for bin directories.
895                let tools_dir: PathBuf = dir.join(r"VC\Tools\MSVC");
896                return tools_dir
897                    .read_dir()
898                    .ok()?
899                    .filter_map(|file| {
900                        let file = file.ok()?;
901                        let name = file.file_name().into_string().ok()?;
902
903                        file.path().join("bin").exists().then(|| {
904                            let version = parse_version(&name);
905                            (name, version)
906                        })
907                    })
908                    .max_by(|(_, a), (_, b)| a.cmp(b))
909                    .map(|(version, _)| version);
910            }
911            version_path.push(version_file);
912            File::open(version_path).ok()?
913        };
914
915        // Get the version string from the file we found.
916        let mut version = String::new();
917        version_file.read_to_string(&mut version).ok()?;
918        version.truncate(version.trim_end().len());
919        Some(version)
920    }
921
922    fn use_spectre_mitigated_libs(env_getter: &dyn EnvGetter) -> bool {
923        env_getter
924            .get_env("VSCMD_ARG_VCVARS_SPECTRE")
925            .map(|env| env.as_ref() == "spectre")
926            .unwrap_or_default()
927    }
928
929    fn atl_paths(target: TargetArch, path: &Path) -> Option<(PathBuf, PathBuf)> {
930        let atl_path = path.join("atlmfc");
931        let sub = target.as_vs_arch();
932        if atl_path.exists() {
933            Some((atl_path.join("lib").join(sub), atl_path.join("include")))
934        } else {
935            None
936        }
937    }
938
939    // For MSVC 14 we need to find the Universal CRT as well as either
940    // the Windows 10 SDK or Windows 8.1 SDK.
941    pub(super) fn find_msvc_14(
942        tool: &str,
943        target: TargetArch,
944        env_getter: &dyn EnvGetter,
945    ) -> Option<Tool> {
946        if env_getter.get_env("VCToolsVersion").is_some() {
947            // VCToolsVersion is not set/supported for MSVC 14
948            return None;
949        }
950
951        let vcdir = get_vc_dir("14.0")?;
952        let sdk_info = get_sdks(target, env_getter)?;
953        let mut tool = get_tool(tool, &vcdir, target, &sdk_info)?;
954        tool.add_sdk(sdk_info);
955        Some(tool.into_tool(env_getter))
956    }
957
958    fn get_sdks(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<SdkInfo> {
959        let sub = target.as_vs_arch();
960        let (ucrt, ucrt_version) = get_ucrt_dir()?;
961
962        let host = match host_arch() {
963            X86 => "x86",
964            X86_64 => "x64",
965            AARCH64 => "arm64",
966            _ => return None,
967        };
968
969        let mut info = SdkInfo::default();
970
971        info.path
972            .push(ucrt.join("bin").join(&ucrt_version).join(host));
973
974        let ucrt_include = ucrt.join("include").join(&ucrt_version);
975        info.include.push(ucrt_include.join("ucrt"));
976
977        let ucrt_lib = ucrt.join("lib").join(&ucrt_version);
978        info.libs.push(ucrt_lib.join("ucrt").join(sub));
979
980        if let Some((sdk, version)) = get_sdk10_dir(env_getter) {
981            info.path.push(sdk.join("bin").join(host));
982            let sdk_lib = sdk.join("lib").join(&version);
983            info.libs.push(sdk_lib.join("um").join(sub));
984            let sdk_include = sdk.join("include").join(&version);
985            info.include.push(sdk_include.join("um"));
986            info.include.push(sdk_include.join("cppwinrt"));
987            info.include.push(sdk_include.join("winrt"));
988            info.include.push(sdk_include.join("shared"));
989        } else if let Some(sdk) = get_sdk81_dir() {
990            info.path.push(sdk.join("bin").join(host));
991            let sdk_lib = sdk.join("lib").join("winv6.3");
992            info.libs.push(sdk_lib.join("um").join(sub));
993            let sdk_include = sdk.join("include");
994            info.include.push(sdk_include.join("um"));
995            info.include.push(sdk_include.join("winrt"));
996            info.include.push(sdk_include.join("shared"));
997        }
998
999        Some(info)
1000    }
1001
1002    fn add_env(
1003        tool: &mut Tool,
1004        env: &'static str,
1005        paths: Vec<PathBuf>,
1006        env_getter: &dyn EnvGetter,
1007    ) {
1008        let prev = env_getter.get_env(env);
1009        let prev = prev.as_ref().map(AsRef::as_ref).unwrap_or_default();
1010        let prev = env::split_paths(&prev);
1011        let new = paths.into_iter().chain(prev);
1012        tool.env
1013            .push((env.to_string().into(), env::join_paths(new).unwrap()));
1014    }
1015
1016    // Given a possible MSVC installation directory, we look for the linker and
1017    // then add the MSVC library path.
1018    fn get_tool(
1019        tool: &str,
1020        path: &Path,
1021        target: TargetArch,
1022        sdk_info: &SdkInfo,
1023    ) -> Option<MsvcTool> {
1024        bin_subdir(target)
1025            .into_iter()
1026            .map(|(sub, host)| {
1027                (
1028                    path.join("bin").join(sub).join(tool),
1029                    Some(path.join("bin").join(host)),
1030                )
1031            })
1032            .filter(|(path, _)| path.is_file())
1033            .chain(iter::once_with(|| Some((sdk_info.find_tool(tool)?, None))).flatten())
1034            .map(|(tool_path, host)| {
1035                let mut tool = MsvcTool::new(tool_path);
1036                tool.path.extend(host);
1037                let sub = vc_lib_subdir(target);
1038                tool.libs.push(path.join("lib").join(sub));
1039                tool.include.push(path.join("include"));
1040                let atlmfc_path = path.join("atlmfc");
1041                if atlmfc_path.exists() {
1042                    tool.libs.push(atlmfc_path.join("lib").join(sub));
1043                    tool.include.push(atlmfc_path.join("include"));
1044                }
1045                tool
1046            })
1047            .next()
1048    }
1049
1050    // To find MSVC we look in a specific registry key for the version we are
1051    // trying to find.
1052    fn get_vc_dir(ver: &str) -> Option<PathBuf> {
1053        let key = r"SOFTWARE\Microsoft\VisualStudio\SxS\VC7";
1054        let key = LOCAL_MACHINE.open(key.as_ref()).ok()?;
1055        let path = key.query_str(ver).ok()?;
1056        Some(path.into())
1057    }
1058
1059    // To find the Universal CRT we look in a specific registry key for where
1060    // all the Universal CRTs are located and then sort them asciibetically to
1061    // find the newest version. While this sort of sorting isn't ideal,  it is
1062    // what vcvars does so that's good enough for us.
1063    //
1064    // Returns a pair of (root, version) for the ucrt dir if found
1065    pub(super) fn get_ucrt_dir() -> Option<(PathBuf, String)> {
1066        let key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
1067        let key = LOCAL_MACHINE.open(key.as_ref()).ok()?;
1068        let root = key.query_str("KitsRoot10").ok()?;
1069        let readdir = Path::new(&root).join("lib").read_dir().ok()?;
1070        let max_libdir = readdir
1071            .filter_map(|dir| dir.ok())
1072            .map(|dir| dir.path())
1073            .filter(|dir| {
1074                dir.components()
1075                    .last()
1076                    .and_then(|c| c.as_os_str().to_str())
1077                    .map(|c| c.starts_with("10.") && dir.join("ucrt").is_dir())
1078                    .unwrap_or(false)
1079            })
1080            .max()?;
1081        let version = max_libdir.components().last().unwrap();
1082        let version = version.as_os_str().to_str().unwrap().to_string();
1083        Some((root.into(), version))
1084    }
1085
1086    // Vcvars finds the correct version of the Windows 10 SDK by looking
1087    // for the include `um\Windows.h` because sometimes a given version will
1088    // only have UCRT bits without the rest of the SDK. Since we only care about
1089    // libraries and not includes, we instead look for `um\x64\kernel32.lib`.
1090    // Since the 32-bit and 64-bit libraries are always installed together we
1091    // only need to bother checking x64, making this code a tiny bit simpler.
1092    // Like we do for the Universal CRT, we sort the possibilities
1093    // asciibetically to find the newest one as that is what vcvars does.
1094    // Before doing that, we check the "WindowsSdkDir" and "WindowsSDKVersion"
1095    // environment variables set by vcvars to use the environment sdk version
1096    // if one is already configured.
1097    fn get_sdk10_dir(env_getter: &dyn EnvGetter) -> Option<(PathBuf, String)> {
1098        if let (Some(root), Some(version)) = (
1099            env_getter.get_env("WindowsSdkDir"),
1100            env_getter
1101                .get_env("WindowsSDKVersion")
1102                .as_ref()
1103                .and_then(|version| version.as_ref().to_str()),
1104        ) {
1105            return Some((
1106                PathBuf::from(root),
1107                version.trim_end_matches('\\').to_string(),
1108            ));
1109        }
1110
1111        let key = r"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0";
1112        let key = LOCAL_MACHINE.open(key.as_ref()).ok()?;
1113        let root = key.query_str("InstallationFolder").ok()?;
1114        let readdir = Path::new(&root).join("lib").read_dir().ok()?;
1115        let mut dirs = readdir
1116            .filter_map(|dir| dir.ok())
1117            .map(|dir| dir.path())
1118            .collect::<Vec<_>>();
1119        dirs.sort();
1120        let dir = dirs
1121            .into_iter()
1122            .rev()
1123            .find(|dir| dir.join("um").join("x64").join("kernel32.lib").is_file())?;
1124        let version = dir.components().last().unwrap();
1125        let version = version.as_os_str().to_str().unwrap().to_string();
1126        Some((root.into(), version))
1127    }
1128
1129    // Interestingly there are several subdirectories, `win7` `win8` and
1130    // `winv6.3`. Vcvars seems to only care about `winv6.3` though, so the same
1131    // applies to us. Note that if we were targeting kernel mode drivers
1132    // instead of user mode applications, we would care.
1133    fn get_sdk81_dir() -> Option<PathBuf> {
1134        let key = r"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.1";
1135        let key = LOCAL_MACHINE.open(key.as_ref()).ok()?;
1136        let root = key.query_str("InstallationFolder").ok()?;
1137        Some(root.into())
1138    }
1139
1140    const PROCESSOR_ARCHITECTURE_INTEL: u16 = 0;
1141    const PROCESSOR_ARCHITECTURE_AMD64: u16 = 9;
1142    const PROCESSOR_ARCHITECTURE_ARM64: u16 = 12;
1143    const X86: u16 = PROCESSOR_ARCHITECTURE_INTEL;
1144    const X86_64: u16 = PROCESSOR_ARCHITECTURE_AMD64;
1145    const AARCH64: u16 = PROCESSOR_ARCHITECTURE_ARM64;
1146
1147    // When choosing the tool to use, we have to choose the one which matches
1148    // the target architecture. Otherwise we end up in situations where someone
1149    // on 32-bit Windows is trying to cross compile to 64-bit and it tries to
1150    // invoke the native 64-bit compiler which won't work.
1151    //
1152    // For the return value of this function, the first member of the tuple is
1153    // the folder of the tool we will be invoking, while the second member is
1154    // the folder of the host toolchain for that tool which is essential when
1155    // using a cross linker. We return a Vec since on x64 there are often two
1156    // linkers that can target the architecture we desire. The 64-bit host
1157    // linker is preferred, and hence first, due to 64-bit allowing it more
1158    // address space to work with and potentially being faster.
1159    fn bin_subdir(target: TargetArch) -> Vec<(&'static str, &'static str)> {
1160        match (target, host_arch()) {
1161            (TargetArch::X86, X86) => vec![("", "")],
1162            (TargetArch::X86, X86_64) => vec![("amd64_x86", "amd64"), ("", "")],
1163            (TargetArch::X64, X86) => vec![("x86_amd64", "")],
1164            (TargetArch::X64, X86_64) => vec![("amd64", "amd64"), ("x86_amd64", "")],
1165            (TargetArch::Arm, X86) => vec![("x86_arm", "")],
1166            (TargetArch::Arm, X86_64) => vec![("amd64_arm", "amd64"), ("x86_arm", "")],
1167            _ => vec![],
1168        }
1169    }
1170
1171    // MSVC's x86 libraries are not in a subfolder
1172    fn vc_lib_subdir(target: TargetArch) -> &'static str {
1173        match target {
1174            TargetArch::X86 => "",
1175            TargetArch::X64 => "amd64",
1176            TargetArch::Arm => "arm",
1177            TargetArch::Arm64 | TargetArch::Arm64ec => "arm64",
1178        }
1179    }
1180
1181    #[allow(bad_style)]
1182    fn host_arch() -> u16 {
1183        type DWORD = u32;
1184        type WORD = u16;
1185        type LPVOID = *mut u8;
1186        type DWORD_PTR = usize;
1187
1188        #[repr(C)]
1189        struct SYSTEM_INFO {
1190            wProcessorArchitecture: WORD,
1191            _wReserved: WORD,
1192            _dwPageSize: DWORD,
1193            _lpMinimumApplicationAddress: LPVOID,
1194            _lpMaximumApplicationAddress: LPVOID,
1195            _dwActiveProcessorMask: DWORD_PTR,
1196            _dwNumberOfProcessors: DWORD,
1197            _dwProcessorType: DWORD,
1198            _dwAllocationGranularity: DWORD,
1199            _wProcessorLevel: WORD,
1200            _wProcessorRevision: WORD,
1201        }
1202
1203        extern "system" {
1204            fn GetNativeSystemInfo(lpSystemInfo: *mut SYSTEM_INFO);
1205        }
1206
1207        unsafe {
1208            let mut info = mem::zeroed();
1209            GetNativeSystemInfo(&mut info);
1210            info.wProcessorArchitecture
1211        }
1212    }
1213
1214    #[cfg(test)]
1215    mod tests {
1216        use super::*;
1217        use std::path::Path;
1218        // Import the find function from the module level
1219        use crate::find_tools::find;
1220
1221        fn host_arch_to_string(host_arch_value: u16) -> &'static str {
1222            match host_arch_value {
1223                X86 => "x86",
1224                X86_64 => "x64",
1225                AARCH64 => "arm64",
1226                _ => panic!("Unsupported host architecture: {}", host_arch_value),
1227            }
1228        }
1229
1230        #[test]
1231        fn test_find_cl_exe() {
1232            // Test that we can find cl.exe for common target architectures
1233            // and validate the correct host-target combination paths
1234            // This should pass on Windows CI with Visual Studio installed
1235
1236            let target_architectures = ["x64", "x86", "arm64"];
1237            let mut found_any = false;
1238
1239            // Determine the host architecture
1240            let host_arch_value = host_arch();
1241            let host_name = host_arch_to_string(host_arch_value);
1242
1243            for &target_arch in &target_architectures {
1244                if let Some(cmd) = find(target_arch, "cl.exe") {
1245                    // Verify the command looks valid
1246                    assert!(
1247                        !cmd.get_program().is_empty(),
1248                        "cl.exe program path should not be empty"
1249                    );
1250                    assert!(
1251                        Path::new(cmd.get_program()).exists(),
1252                        "cl.exe should exist at: {:?}",
1253                        cmd.get_program()
1254                    );
1255
1256                    // Verify the path contains the correct host-target combination
1257                    // Use case-insensitive comparison since VS IDE uses "Hostx64" while Build Tools use "HostX64"
1258                    let path_str = cmd.get_program().to_string_lossy();
1259                    let path_str_lower = path_str.to_lowercase();
1260                    let expected_host_target_path =
1261                        format!("\\bin\\host{host_name}\\{target_arch}");
1262                    let expected_host_target_path_unix =
1263                        expected_host_target_path.replace("\\", "/");
1264
1265                    assert!(
1266                        path_str_lower.contains(&expected_host_target_path) || path_str_lower.contains(&expected_host_target_path_unix),
1267                        "cl.exe path should contain host-target combination (case-insensitive) '{}' for {} host targeting {}, but found: {}",
1268                        expected_host_target_path,
1269                        host_name,
1270                        target_arch,
1271                        path_str
1272                    );
1273
1274                    found_any = true;
1275                }
1276            }
1277
1278            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");
1279        }
1280
1281        #[test]
1282        #[cfg(not(disable_clang_cl_tests))]
1283        fn test_find_llvm_tools() {
1284            // Import StdEnvGetter from the parent module
1285            use crate::find_tools::StdEnvGetter;
1286
1287            // Test the actual find_llvm_tool function with various LLVM tools
1288            // This test assumes CI environment has Visual Studio + Clang installed
1289            // We test against x64 target since clang can cross-compile to any target
1290            let target_arch = TargetArch::new("x64").expect("Should support x64 architecture");
1291            let llvm_tools = ["clang.exe", "clang++.exe", "lld.exe", "llvm-ar.exe"];
1292
1293            // Determine expected host-specific path based on host architecture
1294            let host_arch_value = host_arch();
1295            let expected_host_path = match host_arch_value {
1296                X86 => "LLVM\\bin",            // x86 host
1297                X86_64 => "LLVM\\x64\\bin",    // x64 host
1298                AARCH64 => "LLVM\\ARM64\\bin", // arm64 host
1299                _ => panic!("Unsupported host architecture: {}", host_arch_value),
1300            };
1301
1302            let host_name = host_arch_to_string(host_arch_value);
1303
1304            let mut found_tools_count = 0;
1305
1306            for &tool in &llvm_tools {
1307                // Test finding LLVM tools using the standard environment getter
1308                let env_getter = StdEnvGetter;
1309                let result = find_llvm_tool(tool, target_arch, &env_getter);
1310
1311                match result {
1312                    Some(found_tool) => {
1313                        found_tools_count += 1;
1314
1315                        // Verify the found tool has a valid, non-empty path
1316                        assert!(
1317                            !found_tool.path().as_os_str().is_empty(),
1318                            "Found LLVM tool '{}' should have a non-empty path",
1319                            tool
1320                        );
1321
1322                        // Verify the tool path actually exists on filesystem
1323                        assert!(
1324                            found_tool.path().exists(),
1325                            "LLVM tool '{}' path should exist: {:?}",
1326                            tool,
1327                            found_tool.path()
1328                        );
1329
1330                        // Verify the tool path contains the expected tool name
1331                        let path_str = found_tool.path().to_string_lossy();
1332                        assert!(
1333                            path_str.contains(tool.trim_end_matches(".exe")),
1334                            "Tool path '{}' should contain tool name '{}'",
1335                            path_str,
1336                            tool
1337                        );
1338
1339                        // Verify it's in the correct host-specific VS LLVM directory
1340                        assert!(
1341                            path_str.contains(expected_host_path) || path_str.contains(&expected_host_path.replace("\\", "/")),
1342                            "LLVM tool should be in host-specific VS LLVM directory '{}' for {} host, but found: {}",
1343                            expected_host_path,
1344                            host_name,
1345                            path_str
1346                        );
1347                    }
1348                    None => {}
1349                }
1350            }
1351
1352            // On CI with VS + Clang installed, we should find at least some LLVM tools
1353            assert!(
1354                found_tools_count > 0,
1355                "Expected to find at least one LLVM tool on CI with Visual Studio + Clang installed for {} host. Found: {}",
1356                host_name,
1357                found_tools_count
1358            );
1359        }
1360    }
1361
1362    // Given a registry key, look at all the sub keys and find the one which has
1363    // the maximal numeric value.
1364    //
1365    // Returns the name of the maximal key as well as the opened maximal key.
1366    fn max_version(key: &RegistryKey) -> Option<(OsString, RegistryKey)> {
1367        let mut max_vers = 0;
1368        let mut max_key = None;
1369        for subkey in key.iter().filter_map(|k| k.ok()) {
1370            let val = subkey
1371                .to_str()
1372                .and_then(|s| s.trim_start_matches('v').replace('.', "").parse().ok());
1373            let val = match val {
1374                Some(s) => s,
1375                None => continue,
1376            };
1377            if val > max_vers {
1378                if let Ok(k) = key.open(&subkey) {
1379                    max_vers = val;
1380                    max_key = Some((subkey, k));
1381                }
1382            }
1383        }
1384        max_key
1385    }
1386
1387    #[inline(always)]
1388    pub(super) fn has_msbuild_version(version: &str, env_getter: &dyn EnvGetter) -> bool {
1389        match version {
1390            "18.0" => {
1391                find_msbuild_vs18(TargetArch::X64, env_getter).is_some()
1392                    || find_msbuild_vs18(TargetArch::X86, env_getter).is_some()
1393                    || find_msbuild_vs18(TargetArch::Arm64, env_getter).is_some()
1394            }
1395            "17.0" => {
1396                find_msbuild_vs17(TargetArch::X64, env_getter).is_some()
1397                    || find_msbuild_vs17(TargetArch::X86, env_getter).is_some()
1398                    || find_msbuild_vs17(TargetArch::Arm64, env_getter).is_some()
1399            }
1400            "16.0" => {
1401                find_msbuild_vs16(TargetArch::X64, env_getter).is_some()
1402                    || find_msbuild_vs16(TargetArch::X86, env_getter).is_some()
1403                    || find_msbuild_vs16(TargetArch::Arm64, env_getter).is_some()
1404            }
1405            "15.0" => {
1406                find_msbuild_vs15(TargetArch::X64, env_getter).is_some()
1407                    || find_msbuild_vs15(TargetArch::X86, env_getter).is_some()
1408                    || find_msbuild_vs15(TargetArch::Arm64, env_getter).is_some()
1409            }
1410            "14.0" => LOCAL_MACHINE
1411                .open(&OsString::from(format!(
1412                    "SOFTWARE\\Microsoft\\MSBuild\\ToolsVersions\\{}",
1413                    version
1414                )))
1415                .is_ok(),
1416            _ => false,
1417        }
1418    }
1419
1420    pub(super) fn find_devenv(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
1421        find_devenv_vs15(target, env_getter)
1422    }
1423
1424    fn find_devenv_vs15(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
1425        find_tool_in_vs15_path(r"Common7\IDE\devenv.exe", target, env_getter)
1426    }
1427
1428    // see http://stackoverflow.com/questions/328017/path-to-msbuild
1429    pub(super) fn find_msbuild(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
1430        // VS 15 (2017) changed how to locate msbuild
1431        if let Some(r) = find_msbuild_vs18(target, env_getter) {
1432            Some(r)
1433        } else if let Some(r) = find_msbuild_vs17(target, env_getter) {
1434            Some(r)
1435        } else if let Some(r) = find_msbuild_vs16(target, env_getter) {
1436            return Some(r);
1437        } else if let Some(r) = find_msbuild_vs15(target, env_getter) {
1438            return Some(r);
1439        } else {
1440            find_old_msbuild(target)
1441        }
1442    }
1443
1444    fn find_msbuild_vs15(target: TargetArch, env_getter: &dyn EnvGetter) -> Option<Tool> {
1445        find_tool_in_vs15_path(r"MSBuild\15.0\Bin\MSBuild.exe", target, env_getter)
1446    }
1447
1448    fn find_old_msbuild(target: TargetArch) -> Option<Tool> {
1449        let key = r"SOFTWARE\Microsoft\MSBuild\ToolsVersions";
1450        LOCAL_MACHINE
1451            .open(key.as_ref())
1452            .ok()
1453            .and_then(|key| {
1454                max_version(&key).and_then(|(_vers, key)| key.query_str("MSBuildToolsPath").ok())
1455            })
1456            .map(|path| {
1457                let mut path = PathBuf::from(path);
1458                path.push("MSBuild.exe");
1459                let mut tool = Tool {
1460                    tool: path,
1461                    is_clang_cl: false,
1462                    env: Vec::new(),
1463                };
1464                if target == TargetArch::X64 {
1465                    tool.env.push(("Platform".into(), "X64".into()));
1466                }
1467                tool
1468            })
1469    }
1470}
1471
1472/// Non-Windows Implementation.
1473#[cfg(not(windows))]
1474mod impl_ {
1475    use std::{env, ffi::OsStr, path::PathBuf};
1476
1477    use super::{EnvGetter, TargetArch};
1478    use crate::Tool;
1479
1480    /// Finding msbuild.exe tool under unix system is not currently supported.
1481    /// Maybe can check it using an environment variable looks like `MSBUILD_BIN`.
1482    #[inline(always)]
1483    pub(super) fn find_msbuild(_target: TargetArch, _: &dyn EnvGetter) -> Option<Tool> {
1484        None
1485    }
1486
1487    // Finding devenv.exe tool under unix system is not currently supported.
1488    // Maybe can check it using an environment variable looks like `DEVENV_BIN`.
1489    #[inline(always)]
1490    pub(super) fn find_devenv(_target: TargetArch, _: &dyn EnvGetter) -> Option<Tool> {
1491        None
1492    }
1493
1494    // Finding Clang/LLVM-related tools on unix systems is not currently supported.
1495    #[inline(always)]
1496    pub(super) fn find_llvm_tool(
1497        _tool: &str,
1498        _target: TargetArch,
1499        _: &dyn EnvGetter,
1500    ) -> Option<Tool> {
1501        None
1502    }
1503
1504    /// Attempt to find the tool using environment variables set by vcvars.
1505    pub(super) fn find_msvc_environment(
1506        tool: &str,
1507        _target: TargetArch,
1508        env_getter: &dyn EnvGetter,
1509    ) -> Option<Tool> {
1510        // Early return if the environment doesn't contain a VC install.
1511        let vc_install_dir = env_getter.get_env("VCINSTALLDIR")?;
1512        let vs_install_dir = env_getter.get_env("VSINSTALLDIR")?;
1513
1514        let get_tool = |install_dir: &OsStr| {
1515            env::split_paths(install_dir)
1516                .map(|p| p.join(tool))
1517                .find(|p| p.exists())
1518                .map(|path| Tool {
1519                    tool: path,
1520                    is_clang_cl: false,
1521                    env: Vec::new(),
1522                })
1523        };
1524
1525        // Take the path of tool for the vc install directory.
1526        get_tool(vc_install_dir.as_ref())
1527            // Take the path of tool for the vs install directory.
1528            .or_else(|| get_tool(vs_install_dir.as_ref()))
1529            // Take the path of tool for the current path environment.
1530            .or_else(|| {
1531                env_getter
1532                    .get_env("PATH")
1533                    .as_ref()
1534                    .map(|path| path.as_ref())
1535                    .and_then(get_tool)
1536            })
1537    }
1538
1539    #[inline(always)]
1540    pub(super) fn find_msvc_15plus(
1541        _tool: &str,
1542        _target: TargetArch,
1543        _: &dyn EnvGetter,
1544    ) -> Option<Tool> {
1545        None
1546    }
1547
1548    // For MSVC 14 we need to find the Universal CRT as well as either
1549    // the Windows 10 SDK or Windows 8.1 SDK.
1550    #[inline(always)]
1551    pub(super) fn find_msvc_14(
1552        _tool: &str,
1553        _target: TargetArch,
1554        _: &dyn EnvGetter,
1555    ) -> Option<Tool> {
1556        None
1557    }
1558
1559    #[inline(always)]
1560    pub(super) fn has_msbuild_version(_version: &str, _: &dyn EnvGetter) -> bool {
1561        false
1562    }
1563
1564    #[inline(always)]
1565    pub(super) fn get_ucrt_dir() -> Option<(PathBuf, String)> {
1566        None
1567    }
1568}