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;
27
28use rucc_sysroot::{Kernel, Options, Sysroot, include_paths};
29use rucc_target::{Env, Os, Triple};
30
31/// What the machine says about itself, and what the command line said over the top of it.
32///
33/// Separated from the lookup so that the lookup is a function of its arguments and can be
34/// tested for a platform the test is not running on. Everything here is read once, in
35/// [`system_dirs`], which is the only place that talks to the environment.
36#[derive(Debug, Default, Clone, PartialEq, Eq)]
37pub struct Machine {
38    /// The triple of the machine the compiler is running on, when it is one we know.
39    pub host: Option<Triple>,
40    /// `--sysroot`, which prefixes the configured directories, or `-isysroot`, which is the
41    /// spelling Apple's tools use and which means the same thing to us.
42    pub sysroot: Option<PathBuf>,
43    /// The SDK to compile against on an Apple platform, once it has been found.
44    pub sdk: Option<PathBuf>,
45    /// `INCLUDE`, which is how every Windows toolchain says where its headers are. The
46    /// entries are separated by `;`, which is a path separator there and a legal character in
47    /// a file name nowhere.
48    pub include: Option<String>,
49}
50
51/// The directories the library's headers might be in, in search order.
52///
53/// Every candidate, whether or not it is there. [`system_dirs`] is what filters them, and the
54/// split is so that this half can be read as the platform knowledge it is.
55#[must_use]
56pub fn candidates(target: Triple, machine: &Machine) -> Vec<PathBuf> {
57    // A target that is not this machine has no directories on this machine. The one exception
58    // is a sysroot, which is a statement that the headers for that target are over there.
59    if machine.sysroot.is_none() && machine.host.is_some_and(|host| host.os != target.os) {
60        return Vec::new();
61    }
62    let root = machine.sysroot.as_deref();
63    match target.os {
64        Os::Linux => linux(target, root),
65        Os::Darwin => darwin(machine.sdk.as_deref().or(root)),
66        Os::Windows => windows(machine.include.as_deref()),
67        // Freestanding. There is no library, so there are no headers of one, and the nine the
68        // compiler ships are the whole of what a program may include.
69        Os::None => Vec::new(),
70    }
71}
72
73/// gcc's order on a glibc system, which is what every Linux distribution lays out.
74///
75/// `/usr/local/include` first because that is where a locally built library installs and the
76/// point of installing one there is that it wins. The multiarch directory before
77/// `/usr/include` because that is where Debian and its derivatives put the headers that
78/// differ between two architectures of the same machine, and a distribution that does not use
79/// multiarch simply does not have the directory.
80fn linux(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
81    let libc = match target.env {
82        Env::Musl => "musl",
83        // Not `Env::as_str`, which answers `none` for a target written without an environment.
84        // A bare `x86_64-linux` on a Linux box means the machine's own libc, and on every
85        // machine that lays its headers out per architecture that libc is glibc.
86        Env::None | Env::Gnu | Env::Msvc => "gnu",
87    };
88    let multiarch = format!("{}-linux-{libc}", target.arch.as_str());
89    ["/usr/local/include".into(), format!("/usr/include/{multiarch}"), "/usr/include".into()]
90        .into_iter()
91        .map(|dir| under(sysroot, &dir))
92        .collect()
93}
94
95/// The SDK, which on an Apple platform is the whole of it.
96///
97/// There is no `/usr/include` on a Mac since the command line tools stopped installing one,
98/// and the headers live inside the SDK that Xcode or the command line tools brought with
99/// them. Nothing is offered when there is no SDK, because a guess at a path that is not there
100/// only makes the diagnostic longer.
101fn darwin(sdk: Option<&Path>) -> Vec<PathBuf> {
102    sdk.map(|sdk| vec![sdk.join("usr/include")]).unwrap_or_default()
103}
104
105/// Whatever `INCLUDE` says, in the order it says it.
106///
107/// Windows has no fixed place for the headers. The MSVC ones move with the toolchain version
108/// and the SDK ones move with the SDK version, and the way both are found is the environment
109/// that `vcvarsall.bat` sets, which is what every compiler on that platform reads and what
110/// every build there already has.
111fn windows(include: Option<&str>) -> Vec<PathBuf> {
112    include
113        .unwrap_or_default()
114        .split(';')
115        .map(str::trim)
116        .filter(|dir| !dir.is_empty())
117        .map(PathBuf::from)
118        .collect()
119}
120
121/// A path under the sysroot, when there is one.
122fn under(sysroot: Option<&Path>, dir: &str) -> PathBuf {
123    match sysroot {
124        // `strip_prefix` because joining an absolute path replaces the root rather than
125        // extending it, which would make every entry the unprefixed one.
126        Some(root) => root.join(dir.strip_prefix('/').unwrap_or(dir)),
127        None => PathBuf::from(dir),
128    }
129}
130
131/// The directories the library's headers are actually in, in search order.
132///
133/// This is the one function here that talks to the machine: it reads the environment, asks
134/// `xcrun` where the SDK is when it has to, and keeps the candidates that exist.
135#[must_use]
136pub fn system_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
137    let machine = Machine {
138        host: Triple::host(),
139        sysroot: sysroot.map(Path::to_path_buf),
140        // Asked for only on the platform that has one, since finding it can mean running a
141        // program and a compile for Linux should not wait on Xcode.
142        sdk: if target.os == Os::Darwin { sdk(sysroot) } else { None },
143        include: if target.os == Os::Windows { std::env::var("INCLUDE").ok() } else { None },
144    };
145    candidates(target, &machine).into_iter().filter(|dir| dir.is_dir()).collect()
146}
147
148/// The system header directories for this compile, which is step 3 of section 8.5.
149///
150/// Design: `spec/cross-compile/08-sysroots.md` section 8.5.
151///
152/// Three sources and the first that has anything wins: a tree the user named with `--sysroot` or
153/// `-isysroot`, then the sysroot for this target, then this machine's own directories and only when
154/// the target is this machine. `bundled` is [`crate::link::cross_sysroot`], which is the one place
155/// the two kinds of compile are told apart, so the headers a file is compiled against and the
156/// libraries it is linked against cannot disagree about which kind it is.
157///
158/// The ordering between the three is not decided here. It is `rucc_sysroot::search::include_paths`,
159/// which is section 8.5 as a function, and the condition that a host directory is legal only when
160/// the target is the host lives there and nowhere else. What this adds is the part that has to talk
161/// to the machine, which is [`system_dirs`] above.
162///
163/// Steps 1 and 2 are the driver's own. `-I` and its relatives are in [`rucc_session`]'s search path
164/// already, in the order the command line gave them, and the compiler's own headers are not a
165/// directory at all but the `<builtin>` entry the caller pushes before this.
166///
167/// A sysroot that is not on the disk yet is still named. The list is not filtered for existence the
168/// way [`system_dirs`] filters the machine's, because the answer to `rucc --target=... -v` on a
169/// machine where the tree has not been built should be the path it would be at rather than silence.
170///
171/// `kernel` is [`crate::link::cross_kernel`], and on a Linux target it adds two more directories
172/// after the libc's. They are the kernel's `asm/` for the architecture and its shared `linux/` and
173/// `asm-generic/`, they are not under any sysroot because every target sharing an architecture reads
174/// the same files, and they come last for the reason section 8.5 gives: both trees have a `sys/` and
175/// the libc's is the one a program means.
176#[must_use]
177pub fn header_dirs(
178    target: Triple,
179    sysroot: Option<&Path>,
180    bundled: Option<&Sysroot>,
181    kernel: Option<&Kernel>,
182) -> Vec<PathBuf> {
183    // Once, because asking can mean running `xcrun`. The answer goes to whichever of the two
184    // fields the command line put it in: with a `--sysroot` these are the directories under the
185    // tree the user named, and without one they are the machine's own.
186    let dirs = system_dirs(target, sysroot);
187    let (named, host) = if sysroot.is_some() { (dirs, Vec::new()) } else { (Vec::new(), dirs) };
188    let options =
189        Options { sysroot: &named, bundled, kernel, host_include: &host, ..Options::default() };
190    include_paths(target.tuple(), Triple::host().map(Triple::tuple), &options)
191        .into_iter()
192        .map(|entry| entry.path)
193        .collect()
194}
195
196/// The SDK to compile against, in the order the platform's own tools look.
197///
198/// `-isysroot` beats `SDKROOT` beats `xcrun` beats the place the command line tools put it.
199/// `xcrun` is a program rather than a path because the answer moves with the Xcode that is
200/// selected and asking is the only way to be told which one that is, and it is third rather
201/// than first because it costs a process and the two before it are free.
202fn sdk(sysroot: Option<&Path>) -> Option<PathBuf> {
203    if let Some(root) = sysroot {
204        return Some(root.to_path_buf());
205    }
206    if let Some(root) = std::env::var_os("SDKROOT") {
207        let root = PathBuf::from(root);
208        if root.is_dir() {
209            return Some(root);
210        }
211    }
212    if let Some(root) = xcrun() {
213        return Some(root);
214    }
215    let tools = PathBuf::from("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk");
216    tools.is_dir().then_some(tools)
217}
218
219/// Asks `xcrun` for the SDK path, and says nothing if it is not there to ask.
220fn xcrun() -> Option<PathBuf> {
221    let out = Command::new("/usr/bin/xcrun").args(["--show-sdk-path"]).output().ok()?;
222    if !out.status.success() {
223        return None;
224    }
225    let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.trim());
226    (path.is_absolute() && path.is_dir()).then_some(path)
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use rucc_target::Arch;
233
234    fn triple(os: Os, env: Env) -> Triple {
235        Triple::new(Arch::X86_64, os, env)
236    }
237
238    fn on(host: Os) -> Machine {
239        Machine { host: Some(triple(host, Env::Gnu)), ..Machine::default() }
240    }
241
242    #[test]
243    fn the_local_directory_comes_before_the_distributions_and_the_specific_before_the_general() {
244        let dirs = candidates(triple(Os::Linux, Env::Gnu), &on(Os::Linux));
245        let dirs: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
246        assert_eq!(dirs, ["/usr/local/include", "/usr/include/x86_64-linux-gnu", "/usr/include"]);
247    }
248
249    #[test]
250    fn the_directory_headers_are_kept_apart_in_is_named_after_the_targets_own_library() {
251        let of = |env| candidates(triple(Os::Linux, env), &on(Os::Linux))[1].display().to_string();
252        assert_eq!(of(Env::Musl), "/usr/include/x86_64-linux-musl");
253        assert_eq!(of(Env::Gnu), "/usr/include/x86_64-linux-gnu");
254        // A triple written without an environment is the machine's own, and a machine that
255        // sorts its headers by architecture at all is one running glibc.
256        assert_eq!(of(Env::None), "/usr/include/x86_64-linux-gnu");
257    }
258
259    #[test]
260    fn a_sysroot_is_in_front_of_every_one_of_them_rather_than_replacing_the_root() {
261        let machine = Machine { sysroot: Some("/opt/cross".into()), ..on(Os::Linux) };
262        let dirs = candidates(triple(Os::Linux, Env::Gnu), &machine);
263        // Joined rather than spelled out, because a path prints with the separator the host
264        // uses and this test runs on a host where that is a backslash.
265        let under = |dir| PathBuf::from("/opt/cross").join(dir);
266        assert_eq!(
267            dirs,
268            [
269                under("usr/local/include"),
270                under("usr/include/x86_64-linux-gnu"),
271                under("usr/include")
272            ]
273        );
274    }
275
276    #[test]
277    fn this_machines_headers_are_not_offered_to_a_program_being_built_for_another_system() {
278        assert!(candidates(triple(Os::Windows, Env::Msvc), &on(Os::Linux)).is_empty());
279        assert!(candidates(triple(Os::Linux, Env::Gnu), &on(Os::Darwin)).is_empty());
280        // With a sysroot they are, because that is what naming one says.
281        let machine = Machine { sysroot: Some("/opt/cross".into()), ..on(Os::Darwin) };
282        assert!(!candidates(triple(Os::Linux, Env::Gnu), &machine).is_empty());
283    }
284
285    #[test]
286    fn an_unknown_host_offers_the_targets_own_directories_rather_than_none() {
287        // `Triple::host` answers nothing on a machine this compiler has no target for, and a
288        // native compile there is still a native compile.
289        let machine = Machine { host: None, ..Machine::default() };
290        assert_eq!(candidates(triple(Os::Linux, Env::Gnu), &machine).len(), 3);
291    }
292
293    #[test]
294    fn an_apple_target_is_the_sdk_and_nothing_else_and_nothing_without_one() {
295        let machine = Machine { sdk: Some("/S.sdk".into()), ..on(Os::Darwin) };
296        let dirs = candidates(triple(Os::Darwin, Env::None), &machine);
297        assert_eq!(dirs, [PathBuf::from("/S.sdk/usr/include")]);
298        assert!(candidates(triple(Os::Darwin, Env::None), &on(Os::Darwin)).is_empty());
299    }
300
301    #[test]
302    fn windows_is_told_where_its_headers_are_and_is_not_guessed_at() {
303        let machine =
304            Machine { include: Some(r"C:\vc\include;C:\sdk\ucrt ;".to_owned()), ..on(Os::Windows) };
305        let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
306        assert_eq!(dirs, [PathBuf::from(r"C:\vc\include"), PathBuf::from(r"C:\sdk\ucrt")]);
307        assert!(candidates(triple(Os::Windows, Env::Msvc), &on(Os::Windows)).is_empty());
308    }
309
310    #[test]
311    fn a_freestanding_target_has_no_library_to_find_the_headers_of() {
312        let machine = Machine { sysroot: Some("/opt/cross".into()), ..Machine::default() };
313        assert!(candidates(triple(Os::None, Env::None), &machine).is_empty());
314    }
315
316    #[test]
317    fn what_is_offered_on_this_machine_is_there_because_it_was_checked_for() {
318        for dir in system_dirs(Triple::host().unwrap_or(triple(Os::Linux, Env::Gnu)), None) {
319            assert!(dir.is_dir(), "{}", dir.display());
320        }
321    }
322}