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 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}