Skip to main content

rucc_driver/
library.rs

1//! Where the library's headers are.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.4.
4//!
5//! A hosted implementation is two halves and `rucc_session::runtime` is one of them. The
6//! other is the library's, and finding it is the compiler's job because nothing else can do
7//! it. A compiler that has to be told `-isystem /usr/include` on every command line is a
8//! compiler nobody can run `make` with.
9//!
10//! gcc settles this at configure time, which it can do because a gcc is built for the machine
11//! it will run on and the directories are baked into the binary. This compiler is one binary
12//! that runs wherever it is copied, so it has to ask the machine instead, and the shape of
13//! the answer is a list of candidates per platform of which the ones that exist are taken.
14//!
15//! Cross compiling to another operating system produces nothing here on purpose. The host's
16//! `/usr/include` describes the host's library and handing it to a program being built for
17//! somewhere else is worse than handing it nothing, because the failure moves from the
18//! `#include` that could not be resolved to a declaration that is quietly wrong.
19//!
20//! What a cross build gets instead is the target's own headers, out of the sysroot for that
21//! target, and [`header_dirs`] is where the two cases meet. It is the header half of what
22//! [`crate::link`] does for the libraries, it decides nothing itself, and the rule it asks is
23//! `rucc_sysroot::search`, which is section 8.5 written once.
24
25use std::path::{Path, PathBuf};
26use std::process::Command;
27use std::sync::OnceLock;
28
29use rucc_sysroot::{Kernel, Options, Sysroot, Wall, include_paths};
30use rucc_target::{Env, Os, Triple};
31
32use crate::link::Distro;
33
34/// What the machine says about itself, and what the command line said over the top of it.
35///
36/// Separated from the lookup so that the lookup is a function of its arguments and can be
37/// tested for a platform the test is not running on. Everything here is read once, in
38/// [`system_dirs`], which is the only place that talks to the environment.
39#[derive(Debug, Default, Clone, PartialEq, Eq)]
40pub struct Machine {
41    /// The triple of the machine the compiler is running on, when it is one we know.
42    pub host: Option<Triple>,
43    /// `--sysroot`, which prefixes the configured directories, or `-isysroot`, which is the
44    /// spelling Apple's tools use and which means the same thing to us.
45    pub sysroot: Option<PathBuf>,
46    /// The SDK to compile against on an Apple platform, once it has been found.
47    pub sdk: Option<PathBuf>,
48    /// The Windows SDK to compile against, as an `INCLUDE` spells one, once it has been found.
49    ///
50    /// `INCLUDE` itself when the environment has it, which is what `vcvarsall.bat` sets and what
51    /// every build on that platform already reads, and otherwise the same list assembled from the
52    /// Visual Studio installation this machine has. The entries are separated by `;`, which is a
53    /// path separator there and a legal character in a file name nowhere.
54    pub include: Option<String>,
55}
56
57/// The directories the library's headers might be in, in search order.
58///
59/// Every candidate, whether or not it is there. [`system_dirs`] is what filters them, and the
60/// split is so that this half can be read as the platform knowledge it is.
61#[must_use]
62pub fn candidates(target: Triple, machine: &Machine) -> Vec<PathBuf> {
63    // A target that is not this machine has no directories on this machine. There are two
64    // exceptions and they are the same exception twice: a sysroot and an SDK are both somebody
65    // saying that the headers for that target are over there, and `INCLUDE` is a third somebody
66    // saying it in the words that platform uses. The SDK case is how `SDKROOT` reaches an Apple
67    // target from a machine that is not a mac, which is the path
68    // `spec/cross-compile/08-sysroots.md` section 8.6 leaves open when it says a user supplies one.
69    if machine.sysroot.is_none()
70        && machine.sdk.is_none()
71        && machine.include.is_none()
72        && machine.host.is_some_and(|host| host.os != target.os)
73    {
74        return Vec::new();
75    }
76    let root = machine.sysroot.as_deref();
77    match target.os {
78        Os::Linux => linux(target, root),
79        Os::Darwin => darwin(machine.sdk.as_deref().or(root)),
80        Os::Windows => windows(root, machine.include.as_deref()),
81        // Freestanding. There is no library, so there are no headers of one, and the nine the
82        // compiler ships are the whole of what a program may include.
83        Os::None => Vec::new(),
84    }
85}
86
87/// gcc's order on a glibc system, which is what every Linux distribution lays out.
88///
89/// `/usr/local/include` first because that is where a locally built library installs and the
90/// point of installing one there is that it wins. The multiarch directory before
91/// `/usr/include` because that is where Debian and its derivatives put the headers that
92/// differ between two architectures of the same machine, and a distribution that does not use
93/// multiarch simply does not have the directory.
94fn linux(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
95    let libc = match target.env {
96        Env::Musl => "musl",
97        // Not `Env::as_str`, which answers `none` for a target written without an environment.
98        // A bare `x86_64-linux` on a Linux box means the machine's own libc, and on every
99        // machine that lays its headers out per architecture that libc is glibc.
100        Env::None | Env::Gnu | Env::Msvc => "gnu",
101    };
102    let multiarch = format!("{}-linux-{libc}", target.arch.as_str());
103    ["/usr/local/include".into(), format!("/usr/include/{multiarch}"), "/usr/include".into()]
104        .into_iter()
105        .map(|dir| under(sysroot, &dir))
106        .collect()
107}
108
109/// The SDK, which on an Apple platform is the whole of it.
110///
111/// There is no `/usr/include` on a Mac since the command line tools stopped installing one,
112/// and the headers live inside the SDK that Xcode or the command line tools brought with
113/// them. Nothing is offered when there is no SDK, because a guess at a path that is not there
114/// only makes the diagnostic longer, and the diagnostic is `rucc_sysroot::Wall::no_headers`,
115/// which the driver leaves on the search path: an Apple target with no SDK anywhere is Apple's
116/// licence wall rather than a missing directory, and the include that failed is where it is said.
117fn darwin(sdk: Option<&Path>) -> Vec<PathBuf> {
118    sdk.map(|sdk| vec![sdk.join("usr/include")]).unwrap_or_default()
119}
120
121/// A tree somebody named, or whatever `INCLUDE` says, in the order it says it.
122///
123/// Windows has no fixed place for the headers. The MSVC ones move with the toolchain version
124/// and the SDK ones move with the SDK version, and the way both are found is the environment
125/// that `vcvarsall.bat` sets, which is what every compiler on that platform reads and what
126/// every build there already has. So `INCLUDE` is a list of directories rather than a root,
127/// and it is taken as it stands.
128///
129/// A named tree is the other way in, and it is the one a cross compile uses, because nothing on
130/// a Linux box ran `vcvarsall.bat`. The layout is the one `xwin` writes and `cargo-xwin` builds
131/// against, which is the only relocatable shape an MSVC tree has: the CRT's headers under
132/// `crt/include` and the Windows SDK's under `sdk/include`, lowercase, with the version
133/// directories already resolved away. A copied Visual Studio installation is reached by setting
134/// `INCLUDE` instead, which is that platform's own spelling for it.
135fn windows(sysroot: Option<&Path>, include: Option<&str>) -> Vec<PathBuf> {
136    if let Some(root) = sysroot {
137        return ["crt/include", "sdk/include/ucrt", "sdk/include/shared", "sdk/include/um"]
138            .into_iter()
139            .chain(["sdk/include/winrt", "sdk/include/cppwinrt"])
140            .map(|dir| root.join(dir))
141            .collect();
142    }
143    include
144        .unwrap_or_default()
145        .split(';')
146        .map(str::trim)
147        .filter(|dir| !dir.is_empty())
148        .map(PathBuf::from)
149        .collect()
150}
151
152/// The header directories of a Visual Studio installation, in the order `vcvarsall.bat` puts them
153/// in `INCLUDE`.
154///
155/// `vc` is the versioned directory under `VC/Tools/MSVC` and `kit` is the versioned directory under
156/// the Windows Kit's `Include`, because the two halves are versioned separately and installed by
157/// different things: one comes with the compiler and holds the CRT, and the other is the platform
158/// and holds `windows.h` and the universal CRT. A machine can have several of each.
159///
160/// The five kit directories rather than the one, because they are five search roots and not a
161/// hierarchy. `ucrt` is the C library, `um` is the Win32 API, `shared` is what those two have in
162/// common, and the last two are for a language this compiler does not compile, so they are here for
163/// the same reason `vcvarsall.bat` puts them there: a header in one of them includes one of the
164/// others by its bare name.
165fn msvc_dirs(vc: &Path, kit: &Path) -> Vec<PathBuf> {
166    let mut dirs = vec![vc.join("include")];
167    for dir in ["ucrt", "shared", "um", "winrt", "cppwinrt"] {
168        dirs.push(kit.join(dir));
169    }
170    dirs
171}
172
173/// A version directory's name as numbers, for comparing two of them.
174///
175/// Text comparison is wrong here and quietly so. `10.0.9.0` sorts after `10.0.22621.0` as text and
176/// before it as a version, and the Windows Kit's directories are exactly that shape, so a compiler
177/// that picked the larger string would compile against an SDK from several years before the one the
178/// machine has. [`None`] for a name that is not a version at all, which is how a `Catalogs` or a
179/// `Source` directory beside the versioned ones is passed over.
180fn version_key(name: &str) -> Option<Vec<u64>> {
181    let parts: Vec<u64> = name.split('.').map(|part| part.parse().ok()).collect::<Option<_>>()?;
182    (!parts.is_empty()).then_some(parts)
183}
184
185/// The newest version directory under `dir`, which is the one to compile against.
186///
187/// The newest rather than a configured one, because there is nothing to configure it with and a
188/// person who installed a second SDK installed a newer one. A named `--sysroot` is how somebody
189/// says which tree they meant, and `INCLUDE` is how they say it in that platform's own words.
190fn newest(dir: &Path) -> Option<PathBuf> {
191    let mut best: Option<(Vec<u64>, PathBuf)> = None;
192    for entry in std::fs::read_dir(dir).ok()?.flatten() {
193        let name = entry.file_name();
194        let Some(key) = name.to_str().and_then(version_key) else { continue };
195        if !entry.path().is_dir() {
196            continue;
197        }
198        if best.as_ref().is_none_or(|(found, _)| key > *found) {
199            best = Some((key, entry.path()));
200        }
201    }
202    best.map(|(_, path)| path)
203}
204
205/// What this machine's own Visual Studio installation says, as an `INCLUDE` would say it.
206///
207/// Asked at most once per process, for the reason [`xcrun`] is: it costs two subprocesses and the
208/// answer does not change inside one compile. Joined with `;` rather than kept as a list so that
209/// there is one parser for both ways in, which is lossless because `;` is a path separator on that
210/// platform and a legal character in a file name nowhere.
211///
212/// This is the Windows half of what `xcrun` is on a mac, and it exists for the same reason: the
213/// headers of a platform whose SDK is not ours to ship are on the machine or they are nowhere, and
214/// the only way to be told where is to ask the thing that installed them. `vswhere.exe` is at a
215/// fixed path on every machine with Visual Studio 2017 or later, which is what makes it askable at
216/// all, and the kit is in the registry because that is where its installer puts it.
217fn installed_msvc() -> Option<String> {
218    static ANSWER: OnceLock<Option<String>> = OnceLock::new();
219    ANSWER
220        .get_or_init(|| {
221            let vc = newest(&visual_studio()?.join("VC").join("Tools").join("MSVC"))?;
222            let kit = newest(&windows_kit()?.join("Include"))?;
223            let dirs: Vec<String> =
224                msvc_dirs(&vc, &kit).iter().map(|dir| dir.display().to_string()).collect();
225            Some(dirs.join(";"))
226        })
227        .clone()
228}
229
230/// Where Visual Studio is, according to the installer that put it there.
231///
232/// `-products *` because the C++ build tools are a product of their own and a machine with those and
233/// no Visual Studio is the ordinary shape of a build server. `-latest` because the alternative is to
234/// read a list and pick, which is [`newest`]'s job one level down.
235fn visual_studio() -> Option<PathBuf> {
236    let program_files = std::env::var_os("ProgramFiles(x86)")?;
237    let vswhere = PathBuf::from(program_files)
238        .join("Microsoft Visual Studio")
239        .join("Installer")
240        .join("vswhere.exe");
241    if !vswhere.is_file() {
242        return None;
243    }
244    let out = Command::new(vswhere)
245        .args(["-latest", "-products", "*", "-property", "installationPath"])
246        .output()
247        .ok()?;
248    if !out.status.success() {
249        return None;
250    }
251    let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.lines().next()?.trim());
252    path.is_dir().then_some(path)
253}
254
255/// Where the Windows Kit is, which is the Windows SDK and the universal CRT.
256///
257/// The registry first and the default location second, rather than the default location only,
258/// because the installer lets somebody move it and writes down where it went. `reg.exe` is how a
259/// program with no dependencies reads a key, and the value is the rest of the line after the type
260/// because a path there has spaces in it and `Program Files (x86)` has two.
261fn windows_kit() -> Option<PathBuf> {
262    const KEY: &str = r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots";
263    if let Ok(out) = Command::new("reg.exe").args(["query", KEY, "/v", "KitsRoot10"]).output() {
264        if let Ok(text) = String::from_utf8(out.stdout) {
265            for line in text.lines() {
266                let Some((name, value)) = line.trim().split_once("REG_SZ") else { continue };
267                if name.trim() != "KitsRoot10" {
268                    continue;
269                }
270                let root = PathBuf::from(value.trim());
271                if root.is_dir() {
272                    return Some(root);
273                }
274            }
275        }
276    }
277    let default = PathBuf::from(std::env::var_os("ProgramFiles(x86)")?).join("Windows Kits/10");
278    default.is_dir().then_some(default)
279}
280
281/// A path under the sysroot, when there is one.
282fn under(sysroot: Option<&Path>, dir: &str) -> PathBuf {
283    match sysroot {
284        // `strip_prefix` because joining an absolute path replaces the root rather than
285        // extending it, which would make every entry the unprefixed one.
286        Some(root) => root.join(dir.strip_prefix('/').unwrap_or(dir)),
287        None => PathBuf::from(dir),
288    }
289}
290
291/// The directories the library's headers are actually in, in search order.
292///
293/// This is the one function here that talks to the machine: it reads the environment, asks
294/// `xcrun` where the SDK is when it has to, and keeps the candidates that exist.
295#[must_use]
296pub fn system_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
297    let machine = Machine {
298        host: Triple::host(),
299        sysroot: sysroot.map(Path::to_path_buf),
300        // Asked for only on the platforms that have one, since finding either can mean running a
301        // program and a compile for Linux should not wait on Xcode or on the Visual Studio
302        // installer. The MSVC environment and not every Windows target, because a mingw-w64 target's
303        // headers are ours and are in the cache, and handing it Microsoft's would be giving a
304        // program the declarations of a C library it is not being linked against.
305        sdk: if target.os == Os::Darwin { sdk(sysroot) } else { None },
306        include: if target.os == Os::Windows && target.env == Env::Msvc {
307            msvc(sysroot)
308        } else {
309            None
310        },
311    };
312    candidates(target, &machine).into_iter().filter(|dir| dir.is_dir()).collect()
313}
314
315/// The system header directories for this compile, which is step 3 of section 8.5.
316///
317/// Design: `spec/cross-compile/08-sysroots.md` section 8.5.
318///
319/// Three sources and the first that has anything wins: a tree the user named with `--sysroot` or
320/// `-isysroot`, then the sysroot for this target, then this machine's own directories and only when
321/// the target is this machine. `bundled` is [`crate::link::cross_sysroot`], which is the one place
322/// the two kinds of compile are told apart, so the headers a file is compiled against and the
323/// libraries it is linked against cannot disagree about which kind it is.
324///
325/// The ordering between the three is not decided here. It is `rucc_sysroot::search::include_paths`,
326/// which is section 8.5 as a function, and the condition that a host directory is legal only when
327/// the target is the host lives there and nowhere else. What this adds is the part that has to talk
328/// to the machine, which is [`system_dirs`] above.
329///
330/// Steps 1 and 2 are the driver's own. `-I` and its relatives are in [`rucc_session`]'s search path
331/// already, in the order the command line gave them, and the compiler's own headers are not a
332/// directory at all but the `<builtin>` entry the caller pushes before this.
333///
334/// A sysroot that is not on the disk yet is still named. The list is not filtered for existence the
335/// way [`system_dirs`] filters the machine's, because the answer to `rucc --target=... -v` on a
336/// machine where the tree has not been built should be the path it would be at rather than silence.
337///
338/// `kernel` is [`crate::link::cross_kernel`], and on a Linux target it adds two more directories
339/// after the libc's. They are the kernel's `asm/` for the architecture and its shared `linux/` and
340/// `asm-generic/`, they are not under any sysroot because every target sharing an architecture reads
341/// the same files, and they come last for the reason section 8.5 gives: both trees have a `sys/` and
342/// the libc's is the one a program means.
343///
344/// `distro` is [`crate::link::distro_cross`], a tree the distribution's cross packages installed,
345/// and it stands in for a named sysroot. Its `include` holds the kernel's headers as well as the
346/// libc's, so nothing is added after it.
347#[must_use]
348pub fn header_dirs(
349    target: Triple,
350    sysroot: Option<&Path>,
351    bundled: Option<&Sysroot>,
352    kernel: Option<&Kernel>,
353    distro: Option<&Distro>,
354) -> Vec<PathBuf> {
355    // Once, because asking can mean running `xcrun` or the Visual Studio installer. The answer goes
356    // to whichever of the three fields it belongs in, and which one that is decides how step 3 treats
357    // it rather than being a detail of how it was found. With a `--sysroot` these are the directories
358    // under the tree the user named. Without one, on a target behind a licence wall, they are an SDK
359    // this machine has, which is the target's own headers for every architecture of that platform and
360    // not this machine's library, so one Xcode serves `x86_64-macos` on an arm64 mac and one Windows
361    // Kit serves `aarch64-windows-msvc` on an x86_64 box, which is how the platform's own tools use
362    // them. Otherwise they are the machine's own directories and step 3 will only take them when the
363    // target is the host.
364    let dirs = system_dirs(target, sysroot);
365    // `Wall` rather than a second list of the two operating systems, because the targets whose
366    // headers are found as an SDK are exactly the targets whose headers are not ours to ship, and a
367    // copy of that rule here is a copy that can disagree with the one the diagnostic reads.
368    let walled = Wall::of(target.tuple()).is_some();
369    let (named, sdk, host) = match (distro, sysroot.is_some(), walled) {
370        // A distribution's cross tree takes the place a named one would, because it is one: a
371        // directory somebody installed that holds the target's headers and nothing else.
372        (Some(distro), _, _) => (vec![distro.include()], Vec::new(), Vec::new()),
373        (None, true, _) => (dirs, Vec::new(), Vec::new()),
374        (None, false, true) => (Vec::new(), dirs, Vec::new()),
375        (None, false, false) => (Vec::new(), Vec::new(), dirs),
376    };
377    let options = Options {
378        sysroot: &named,
379        sdk: &sdk,
380        bundled,
381        kernel,
382        host_include: &host,
383        ..Options::default()
384    };
385    include_paths(target.tuple(), Triple::host().map(Triple::tuple), &options)
386        .into_iter()
387        .map(|entry| entry.path)
388        .collect()
389}
390
391/// The SDK to compile against, in the order the platform's own tools look.
392///
393/// `-isysroot` beats `SDKROOT` beats `xcrun` beats the place the command line tools put it.
394/// `xcrun` is a program rather than a path because the answer moves with the Xcode that is
395/// selected and asking is the only way to be told which one that is, and it is third rather
396/// than first because it costs a process and the two before it are free.
397fn sdk(sysroot: Option<&Path>) -> Option<PathBuf> {
398    if let Some(root) = sysroot {
399        return Some(root.to_path_buf());
400    }
401    if let Some(root) = std::env::var_os("SDKROOT") {
402        let root = PathBuf::from(root);
403        if root.is_dir() {
404            return Some(root);
405        }
406    }
407    if let Some(root) = xcrun() {
408        return Some(root);
409    }
410    let tools = PathBuf::from("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk");
411    tools.is_dir().then_some(tools)
412}
413
414/// The Windows SDK to compile against, in the order somebody would expect to be obeyed.
415///
416/// A tree somebody named beats `INCLUDE` beats the installation this machine has, which is the order
417/// [`sdk`] uses on the Apple side and for the same reasons: the cheap answers are the ones somebody
418/// gave us, and the one that costs a process is last. A named tree answers nothing here because it is
419/// not a list of directories, and [`windows`] is handed the root itself.
420fn msvc(sysroot: Option<&Path>) -> Option<String> {
421    if sysroot.is_some() {
422        return None;
423    }
424    match std::env::var("INCLUDE") {
425        Ok(include) if !include.trim().is_empty() => Some(include),
426        _ => installed_msvc(),
427    }
428}
429
430/// What `xcrun` said, asked at most once in a process.
431///
432/// A compiler that ran it for the header search and again for the diagnostic that explains an empty
433/// one would pay for a process twice to be told the same path. The answer cannot change underneath us
434/// in a way that matters either: a run of the compiler compiles against one SDK.
435fn xcrun() -> Option<PathBuf> {
436    static ANSWER: OnceLock<Option<PathBuf>> = OnceLock::new();
437    ANSWER.get_or_init(ask_xcrun).clone()
438}
439
440/// Asks `xcrun` for the SDK path, and says nothing if it is not there to ask.
441fn ask_xcrun() -> Option<PathBuf> {
442    let out = Command::new("/usr/bin/xcrun").args(["--show-sdk-path"]).output().ok()?;
443    if !out.status.success() {
444        return None;
445    }
446    let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.trim());
447    (path.is_absolute() && path.is_dir()).then_some(path)
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use rucc_target::Arch;
454
455    fn triple(os: Os, env: Env) -> Triple {
456        Triple::new(Arch::X86_64, os, env)
457    }
458
459    fn on(host: Os) -> Machine {
460        Machine { host: Some(triple(host, Env::Gnu)), ..Machine::default() }
461    }
462
463    #[test]
464    fn the_local_directory_comes_before_the_distributions_and_the_specific_before_the_general() {
465        let dirs = candidates(triple(Os::Linux, Env::Gnu), &on(Os::Linux));
466        let dirs: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
467        assert_eq!(dirs, ["/usr/local/include", "/usr/include/x86_64-linux-gnu", "/usr/include"]);
468    }
469
470    #[test]
471    fn the_directory_headers_are_kept_apart_in_is_named_after_the_targets_own_library() {
472        let of = |env| candidates(triple(Os::Linux, env), &on(Os::Linux))[1].display().to_string();
473        assert_eq!(of(Env::Musl), "/usr/include/x86_64-linux-musl");
474        assert_eq!(of(Env::Gnu), "/usr/include/x86_64-linux-gnu");
475        // A triple written without an environment is the machine's own, and a machine that
476        // sorts its headers by architecture at all is one running glibc.
477        assert_eq!(of(Env::None), "/usr/include/x86_64-linux-gnu");
478    }
479
480    #[test]
481    fn a_sysroot_is_in_front_of_every_one_of_them_rather_than_replacing_the_root() {
482        let machine = Machine { sysroot: Some("/opt/cross".into()), ..on(Os::Linux) };
483        let dirs = candidates(triple(Os::Linux, Env::Gnu), &machine);
484        // Joined rather than spelled out, because a path prints with the separator the host
485        // uses and this test runs on a host where that is a backslash.
486        let under = |dir| PathBuf::from("/opt/cross").join(dir);
487        assert_eq!(
488            dirs,
489            [
490                under("usr/local/include"),
491                under("usr/include/x86_64-linux-gnu"),
492                under("usr/include")
493            ]
494        );
495    }
496
497    #[test]
498    fn this_machines_headers_are_not_offered_to_a_program_being_built_for_another_system() {
499        assert!(candidates(triple(Os::Windows, Env::Msvc), &on(Os::Linux)).is_empty());
500        assert!(candidates(triple(Os::Linux, Env::Gnu), &on(Os::Darwin)).is_empty());
501        // With a sysroot they are, because that is what naming one says.
502        let machine = Machine { sysroot: Some("/opt/cross".into()), ..on(Os::Darwin) };
503        assert!(!candidates(triple(Os::Linux, Env::Gnu), &machine).is_empty());
504    }
505
506    #[test]
507    fn an_unknown_host_offers_the_targets_own_directories_rather_than_none() {
508        // `Triple::host` answers nothing on a machine this compiler has no target for, and a
509        // native compile there is still a native compile.
510        let machine = Machine { host: None, ..Machine::default() };
511        assert_eq!(candidates(triple(Os::Linux, Env::Gnu), &machine).len(), 3);
512    }
513
514    #[test]
515    fn an_apple_target_is_the_sdk_and_nothing_else_and_nothing_without_one() {
516        let machine = Machine { sdk: Some("/S.sdk".into()), ..on(Os::Darwin) };
517        let dirs = candidates(triple(Os::Darwin, Env::None), &machine);
518        assert_eq!(dirs, [PathBuf::from("/S.sdk/usr/include")]);
519        assert!(candidates(triple(Os::Darwin, Env::None), &on(Os::Darwin)).is_empty());
520    }
521
522    #[test]
523    fn an_sdk_reaches_an_apple_target_from_a_host_that_is_not_a_mac() {
524        // A Linux box with `SDKROOT` pointing at an SDK somebody downloaded under their own licence,
525        // which is the path section 8.6 leaves open on every host that is not a mac. It is the same
526        // exception a `--sysroot` gets and for the same reason: somebody said where the headers are.
527        let machine = Machine { sdk: Some("/S.sdk".into()), ..on(Os::Linux) };
528        let dirs = candidates(triple(Os::Darwin, Env::None), &machine);
529        assert_eq!(dirs, [PathBuf::from("/S.sdk/usr/include")]);
530        // And with no SDK there is nothing, which is what the licence wall's message is about.
531        assert!(candidates(triple(Os::Darwin, Env::None), &on(Os::Linux)).is_empty());
532    }
533
534    #[test]
535    fn windows_is_told_where_its_headers_are_and_is_not_guessed_at() {
536        let machine =
537            Machine { include: Some(r"C:\vc\include;C:\sdk\ucrt ;".to_owned()), ..on(Os::Windows) };
538        let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
539        assert_eq!(dirs, [PathBuf::from(r"C:\vc\include"), PathBuf::from(r"C:\sdk\ucrt")]);
540        assert!(candidates(triple(Os::Windows, Env::Msvc), &on(Os::Windows)).is_empty());
541    }
542
543    #[test]
544    fn an_sdk_reaches_an_msvc_target_from_a_host_that_is_not_windows() {
545        // The same exception the Apple side gets, and the case it is for is a Linux build machine
546        // with a tree `xwin` assembled on it under a licence its owner accepted.
547        let machine = Machine { include: Some(r"C:\sdk\um".to_owned()), ..on(Os::Linux) };
548        let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
549        assert_eq!(dirs, [PathBuf::from(r"C:\sdk\um")]);
550    }
551
552    #[test]
553    fn a_named_tree_for_an_msvc_target_is_the_layout_a_relocatable_one_has() {
554        let machine = Machine { sysroot: Some("/opt/xwin".into()), ..on(Os::Linux) };
555        let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
556        let under = |dir| PathBuf::from("/opt/xwin").join(dir);
557        assert_eq!(
558            dirs,
559            [
560                under("crt/include"),
561                under("sdk/include/ucrt"),
562                under("sdk/include/shared"),
563                under("sdk/include/um"),
564                under("sdk/include/winrt"),
565                under("sdk/include/cppwinrt"),
566            ]
567        );
568    }
569
570    #[test]
571    fn the_mingw_target_is_not_offered_microsofts_headers() {
572        // Its headers are ours, they are in the cache, and Microsoft's are the declarations of a
573        // library it is not linked against. `system_dirs` is what decides this, by asking for an
574        // `INCLUDE` only in the MSVC environment, so a `Machine` with one set is the test.
575        let machine = Machine { include: Some(r"C:\sdk\um".to_owned()), ..on(Os::Windows) };
576        assert!(system_dirs(triple(Os::Windows, Env::Gnu), None).is_empty());
577        // And the field itself is still obeyed when it is set, which is what keeps this test honest
578        // about where the decision is rather than asserting it twice.
579        assert!(!candidates(triple(Os::Windows, Env::Gnu), &machine).is_empty());
580    }
581
582    #[test]
583    fn the_headers_of_an_installation_are_in_the_order_the_developer_prompt_puts_them() {
584        // Joined rather than spelled out, because this test runs on hosts whose separator is not
585        // the one a path like this is written with.
586        let vc = PathBuf::from(r"C:\BuildTools\VC\Tools\MSVC\14.44.35207");
587        let dirs = msvc_dirs(&vc, Path::new("/k"));
588        assert_eq!(dirs[0], vc.join("include"));
589        let rest: Vec<String> =
590            dirs[1..].iter().map(|dir| dir.file_name().unwrap().to_string_lossy().into()).collect();
591        assert_eq!(rest, ["ucrt", "shared", "um", "winrt", "cppwinrt"]);
592    }
593
594    #[test]
595    fn a_version_directory_is_compared_as_numbers_and_not_as_text() {
596        // The case that makes this matter. As text the first of these is the larger.
597        assert!(version_key("10.0.9.0") < version_key("10.0.22621.0"));
598        assert!(version_key("10.0.22621.0") < version_key("10.0.26100.0"));
599        assert!(version_key("14.44.35207") > version_key("14.39.33519"));
600        // And the directories that sit beside the versioned ones in a Windows Kit.
601        assert_eq!(version_key("Catalogs"), None);
602        assert_eq!(version_key("wdf"), None);
603        assert_eq!(version_key(""), None);
604    }
605
606    #[test]
607    fn a_freestanding_target_has_no_library_to_find_the_headers_of() {
608        let machine = Machine { sysroot: Some("/opt/cross".into()), ..Machine::default() };
609        assert!(candidates(triple(Os::None, Env::None), &machine).is_empty());
610    }
611
612    #[test]
613    fn what_is_offered_on_this_machine_is_there_because_it_was_checked_for() {
614        for dir in system_dirs(Triple::host().unwrap_or(triple(Os::Linux, Env::Gnu)), None) {
615            assert!(dir.is_dir(), "{}", dir.display());
616        }
617    }
618}