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