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