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