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}