Skip to main content

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}