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 our bundled tree for
24//! that tuple, otherwise, and only when the target is the host, from the host's directories. For
25//! a Linux target the bundled case is four directories rather than two: the libc's per
26//! architecture tree, the libc's generic tree, the kernel's `asm/` for the architecture, and the
27//! kernel's shared tree. That is the order `zig cc -E -v` prints for a glibc target.
28//! 4. Nothing else. No `/usr/local/include` in a cross build, ever.
29//!
30//! `-nostdinc` removes 3, `-nobuiltininc` removes 2, `--sysroot` replaces 3's root, and `-isysroot`
31//! is the Darwin spelling of the same thing.
32
33use std::path::{Path, PathBuf};
34
35use rucc_tuple::TargetTuple;
36
37use crate::layout::{Kernel, Sysroot};
38
39/// Which of section 8.5's four steps put a directory in the list.
40///
41/// Carried rather than discarded because `-print-search-dirs` has to say it, because a user
42/// debugging a wrong header needs to know which rule chose it, and because the test that no host
43/// directory appears in a cross build is written against this rather than against path spelling.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Origin {
46 /// Step 1. A `-I` the user gave, in the position they gave it.
47 User,
48 /// Step 2. The compiler's own headers, which describe the compiler rather than the platform.
49 Compiler,
50 /// Step 3, taken from a `--sysroot` or `-isysroot` the user named.
51 Sysroot,
52 /// Step 3, taken from the tree we bundle for this target.
53 Bundled,
54 /// Step 3, taken from the kernel header tree, which is bundled too and is not the libc's.
55 ///
56 /// Separate from [`Origin::Bundled`] because the two trees have different owners, different
57 /// licences and different producers, and a user looking at where `linux/stat.h` came from is
58 /// asking about the kernel and not about glibc.
59 Kernel,
60 /// Step 3, taken from the host, which is legal only when the target is the host.
61 Host,
62}
63
64impl Origin {
65 /// Whether a directory from this origin belongs to the machine the compiler is running on.
66 ///
67 /// The property the cross compilation test asserts: for a target that is not the host, no entry
68 /// in the search path answers true.
69 #[must_use]
70 pub const fn is_host(self) -> bool {
71 matches!(self, Origin::Host)
72 }
73
74 /// A short word for `-print-search-dirs` and for a diagnostic that has to say where a header
75 /// came from.
76 #[must_use]
77 pub const fn as_str(self) -> &'static str {
78 match self {
79 Origin::User => "-I",
80 Origin::Compiler => "compiler",
81 Origin::Sysroot => "sysroot",
82 Origin::Bundled => "bundled",
83 Origin::Kernel => "kernel",
84 Origin::Host => "host",
85 }
86 }
87}
88
89/// One directory in the search path, and the reason it is there.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Entry {
92 /// The directory.
93 pub path: PathBuf,
94 /// Which step of section 8.5 put it there.
95 pub origin: Origin,
96}
97
98/// What the driver knows that the rule needs.
99///
100/// A struct rather than seven arguments, because six of the seven are empty in the common case and
101/// a function with six defaulted parameters is a function somebody calls wrong.
102#[derive(Debug, Clone, Default)]
103pub struct Options<'a> {
104 /// `-I`, in the order given. Order is preserved exactly, because a user who put one `-I` before
105 /// another meant it.
106 pub user: &'a [PathBuf],
107 /// The compiler's own header directory, which is where `stddef.h` and the intrinsic headers
108 /// live. Supplied by the caller rather than found here, because finding it means asking the
109 /// host where the compiler is installed and that is not this crate's business.
110 pub resources: Option<&'a Path>,
111 /// `--sysroot` or `-isysroot`, as the directories under the tree the user named.
112 ///
113 /// Paths rather than a [`Sysroot`], because a tree somebody else assembled has whatever shape
114 /// they gave it. A buildroot or Yocto or distribution tree keeps its headers under
115 /// `usr/include` and not under the two directories [`Sysroot::includes`] names, so the caller
116 /// computes the list and this replaces step 3 with it wholesale. A user who did lay their tree
117 /// out the way we lay one out passes [`Sysroot::includes`] and gets the same thing.
118 pub sysroot: &'a [PathBuf],
119 /// The tree we bundle for this target, when there is one.
120 pub bundled: Option<&'a Sysroot>,
121 /// The kernel headers for this target, when it has any and we have them.
122 ///
123 /// Used only with [`Options::bundled`], because it is the other half of the tree we produced.
124 /// A user who named a tree of their own named one that has a `linux/` in it or does not need
125 /// one, and putting ours underneath it would be composing with a named sysroot, which section
126 /// 8.5 does not do.
127 pub kernel: Option<&'a Kernel>,
128 /// The host's own include directories, as the driver computes them today.
129 ///
130 /// Used only when the target is the host. On any other target this field is ignored, and that
131 /// is the whole of the cross compilation guarantee in this file.
132 pub host_include: &'a [PathBuf],
133 /// `-nostdinc`. Removes step 3.
134 pub no_std_inc: bool,
135 /// `-nobuiltininc`. Removes step 2.
136 pub no_builtin_inc: bool,
137}
138
139/// The directories to search for an included file, in order.
140///
141/// `host` is what the compiler is running on, and it is an argument rather than something read from
142/// the environment so that the rule can be tested for a host it is not running on. Passing [`None`]
143/// says the host is unknown, which is treated as not being the target: an unknown host cannot be
144/// proved to be the target, and guessing yes is the contamination this function is written against.
145#[must_use]
146pub fn include_paths(
147 target: TargetTuple,
148 host: Option<TargetTuple>,
149 options: &Options<'_>,
150) -> Vec<Entry> {
151 let mut paths = Vec::new();
152
153 // Step 1. Exactly what the user said, in the order they said it.
154 for path in options.user {
155 paths.push(Entry { path: path.clone(), origin: Origin::User });
156 }
157
158 // Step 2. The compiler's own headers, on every target including freestanding. They are not in
159 // the sysroot and they never come from one: `stddef.h` describes what this compiler does with
160 // `size_t`, and a copy of it belonging to some other compiler is a different `size_t`.
161 if !options.no_builtin_inc {
162 if let Some(resources) = options.resources {
163 paths.push(Entry { path: resources.join("include"), origin: Origin::Compiler });
164 }
165 }
166
167 // Step 3. The target's libc headers, from the first of three sources that has them.
168 if !options.no_std_inc {
169 if !options.sysroot.is_empty() {
170 for path in options.sysroot {
171 paths.push(Entry { path: path.clone(), origin: Origin::Sysroot });
172 }
173 } else if let Some(bundled) = options.bundled {
174 for path in bundled.includes() {
175 paths.push(Entry { path, origin: Origin::Bundled });
176 }
177 // After the libc's, because a libc header and a kernel header with the same name are
178 // the libc's: `asm/` and `linux/` are the kernel's own names and nothing in a libc
179 // shadows them, while `sys/` exists in both and the libc's is the one a program means.
180 for path in options.kernel.map(Kernel::includes).unwrap_or_default() {
181 paths.push(Entry { path, origin: Origin::Kernel });
182 }
183 } else if host == Some(target) {
184 // The only place a host directory enters, and it is guarded by the target being the
185 // host. Everything about claim 5 rests on this one condition.
186 for path in options.host_include {
187 paths.push(Entry { path: path.clone(), origin: Origin::Host });
188 }
189 }
190 }
191
192 // Step 4 is that there is no step 4. No `/usr/local/include`, no `/usr/include` appended
193 // because the list came out short, and nothing derived from an environment variable.
194 paths
195}