rucc_sysroot/search.rs
1//! The header search path, as section 8.5 states it.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.5.
4//!
5//! # The failure this prevents
6//!
7//! Host contamination. A cross build picks up a header from the machine it is running on, produces
8//! something that works there, and does not work anywhere else. It is a quiet failure: the build
9//! succeeds, the tests pass on the build machine, and the binary is wrong somewhere the person who
10//! made it will not look.
11//!
12//! `spec/cross-compile/02-the-goal.md` claim 5 is the test that catches it, byte identical output
13//! from two different hosts, and it catches it only because the rule below makes step 3 a function
14//! of the target when the target is not the host. That is why [`Options::host_include`] exists as a
15//! separate field rather than as a default: there is exactly one place a host directory can enter,
16//! it is guarded by one condition, and both are in [`include_paths`] where they can be read.
17//!
18//! # The rule
19//!
20//! 1. `-I` in the order given.
21//! 2. The compiler's own headers. Always present, on every target including freestanding, and never
22//! taken from a sysroot, because `stddef.h` describes the compiler and not the C library.
23//! 3. The target's libc headers, from `--sysroot` if given, otherwise from an Apple SDK this
24//! machine has, otherwise from our bundled tree for that tuple, otherwise, and only when the
25//! target is the host, from the host's directories. For a Linux target the bundled case is four
26//! directories rather than two: the libc's per architecture tree, the libc's generic tree, the
27//! kernel's `asm/` for the architecture, and the kernel's shared tree. That is the order
28//! `zig cc -E -v` prints for a glibc target.
29//! 4. Nothing else. No `/usr/local/include` in a cross build, ever.
30//!
31//! `-nostdinc` removes 3, `-nobuiltininc` removes 2, `--sysroot` replaces 3's root, and `-isysroot`
32//! is the Darwin spelling of the same thing.
33//!
34//! # Why an SDK is a source of its own
35//!
36//! [`Options::sdk`] is the fourth of those sources and it is not one of the other three. It is not a
37//! tree the user named, because on a mac it is found by asking `xcrun` and on Windows by asking the
38//! Visual Studio installer, and nobody wrote either on the command line. It is not a bundled tree,
39//! because `spec/cross-compile/13-distribution.md` section 13.4 says we may never ship one. And it is
40//! not the host's own directories, because the SDK holds the headers of every architecture of its
41//! platform rather than of this machine, so one installed SDK serves `x86_64-macos` on an arm64 mac
42//! and one Windows Kit serves `aarch64-windows-msvc` on an x86_64 box, which is what the platform's
43//! own tools do with them.
44//!
45//! The other half of the same rule is that a target behind one of those licence walls never takes
46//! the bundled branch at all, whatever the caller passes, because [`crate::Wall`] is the statement
47//! that no tree of ours can exist for it. A path under the cache for such a target would be a
48//! directory nothing will ever put a file in, named in a `-v` listing as though a fetch were coming.
49
50use std::path::{Path, PathBuf};
51
52use rucc_tuple::TargetTuple;
53
54use crate::layout::{Kernel, Sysroot};
55use crate::wall::Wall;
56
57/// Which of section 8.5's four steps put a directory in the list.
58///
59/// Carried rather than discarded because `-print-search-dirs` has to say it, because a user
60/// debugging a wrong header needs to know which rule chose it, and because the test that no host
61/// directory appears in a cross build is written against this rather than against path spelling.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Origin {
64 /// Step 1. A `-I` the user gave, in the position they gave it.
65 User,
66 /// Step 2. The compiler's own headers, which describe the compiler rather than the platform.
67 Compiler,
68 /// Step 3, taken from a `--sysroot` or `-isysroot` the user named.
69 Sysroot,
70 /// Step 3, taken from an Apple SDK installed on this machine.
71 ///
72 /// Separate from [`Origin::Sysroot`] because nobody named it, and separate from
73 /// [`Origin::Host`] because what is in it is the target's headers and not this machine's: one
74 /// SDK holds every Apple architecture. A user who sees this in `-print-search-dirs` is being
75 /// told that the path came from Xcode rather than from their command line.
76 Sdk,
77 /// Step 3, taken from the tree we bundle for this target.
78 Bundled,
79 /// Step 3, taken from the kernel header tree, which is bundled too and is not the libc's.
80 ///
81 /// Separate from [`Origin::Bundled`] because the two trees have different owners, different
82 /// licences and different producers, and a user looking at where `linux/stat.h` came from is
83 /// asking about the kernel and not about glibc.
84 Kernel,
85 /// Step 3, taken from the host, which is legal only when the target is the host.
86 Host,
87}
88
89impl Origin {
90 /// Whether a directory from this origin belongs to the machine the compiler is running on.
91 ///
92 /// The property the cross compilation test asserts: for a target that is not the host, no entry
93 /// in the search path answers true.
94 #[must_use]
95 pub const fn is_host(self) -> bool {
96 matches!(self, Origin::Host)
97 }
98
99 /// A short word for `-print-search-dirs` and for a diagnostic that has to say where a header
100 /// came from.
101 #[must_use]
102 pub const fn as_str(self) -> &'static str {
103 match self {
104 Origin::User => "-I",
105 Origin::Compiler => "compiler",
106 Origin::Sysroot => "sysroot",
107 Origin::Sdk => "sdk",
108 Origin::Bundled => "bundled",
109 Origin::Kernel => "kernel",
110 Origin::Host => "host",
111 }
112 }
113}
114
115/// One directory in the search path, and the reason it is there.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Entry {
118 /// The directory.
119 pub path: PathBuf,
120 /// Which step of section 8.5 put it there.
121 pub origin: Origin,
122}
123
124/// What the driver knows that the rule needs.
125///
126/// A struct rather than seven arguments, because six of the seven are empty in the common case and
127/// a function with six defaulted parameters is a function somebody calls wrong.
128#[derive(Debug, Clone, Default)]
129pub struct Options<'a> {
130 /// `-I`, in the order given. Order is preserved exactly, because a user who put one `-I` before
131 /// another meant it.
132 pub user: &'a [PathBuf],
133 /// The compiler's own header directory, which is where `stddef.h` and the intrinsic headers
134 /// live. Supplied by the caller rather than found here, because finding it means asking the
135 /// host where the compiler is installed and that is not this crate's business.
136 pub resources: Option<&'a Path>,
137 /// `--sysroot` or `-isysroot`, as the directories under the tree the user named.
138 ///
139 /// Paths rather than a [`Sysroot`], because a tree somebody else assembled has whatever shape
140 /// they gave it. A buildroot or Yocto or distribution tree keeps its headers under
141 /// `usr/include` and not under the two directories [`Sysroot::includes`] names, so the caller
142 /// computes the list and this replaces step 3 with it wholesale. A user who did lay their tree
143 /// out the way we lay one out passes [`Sysroot::includes`] and gets the same thing.
144 pub sysroot: &'a [PathBuf],
145 /// The include directories of an Apple SDK this machine has, for an Apple target.
146 ///
147 /// Paths rather than a root, for the same reason [`Options::sysroot`] is paths: the layout inside
148 /// an SDK is Apple's and the caller is the half of the compiler that knows it. Empty on every
149 /// other target and on a machine that has no SDK, and the second of those is what
150 /// `spec/cross-compile/08-sysroots.md` section 8.6 turns into a refusal that names the licence.
151 pub sdk: &'a [PathBuf],
152 /// The tree we bundle for this target, when there is one.
153 ///
154 /// Ignored for a target behind a licence wall, because there is no such tree and there will not
155 /// be one. The caller is free to pass the layout it computed without having to ask.
156 pub bundled: Option<&'a Sysroot>,
157 /// The kernel headers for this target, when it has any and we have them.
158 ///
159 /// Used only with [`Options::bundled`], because it is the other half of the tree we produced.
160 /// A user who named a tree of their own named one that has a `linux/` in it or does not need
161 /// one, and putting ours underneath it would be composing with a named sysroot, which section
162 /// 8.5 does not do.
163 pub kernel: Option<&'a Kernel>,
164 /// The host's own include directories, as the driver computes them today.
165 ///
166 /// Used only when the target is the host. On any other target this field is ignored, and that
167 /// is the whole of the cross compilation guarantee in this file.
168 pub host_include: &'a [PathBuf],
169 /// `-nostdinc`. Removes step 3.
170 pub no_std_inc: bool,
171 /// `-nobuiltininc`. Removes step 2.
172 pub no_builtin_inc: bool,
173}
174
175/// The directories to search for an included file, in order.
176///
177/// `host` is what the compiler is running on, and it is an argument rather than something read from
178/// the environment so that the rule can be tested for a host it is not running on. Passing [`None`]
179/// says the host is unknown, which is treated as not being the target: an unknown host cannot be
180/// proved to be the target, and guessing yes is the contamination this function is written against.
181#[must_use]
182pub fn include_paths(
183 target: TargetTuple,
184 host: Option<TargetTuple>,
185 options: &Options<'_>,
186) -> Vec<Entry> {
187 let mut paths = Vec::new();
188
189 // Step 1. Exactly what the user said, in the order they said it.
190 for path in options.user {
191 paths.push(Entry { path: path.clone(), origin: Origin::User });
192 }
193
194 // Step 2. The compiler's own headers, on every target including freestanding. They are not in
195 // the sysroot and they never come from one: `stddef.h` describes what this compiler does with
196 // `size_t`, and a copy of it belonging to some other compiler is a different `size_t`.
197 if !options.no_builtin_inc {
198 if let Some(resources) = options.resources {
199 paths.push(Entry { path: resources.join("include"), origin: Origin::Compiler });
200 }
201 }
202
203 // Step 3. The target's libc headers, from the first of four sources that has them.
204 if !options.no_std_inc {
205 if !options.sysroot.is_empty() {
206 for path in options.sysroot {
207 paths.push(Entry { path: path.clone(), origin: Origin::Sysroot });
208 }
209 } else if !options.sdk.is_empty() {
210 // Before the bundled tree rather than after it, which costs nothing today because the
211 // only targets an SDK is found for are the ones we have no bundled tree for, and says
212 // the right thing if that ever stops being true: an SDK on the machine is the platform's
213 // own headers and ours would be a reconstruction of them.
214 for path in options.sdk {
215 paths.push(Entry { path: path.clone(), origin: Origin::Sdk });
216 }
217 // A walled target has no tree of ours anywhere, so the branch cannot fire for one even when
218 // a caller passes the layout. `Wall` is the whole of that rule and this is the only place it
219 // reaches the search path.
220 } else if let Some(bundled) = options.bundled.filter(|_| Wall::of(target).is_none()) {
221 for path in bundled.includes() {
222 paths.push(Entry { path, origin: Origin::Bundled });
223 }
224 // After the libc's, because a libc header and a kernel header with the same name are
225 // the libc's: `asm/` and `linux/` are the kernel's own names and nothing in a libc
226 // shadows them, while `sys/` exists in both and the libc's is the one a program means.
227 for path in options.kernel.map(Kernel::includes).unwrap_or_default() {
228 paths.push(Entry { path, origin: Origin::Kernel });
229 }
230 } else if host == Some(target) {
231 // The only place a host directory enters, and it is guarded by the target being the
232 // host. Everything about claim 5 rests on this one condition.
233 for path in options.host_include {
234 paths.push(Entry { path: path.clone(), origin: Origin::Host });
235 }
236 }
237 }
238
239 // Step 4 is that there is no step 4. No `/usr/local/include`, no `/usr/include` appended
240 // because the list came out short, and nothing derived from an environment variable.
241 paths
242}