Skip to main content

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