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