Skip to main content

rucc_sysroot/
layout.rs

1//! The directory layout of one target's sysroot, and the cache key that names it.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` sections 8.2 and 8.3.
4//!
5//! # Why the directories are split the way they are
6//!
7//! Section 8.3 is about a multiplication. Headers are naively `arch x os x libc x libc-version`
8//! trees, which for glibc alone is eight architectures times six versions and several hundred
9//! megabytes, and `spec/cross-compile/13-distribution.md` has a size budget that number destroys.
10//!
11//! The fix is two splits that turn the product into a sum. The per version differences go inside
12//! the files as `#if __GLIBC_MINOR__ >= n`, so one tree serves every version. The per architecture
13//! differences stay in directories, because they are whole files rather than lines, but only for
14//! the small part of a libc that has any: `bits/` and a handful of others. Everything else is one
15//! copy.
16//!
17//! That is why a sysroot here has two include directories rather than one. [`Sysroot::arch_include`]
18//! holds the files that differ by architecture and is searched first, and
19//! [`Sysroot::generic_include`] holds the copy that every architecture shares.
20//!
21//! A Linux target searches four directories and not two, because the kernel's headers are a second
22//! pair with the same split and a different owner. They are [`Kernel`], their root is the cache
23//! rather than a sysroot, and the order is the libc's two and then the kernel's two, which is the
24//! order `zig cc -E -v` prints for a glibc target. `linux/` and `asm/` are nine megabytes of files
25//! that are the same for every target, so one tree is shared and only `asm/` is copied per
26//! architecture.
27//!
28//! # Why the root is a function of the tuple
29//!
30//! `spec/cross-compile/02-the-goal.md` claim 5 asks for byte identical output from different hosts.
31//! A sysroot that lands in a directory named after the host, or after the day it was built, or
32//! after a hash of an absolute path, breaks that before anything is compiled. So the root is the
33//! cache directory the caller chose plus the canonical spelling of the tuple, and nothing else.
34//!
35//! The canonical spelling is the key rather than a hash of it because it is already unique, it is
36//! already a legal directory name, and a cache a person can read is a cache a person can debug. It
37//! carries the whole ten field model, so `x86_64-linux-gnu` and `x86_64-linux-gnu.2.28` are
38//! different directories, which is the point of `env_version` being in the tuple at all.
39
40use std::fmt;
41use std::path::{Path, PathBuf};
42
43use rucc_tuple::{Arch, DataModel, Env, Os, TargetTuple, Version};
44
45/// One target's sysroot: where its headers are, where its link inputs are, and where the record
46/// of what they are is.
47///
48/// Constructed rather than discovered. Nothing here checks that any of these directories exists,
49/// because the caller that is about to produce a sysroot needs the same answer as the caller that
50/// is about to read one, and a constructor that failed for an absent directory would give the
51/// first one nothing to create.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Sysroot {
54    target: TargetTuple,
55    root: PathBuf,
56}
57
58impl Sysroot {
59    /// The sysroot for this target inside this cache directory.
60    ///
61    /// The path is `<cache>/sysroots/<canonical tuple>`. Two hosts running this with the same
62    /// cache directory and the same target get the same path, which is what makes the tuple a
63    /// cache key and is the reason `spec/cross-compile/03-target-model.md` section 3.2 admits a
64    /// field only when it changes how a call is made or a struct is laid out.
65    #[must_use]
66    pub fn in_cache(cache: &Path, target: TargetTuple) -> Self {
67        let root = cache.join("sysroots").join(target.to_canonical_string());
68        Sysroot { target, root }
69    }
70
71    /// A sysroot rooted at a directory the user named, with `--sysroot` or `-isysroot`.
72    ///
73    /// The layout below the root is the same, so a user who assembled a tree the way we lay one
74    /// out is served by every other method here. A user who did not is served by
75    /// [`Options::sysroot`](crate::Options::sysroot), which replaces step 3 of section 8.5
76    /// wholesale rather than assuming a shape.
77    #[must_use]
78    pub fn at(root: PathBuf, target: TargetTuple) -> Self {
79        Sysroot { target, root }
80    }
81
82    /// The target this sysroot is for.
83    #[must_use]
84    pub const fn target(&self) -> TargetTuple {
85        self.target
86    }
87
88    /// The directory everything else here is under.
89    #[must_use]
90    pub fn root(&self) -> &Path {
91        &self.root
92    }
93
94    /// The cache key, which is the canonical spelling of the tuple.
95    ///
96    /// The whole tuple and not a summary of it. A key that dropped `env_version` would serve a
97    /// sysroot built against glibc 2.28 to a target that pinned 2.34, and the failure would be a
98    /// missing symbol at link time on one machine and not on another.
99    #[must_use]
100    pub fn cache_key(&self) -> String {
101        self.target.to_canonical_string()
102    }
103
104    /// The headers that differ by architecture, which are searched before the generic ones.
105    ///
106    /// Section 8.3's second split. For musl this is `bits/`, which is a few dozen small files
107    /// against a few hundred shared ones, so the copy per architecture is cheap and the
108    /// alternative of one whole tree per architecture is not.
109    #[must_use]
110    pub fn arch_include(&self) -> PathBuf {
111        self.root.join("include").join(self.header_arch())
112    }
113
114    /// The headers every architecture shares, which is almost all of them.
115    #[must_use]
116    pub fn generic_include(&self) -> PathBuf {
117        self.root.join("include").join("generic")
118    }
119
120    /// The libc's include directories, in search order, most specific first.
121    ///
122    /// Two rather than four. A Linux target also needs the kernel's own headers, which are
123    /// [`Kernel`] and are not under this root, because they are the same files for every target
124    /// that shares an architecture and carrying a copy of them per tuple is nine megabytes times
125    /// the size of the table.
126    #[must_use]
127    pub fn includes(&self) -> Vec<PathBuf> {
128        vec![self.arch_include(), self.generic_include()]
129    }
130
131    /// The link inputs: the start files, the libc archive or its generated stubs, and the
132    /// compiler's own runtime for this target.
133    #[must_use]
134    pub fn lib(&self) -> PathBuf {
135        self.root.join("lib")
136    }
137
138    /// The manifest naming every input with its source, its hash and its licence.
139    ///
140    /// A file rather than a directory, and at the top rather than beside the libraries, because
141    /// the thing a person does with it is read it first.
142    #[must_use]
143    pub fn manifest_path(&self) -> PathBuf {
144        self.root.join("manifest")
145    }
146
147    /// The name the target's libc gives to its per architecture header directory.
148    ///
149    /// Not the architecture component of the canonical tuple, which carries a baseline the headers
150    /// do not care about: `armv7a-linux-musleabihf` and `armv5te-linux-musleabi` read the same
151    /// `arm` directory, because a header does not know which instructions the chip has. 32-bit x86
152    /// is `i386` in musl's source tree whatever the tuple spells it.
153    ///
154    /// # Why the libc is part of the answer
155    ///
156    /// The two libcs do not split their headers at the same place, and the name has to follow the
157    /// libc rather than a scheme of ours, because the producer installs what the libc's own build
158    /// system installs and the compiler has to look where that put it.
159    ///
160    /// musl splits per architecture and per ABI, which is what `arch/` in its source tree is, so
161    /// `x86_64`, `i386` and `x32` are three directories. glibc splits per architecture family and
162    /// handles the rest inside the files: one `x86` directory serves i386, x86-64 and x32, and 22
163    /// of the 31 files in its `bits/` branch on `__x86_64__`, `__ILP32__` or `__WORDSIZE` to do it,
164    /// starting with `bits/wordsize.h`. Checked against Zig 0.16, which ships twelve glibc
165    /// directories named after families and seventeen musl directories named after architectures.
166    ///
167    /// # The rule this is here to enforce
168    ///
169    /// An ILP32 ABI on a 64-bit architecture cannot read the LP64 headers. Every type that carries
170    /// a pointer or a `long` is a different size, and `x86_64-linux-gnux32` is the row that proves
171    /// it. For musl that is a separate directory, which is what the suffix below is. For glibc it
172    /// is a branch inside glibc's own files, so the directory is shared and the thing that checks
173    /// it is section 8.4's structural equivalence corpus rather than a path.
174    #[must_use]
175    pub fn header_arch(&self) -> &'static str {
176        if self.target.env() == Env::Gnu {
177            return self.header_family();
178        }
179        let narrow = self.target.data_model() == DataModel::Ilp32On64;
180        match (self.target.arch(), narrow) {
181            // x32 is what everyone calls it, including musl and glibc, so it does not get the
182            // suffix the rule below would give it.
183            (Arch::X86_64, true) => "x32",
184            (Arch::X86_64, false) => "x86_64",
185            (Arch::X86, _) => "i386",
186            (Arch::Aarch64 | Arch::Arm64Ec, true) => "aarch64_ilp32",
187            (Arch::Aarch64 | Arch::Arm64Ec, false) => "aarch64",
188            (Arch::Arm, _) => "arm",
189            (Arch::Riscv64, true) => "riscv64_ilp32",
190            (Arch::Riscv64, false) => "riscv64",
191            (Arch::Riscv32, _) => "riscv32",
192            (Arch::S390x, true) => "s390x_ilp32",
193            (Arch::S390x, false) => "s390x",
194            (Arch::PowerPc64, true) => "powerpc64_ilp32",
195            (Arch::PowerPc64, false) => "powerpc64",
196            (Arch::LoongArch64, true) => "loongarch64_ilp32",
197            (Arch::LoongArch64, false) => "loongarch64",
198            (Arch::Wasm32, _) => "wasm32",
199        }
200    }
201
202    /// The architecture family, which is how glibc names its per architecture header directory.
203    ///
204    /// The data model is not in it, on purpose, for the reason [`Sysroot::header_arch`] gives: the
205    /// family's files carry the branch themselves. `s390x` and `loongarch` are spelled the way
206    /// glibc's own `sysdeps` tree spells them, which is not the same shortening for both.
207    fn header_family(&self) -> &'static str {
208        match self.target.arch() {
209            Arch::X86_64 | Arch::X86 => "x86",
210            // Arm64EC is a Windows ABI and never has glibc headers. It answers with the family it
211            // belongs to rather than with a word that is not a directory anywhere.
212            Arch::Aarch64 | Arch::Arm64Ec => "aarch64",
213            Arch::Arm => "arm",
214            Arch::Riscv64 | Arch::Riscv32 => "riscv",
215            Arch::S390x => "s390x",
216            Arch::PowerPc64 => "powerpc",
217            Arch::LoongArch64 => "loongarch",
218            // There is no glibc for wasm. The arm of the match exists because the type is closed
219            // and a wildcard here would quietly name a directory for a future architecture.
220            Arch::Wasm32 => "wasm32",
221        }
222    }
223}
224
225/// The kernel's own headers, which are not the libc's and are shared by every target that can read
226/// them.
227///
228/// `linux/` and `asm/` are the system call interface rather than the C library, and a sysroot
229/// without them does not compile 31 of glibc's installed headers or 3 of musl's, `sys/quota.h` and
230/// `net/ethernet.h` among them. So they are part of what section 8.2 calls a sysroot even though no
231/// libc produced them.
232///
233/// # Why they are not under [`Sysroot`]
234///
235/// One tree serves every libc and every architecture except `asm/`, which is per architecture and
236/// small. Copying the shared part into each tuple's sysroot would be nine megabytes times the
237/// number of Linux rows in the table, for files that are identical in every copy. So the root is
238/// the cache directory rather than a sysroot, and a sysroot that was produced with it records the
239/// version in its manifest.
240///
241/// # Why the version is not in the path
242///
243/// The driver has to be able to compute this path before it reads anything, and a version in the
244/// path would mean asking the cache what it has before being able to ask where it is. It is the
245/// same decision [`Sysroot::in_cache`] makes about the libc version, where the tuple carries the
246/// version only because `env_version` is part of the target's identity, and the same gap: a cache
247/// populated by one release and read by the next gets whatever is there.
248/// `spec/cross-compile/13-distribution.md` section 13.2 owns that, because the answer is the
249/// content hash in the cache layout and it belongs to both.
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct Kernel {
252    arch: &'static str,
253    root: PathBuf,
254}
255
256impl Kernel {
257    /// The kernel headers in this cache directory for this target, when the target has any.
258    ///
259    /// [`None`] for everything that is not Linux with a libc we produce a tree for. Windows, the
260    /// BSDs and Darwin have their own system headers and no `linux/` at all, freestanding has no
261    /// system call interface by definition, and Android is Linux but bionic carries its own
262    /// scrubbed copy of the uapi headers, which is a different tree from this one and not a subset
263    /// of it.
264    #[must_use]
265    pub fn for_target(cache: &Path, target: TargetTuple) -> Option<Kernel> {
266        if target.os() != Os::Linux || !matches!(target.env(), Env::Gnu | Env::Musl) {
267            return None;
268        }
269        let arch = kernel_arch(target.arch())?;
270        Some(Kernel { arch, root: cache.join("kernel-headers") })
271    }
272
273    /// The directory both of these are under.
274    #[must_use]
275    pub fn root(&self) -> &Path {
276        &self.root
277    }
278
279    /// `asm/`, which is the part of the interface that is per architecture.
280    ///
281    /// Named after the kernel's own architecture directory and not after ours or the libc's, which
282    /// is a third spelling of the same machine and the reason this is a method rather than a format
283    /// string at the call site.
284    #[must_use]
285    pub fn arch_include(&self) -> PathBuf {
286        self.root.join(self.arch)
287    }
288
289    /// `linux/`, `asm-generic/` and the rest, which are the same files for every architecture.
290    #[must_use]
291    pub fn generic_include(&self) -> PathBuf {
292        self.root.join("generic")
293    }
294
295    /// Both directories, in search order, most specific first.
296    #[must_use]
297    pub fn includes(&self) -> Vec<PathBuf> {
298        vec![self.arch_include(), self.generic_include()]
299    }
300
301    /// The kernel's name for this architecture.
302    #[must_use]
303    pub const fn arch(&self) -> &'static str {
304        self.arch
305    }
306}
307
308/// What `make headers_install ARCH=` takes, which is a third naming of the machine.
309///
310/// `arm64` rather than `aarch64` and `s390` rather than `s390x`, because those are the directories
311/// under `arch/` in the kernel's source tree, and the 31-bit s390 port leaving did not rename the
312/// one that stayed. One directory serves both widths of x86, of riscv and of powerpc, the same way
313/// glibc's family does and for the same reason: the uapi headers branch on the compiler's macros.
314///
315/// [`None`] for an architecture the kernel does not have, which is wasm.
316const fn kernel_arch(arch: Arch) -> Option<&'static str> {
317    match arch {
318        Arch::X86_64 | Arch::X86 => Some("x86"),
319        Arch::Aarch64 | Arch::Arm64Ec => Some("arm64"),
320        Arch::Arm => Some("arm"),
321        Arch::Riscv64 | Arch::Riscv32 => Some("riscv"),
322        Arch::S390x => Some("s390"),
323        Arch::PowerPc64 => Some("powerpc"),
324        Arch::LoongArch64 => Some("loongarch"),
325        Arch::Wasm32 => None,
326    }
327}
328
329/// Whether we can produce a sysroot for this target without the user fetching anything.
330///
331/// Section 8.2's table has seven rows and two of them are legal walls rather than engineering.
332/// The macOS SDK is restricted by the Xcode licence to Apple-branded hardware and the Windows SDK
333/// is not redistributable, so for those two the answer is a path the user supplies under their own
334/// licence, and `spec/cross-compile/13-distribution.md` owns the mechanism.
335///
336/// This returns false for those two and true for everything else, including freestanding, which
337/// needs nine compiler headers and no link inputs at all.
338#[must_use]
339pub fn can_be_bundled(target: TargetTuple) -> bool {
340    !matches!(target.os(), Os::MacOs | Os::IOs) && target.env() != Env::Msvc
341}
342
343/// The glibc our bundled header tree is derived from.
344///
345/// A fact about the tree and not a choice. `sysroots/manifest` in `tamnd/rucc-cross` pins the glibc
346/// source by version and hash, the tree is produced from that source, and this is that version. It
347/// moves when the pin moves and the two are checked against each other by the producer.
348pub const BUNDLED_GLIBC: Version = Version::new(2, 44);
349
350/// The `__GLIBC_MINOR__` a target gets when it is compiled against the bundled glibc tree.
351///
352/// Design: `spec/cross-compile/08-sysroots.md` section 8.3.
353///
354/// One tree serves every glibc release, with the differences written inside the files as
355/// `#if __GLIBC_MINOR__ >= n`, so the release is the part of it the target supplies. That is Zig's
356/// patch to the same tree and the same macro, which is where the spelling comes from: `features.h`
357/// keeps `__GLIBC__` at 2 and leaves the minor to the compiler, and `__GLIBC_PREREQ` reads both.
358///
359/// [`None`] for anything that is not glibc, because there is no such macro on musl or mingw and
360/// defining one would have every probe for it answer yes on a libc that does not have it.
361///
362/// The version is the one the tuple asked for, which is the point of `env_version` being in the
363/// tuple, and [`BUNDLED_GLIBC`] when it asked for nothing. Asking for an older release is how a
364/// program is kept off symbols and declarations the target's libc does not have, and it is honest
365/// only as far as the text goes: the declarations are guarded by the macro and the structure
366/// layouts in the same files are one release's. Issue #926's last box is where that is finished and
367/// it is the same direction as the compat symbol gap of #920, too permissive rather than wrong
368/// about what it does say.
369///
370/// # Errors
371///
372/// A release newer than the tree, which is the one direction that cannot be approximated. Every
373/// `__GLIBC_PREREQ` in the program would answer yes and the declarations behind them would not be
374/// there, so the failure would be a missing declaration at best and a missing symbol at link time
375/// at worst. Both versions are in the error, because the two things a person can do about it are
376/// pin a release the tree has and name a sysroot that has the one they asked for, and neither is a
377/// choice they can make without being told which release the tree is.
378pub fn bundled_glibc_minor(target: TargetTuple) -> Result<Option<u32>, GlibcSkew> {
379    if target.os() != Os::Linux || target.env() != Env::Gnu {
380        return Ok(None);
381    }
382    let Some(asked) = target.env_version() else {
383        return Ok(Some(BUNDLED_GLIBC.minor_part().unwrap_or(0)));
384    };
385    // A glibc version is two components and a tuple will hold one or three, so a request this
386    // cannot read as a glibc release is a request for the tree's own version rather than an error:
387    // `gnu.2` is somebody naming the libc and not pinning it.
388    let Some(minor) = asked.minor_part() else {
389        return Ok(Some(BUNDLED_GLIBC.minor_part().unwrap_or(0)));
390    };
391    if asked.major_part() != BUNDLED_GLIBC.major_part()
392        || minor > BUNDLED_GLIBC.minor_part().unwrap_or(0)
393    {
394        return Err(GlibcSkew { asked, tree: BUNDLED_GLIBC });
395    }
396    Ok(Some(minor))
397}
398
399/// A glibc release the bundled tree cannot serve, and the release the tree is.
400///
401/// A type rather than a pair, because the two versions read the same way round in the message as
402/// they do here and a caller that swapped them would produce a diagnostic exactly as wrong as it is
403/// convincing.
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405pub struct GlibcSkew {
406    /// What the target asked for.
407    pub asked: Version,
408    /// What the bundled tree is, which is [`BUNDLED_GLIBC`].
409    pub tree: Version,
410}
411
412impl fmt::Display for GlibcSkew {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        write!(
415            f,
416            "the target asked for glibc {}, and the bundled headers are glibc {}",
417            self.asked, self.tree
418        )
419    }
420}
421
422impl std::error::Error for GlibcSkew {}