Skip to main content

rucc_sysroot/
manifest.rs

1//! What a produced sysroot carries: every input, where it came from, its hash and its licence.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.6 for the licence half,
4//! `spec/cross-compile/13-distribution.md` section 13.5 for the provenance half, and
5//! `spec/cross-compile/02-the-goal.md` claim 5 for the rest.
6//!
7//! # Three jobs, one file
8//!
9//! Claim 5 asks for byte identical output from two hosts for the same target. Checking that over a
10//! sysroot means comparing several thousand files, and the first thing anybody does when the
11//! comparison fails is ask which file and where it came from. A manifest answers both, and comparing
12//! two manifests is a diff of a few hundred lines rather than of a directory tree.
13//! [`Manifest::digest`] is the same comparison in one line, for when the answer wanted is yes or no
14//! rather than which file.
15//!
16//! The second job is the licence wall. Section 8.6 says the macOS SDK and the Windows SDK cannot be
17//! redistributed, and the way that rule gets enforced rather than remembered is that every input
18//! carries its licence and [`Manifest::redistributable`] is a function anything that publishes an
19//! artifact can call. A rule in a document is a rule somebody breaks in eighteen months.
20//!
21//! The third is provenance. Section 13.5 asks the compiler to emit, for every input that is not its
22//! own code, the name, the upstream project, the version, the source URL, the content hash, the
23//! licence and whether the input was bundled, generated or fetched. That is this file's line with
24//! two fields added, so `-print-sysroot-provenance` prints a manifest rather than a second format
25//! saying the same things in a different order.
26//!
27//! # The format
28//!
29//! Tab separated lines, sorted by path, under a header that is two lines and sometimes three: the
30//! format version, the target, and the Linux release the kernel headers came out of when the sysroot
31//! has kernel headers in it. Not JSON, because the thing this is optimized for is a person reading a
32//! diff between two of them, and not TOML, because it has no nesting and a parser for it is thirty
33//! lines. Sorted because the order files come out of a directory walk is a property of the
34//! filesystem, and a manifest whose line order depended on that would report a difference between
35//! two identical sysroots.
36//!
37//! ```
38//! use rucc_sysroot::{Input, Licence, Manifest, Provenance};
39//! use rucc_tuple::{TargetTuple, Version};
40//!
41//! let target: TargetTuple = "aarch64-linux-musl".parse().unwrap();
42//! let mut manifest = Manifest::new(target);
43//! manifest.set_kernel(Version::new(6, 12));
44//! manifest.push(Input {
45//!     path: "include/generic/stdio.h".into(),
46//!     source: "musl-1.2.5".into(),
47//!     url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".into(),
48//!     sha256: "0".repeat(64),
49//!     licence: Licence::Mit,
50//!     provenance: Provenance::Bundled,
51//! });
52//!
53//! let text = manifest.render();
54//! assert_eq!(Manifest::parse(&text).unwrap(), manifest);
55//! assert!(manifest.redistributable());
56//! ```
57
58use std::fmt;
59use std::str::FromStr;
60
61use rucc_tuple::{TargetTuple, Version};
62
63/// The licence an input arrives under.
64///
65/// The list is section 8.2's table with one variant per row, plus the kernel headers, which every
66/// Linux row needs and which no row is about. A closed list rather than a free text field, because
67/// the question [`Licence::redistributable`] answers has to have an answer for every input and a
68/// string does not.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
70pub enum Licence {
71    /// musl, which is MIT and the reason it goes first.
72    Mit,
73    /// glibc, which is LGPL. Redistributable, with obligations that
74    /// `spec/cross-compile/13-distribution.md` owns.
75    Lgpl,
76    /// The BSD libcs, which are permissive.
77    Bsd,
78    /// The Linux uapi headers, which are GPL-2.0 with the syscall note.
79    ///
80    /// The note is the whole reason this is a separate variant and not a refusal: it says that
81    /// using the headers to make a system call does not put the calling program under the GPL,
82    /// which is what every libc and every cross toolchain relies on. Redistributing the headers
83    /// themselves carries the GPL's own obligation, and the pinned source URL in the manifest is
84    /// how it is met, the same way it is for glibc.
85    LinuxUapi,
86    /// mingw-w64, which is a mix of permissive licences and public domain headers.
87    MingwPermissive,
88    /// Ours. The compiler's own headers and its runtime.
89    Apache2,
90    /// The macOS SDK, restricted by the Xcode agreement to Apple-branded hardware. Never shipped,
91    /// only ever pointed at.
92    AppleSdk,
93    /// The Windows SDK and the universal CRT, which are not redistributable.
94    MicrosoftSdk,
95}
96
97impl Licence {
98    /// Whether an artifact containing this input can be published.
99    ///
100    /// Two of the eight answer false, and they are the two section 8.6 calls legal walls rather
101    /// than engineering. A sysroot containing either is a local thing on the machine of somebody
102    /// who accepted the licence themselves.
103    #[must_use]
104    pub const fn redistributable(self) -> bool {
105        !matches!(self, Licence::AppleSdk | Licence::MicrosoftSdk)
106    }
107
108    /// The spelling in a manifest file.
109    #[must_use]
110    pub const fn as_str(self) -> &'static str {
111        match self {
112            Licence::Mit => "mit",
113            Licence::Lgpl => "lgpl",
114            Licence::Bsd => "bsd",
115            Licence::LinuxUapi => "gpl-2.0-with-linux-syscall-note",
116            Licence::MingwPermissive => "mingw-permissive",
117            Licence::Apache2 => "apache-2.0",
118            Licence::AppleSdk => "apple-sdk",
119            Licence::MicrosoftSdk => "microsoft-sdk",
120        }
121    }
122}
123
124impl fmt::Display for Licence {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.write_str(self.as_str())
127    }
128}
129
130impl FromStr for Licence {
131    type Err = ManifestError;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        match s {
135            "mit" => Ok(Licence::Mit),
136            "lgpl" => Ok(Licence::Lgpl),
137            "bsd" => Ok(Licence::Bsd),
138            "gpl-2.0-with-linux-syscall-note" => Ok(Licence::LinuxUapi),
139            "mingw-permissive" => Ok(Licence::MingwPermissive),
140            "apache-2.0" => Ok(Licence::Apache2),
141            "apple-sdk" => Ok(Licence::AppleSdk),
142            "microsoft-sdk" => Ok(Licence::MicrosoftSdk),
143            other => Err(ManifestError::UnknownLicence(other.to_string())),
144        }
145    }
146}
147
148/// How an input got to where it is.
149///
150/// Section 13.5 asks for this field and does not say what the three answers mean, which makes the
151/// meaning a decision rather than a transcription. The question each answer has to settle is what a
152/// person reproducing a file has to have: an upstream release is enough for one of them, our own
153/// generator and its version is needed for the second, and the third did not exist on any machine
154/// until somebody's network fetched it.
155///
156/// The test to apply is whether the bytes can be found in the upstream release. A header we unpack
157/// and ship unchanged can be, so it is [`Provenance::Bundled`]. A stub shared object, a merged header
158/// tree or a linker script cannot be, because this compiler wrote it, so it is
159/// [`Provenance::Generated`] even though what it was derived from is upstream's. Anything that arrived
160/// over the network after the release was built is [`Provenance::Fetched`], whoever wrote it.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
162pub enum Provenance {
163    /// Shipped inside the distribution, byte for byte as upstream released it. Reproducing it needs
164    /// the release named in [`Input::source`] and nothing of ours.
165    Bundled,
166    /// Written by this compiler from something upstream describes, which is the stubs of
167    /// `spec/cross-compile/09-libc-stubs.md` and the merged header trees of section 8.3.
168    /// Reproducing it needs our generator at the version that wrote it as well as the release.
169    Generated,
170    /// Downloaded onto this machine, which for the two licence walls of section 13.4 is the only way
171    /// the input can legally arrive at all. The record says so because a sysroot holding one is not
172    /// the same artifact as a sysroot that shipped complete.
173    Fetched,
174}
175
176impl Provenance {
177    /// The spelling in a manifest file.
178    #[must_use]
179    pub const fn as_str(self) -> &'static str {
180        match self {
181            Provenance::Bundled => "bundled",
182            Provenance::Generated => "generated",
183            Provenance::Fetched => "fetched",
184        }
185    }
186}
187
188impl fmt::Display for Provenance {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        f.write_str(self.as_str())
191    }
192}
193
194impl FromStr for Provenance {
195    type Err = ManifestError;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        match s {
199            "bundled" => Ok(Provenance::Bundled),
200            "generated" => Ok(Provenance::Generated),
201            "fetched" => Ok(Provenance::Fetched),
202            other => Err(ManifestError::UnknownProvenance(other.to_string())),
203        }
204    }
205}
206
207/// One file in a sysroot, and everything that has to be true of it.
208#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
209pub struct Input {
210    /// Where it sits, relative to the sysroot root. Relative because an absolute path is a fact
211    /// about the machine that built it, and two hosts have different ones.
212    pub path: String,
213    /// What it came out of, named so that the same manifest can be produced again. A release name
214    /// and version rather than a URL, because a URL moves and a release does not.
215    pub source: String,
216    /// Where that release was fetched from, which is the field section 13.5 asks for by name.
217    ///
218    /// The URL of the release rather than of the file, because what anybody checking this does is
219    /// download the release and look inside it, and because a per file URL would be a claim about
220    /// somebody else's directory layout. It is here in spite of a URL moving and a release not,
221    /// which is the reason [`Input::source`] exists and is not replaced by this: the two fields
222    /// answer what it is and where it was got, and only the first of those is still true in ten
223    /// years.
224    pub url: String,
225    /// The hash of the file, lowercase hex.
226    pub sha256: String,
227    /// What it may be done with.
228    pub licence: Licence,
229    /// Whether it was bundled, generated or fetched.
230    pub provenance: Provenance,
231}
232
233/// The record of one produced sysroot.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct Manifest {
236    target: TargetTuple,
237    kernel: Option<Version>,
238    inputs: Vec<Input>,
239}
240
241/// What went wrong reading a manifest.
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum ManifestError {
244    /// The first line was not the one this format starts with.
245    NotAManifest,
246    /// The version in the header is one this build does not read.
247    UnknownVersion(String),
248    /// The second line did not name a target, or named one that does not parse.
249    BadTarget(String),
250    /// The `kernel` line was there and what followed it is not a Linux release.
251    BadKernel(String),
252    /// A line did not have the six fields an input has.
253    BadInput {
254        /// Which line, counting from one.
255        line: usize,
256        /// How many tab separated fields it had.
257        fields: usize,
258    },
259    /// A hash that is not sixty four lowercase hex characters.
260    BadHash {
261        /// Which line, counting from one.
262        line: usize,
263        /// What was there instead.
264        found: String,
265    },
266    /// A licence spelling nothing here knows.
267    UnknownLicence(String),
268    /// A provenance spelling nothing here knows.
269    UnknownProvenance(String),
270    /// An empty field where a source, a URL or a path belongs. A record with a hole in it is worse
271    /// than no record, because it reads as an answer.
272    EmptyField {
273        /// Which line, counting from one.
274        line: usize,
275        /// Which field was empty, spelled the way this module names it.
276        field: &'static str,
277    },
278}
279
280impl fmt::Display for ManifestError {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        match self {
283            ManifestError::NotAManifest => write!(f, "this does not start like a sysroot manifest"),
284            ManifestError::UnknownVersion(v) => {
285                write!(f, "manifest format version {v}, which this build does not read")
286            }
287            ManifestError::BadTarget(t) => write!(f, "`{t}` is not a target this understands"),
288            ManifestError::BadKernel(k) => {
289                write!(f, "`{k}` is not a Linux release, which is what a kernel line carries")
290            }
291            ManifestError::BadInput { line, fields } => {
292                write!(f, "line {line} has {fields} fields where an input has six")
293            }
294            ManifestError::BadHash { line, found } => {
295                write!(f, "line {line} has `{found}` where a sha256 belongs")
296            }
297            ManifestError::UnknownLicence(l) => write!(f, "`{l}` is not a licence this knows"),
298            ManifestError::UnknownProvenance(o) => {
299                write!(f, "`{o}` is not bundled, generated or fetched")
300            }
301            ManifestError::EmptyField { line, field } => {
302                write!(f, "line {line} has nothing where its {field} belongs")
303            }
304        }
305    }
306}
307
308impl std::error::Error for ManifestError {}
309
310/// The first line of every manifest, which is also how one is recognized.
311///
312/// Version 2 is version 1 with the source URL and the provenance on every line, which is what
313/// section 13.5 asks a record of an input to carry. The number went up rather than the two fields
314/// being optional, because a reader that accepted both would have to decide what a missing
315/// provenance means and there is no honest answer to that: an input nobody wrote a provenance for is
316/// an input whose provenance nobody knows. Nothing has written a version 1 file to a place that
317/// outlives a build, so the only cost of the bump is this sentence.
318///
319/// Version 3 adds the optional `kernel` line. The number went up even though the line is optional,
320/// because the whole point of a format version is that a reader can say it does not read a file, and
321/// a version 2 reader handed a file with a `kernel` line in it would report the line as an input
322/// with two fields rather than as a format it does not know.
323const HEADER: &str = "rucc sysroot manifest 3";
324
325impl Manifest {
326    /// An empty manifest for this target.
327    #[must_use]
328    pub const fn new(target: TargetTuple) -> Self {
329        Manifest { target, kernel: None, inputs: Vec::new() }
330    }
331
332    /// The target this sysroot is for.
333    #[must_use]
334    pub const fn target(&self) -> TargetTuple {
335        self.target
336    }
337
338    /// The Linux release the kernel headers in this sysroot came out of, when one was recorded.
339    ///
340    /// Absent has one meaning and it is not "nobody knows": it is that this sysroot has no kernel
341    /// headers in it. Every target that is not Linux is in that case, and so is a Linux sysroot
342    /// produced before a kernel tree was installed beside it, which is a state the producer allows
343    /// because the two trees are two commands. That is the one thing an optional line can mean here
344    /// and it is why this one is optional where the provenance field is not: a file with no kernel
345    /// headers in it has no kernel version, and an input always came from somewhere.
346    ///
347    /// What it is for is the question somebody asks after a cross build read a header nobody
348    /// expected. `-print-sysroot` answers where and the manifest answers what, and a sysroot whose
349    /// record names the release its `linux/` headers came out of makes a stale pairing visible
350    /// instead of leaving it to be guessed at. Nothing here checks the version against the headers
351    /// themselves, which is tamnd/rucc#925's argument applied to the kernel tree rather than to
352    /// glibc.
353    #[must_use]
354    pub const fn kernel(&self) -> Option<Version> {
355        self.kernel
356    }
357
358    /// Record which Linux release the kernel headers came out of.
359    ///
360    /// Infallible, and in particular it does not refuse a target that has no kernel headers. The
361    /// property this type owes its callers is that [`Manifest::parse`] reads back what
362    /// [`Manifest::render`] wrote, so the reader accepts every manifest a producer can build and a
363    /// `kernel` line on a Windows sysroot is a bug in the producer rather than a corrupt file.
364    pub const fn set_kernel(&mut self, version: Version) {
365        self.kernel = Some(version);
366    }
367
368    /// Every input, in the order they were added.
369    #[must_use]
370    pub fn inputs(&self) -> &[Input] {
371        &self.inputs
372    }
373
374    /// Record one input.
375    pub fn push(&mut self, input: Input) {
376        self.inputs.push(input);
377    }
378
379    /// Whether an artifact containing this whole sysroot can be published.
380    ///
381    /// One input under a licence that says no makes the answer no, which is the only reading of a
382    /// licence wall that is worth anything.
383    #[must_use]
384    pub fn redistributable(&self) -> bool {
385        self.inputs.iter().all(|input| input.licence.redistributable())
386    }
387
388    /// Every distinct source in the manifest, sorted.
389    ///
390    /// What a person asks first when two manifests differ, and what a licence notice is generated
391    /// from.
392    #[must_use]
393    pub fn sources(&self) -> Vec<&str> {
394        let mut sources: Vec<&str> =
395            self.inputs.iter().map(|input| input.source.as_str()).collect();
396        sources.sort_unstable();
397        sources.dedup();
398        sources
399    }
400
401    /// The manifest as text, sorted by path.
402    ///
403    /// The sort is what makes two runs comparable. A directory walk returns files in whatever order
404    /// the filesystem keeps them, which differs between ext4 and APFS and sometimes between two
405    /// runs on one of them, and a manifest that carried that order would report a difference
406    /// between two identical sysroots.
407    #[must_use]
408    pub fn render(&self) -> String {
409        let mut sorted = self.inputs.clone();
410        sorted.sort();
411
412        let mut text = String::new();
413        text.push_str(HEADER);
414        text.push('\n');
415        text.push_str("target\t");
416        text.push_str(&self.target.to_canonical_string());
417        text.push('\n');
418        if let Some(kernel) = self.kernel {
419            text.push_str("kernel\t");
420            text.push_str(&kernel.to_string());
421            text.push('\n');
422        }
423        for input in &sorted {
424            text.push_str(&input.path);
425            text.push('\t');
426            text.push_str(&input.source);
427            text.push('\t');
428            text.push_str(&input.url);
429            text.push('\t');
430            text.push_str(&input.sha256);
431            text.push('\t');
432            text.push_str(input.licence.as_str());
433            text.push('\t');
434            text.push_str(input.provenance.as_str());
435            text.push('\n');
436        }
437        text
438    }
439
440    /// One number naming everything this sysroot is made of: the sha256 of [`Manifest::render`],
441    /// as sixty four lowercase hex characters.
442    ///
443    /// The same number `sha256sum` prints for the manifest file itself, which is the property worth
444    /// having. Whoever is handed a digest can check it with a tool they already have, and a digest
445    /// that only our own code could compute would be a claim nobody can audit.
446    ///
447    /// # What it is for
448    ///
449    /// `spec/cross-compile/13-distribution.md` section 13.2 asks for the hash of a cache directory's
450    /// contents in the directory's name, and a name cannot carry one: the path has to be computable
451    /// before anything has been read, by the producer that is about to write the files and by the
452    /// compiler that is about to read them, and neither of them has the contents when it asks. What
453    /// the rule wanted is a way to say in one line what is under a directory, and this is that line.
454    /// Two hosts producing a sysroot for one target compare a digest instead of a few thousand
455    /// files, and a digest published with a release can be held against a directory on a machine.
456    ///
457    /// # What it covers
458    ///
459    /// What the manifest covers, which is every file in the sysroot and the Linux release its kernel
460    /// headers came out of. Not the kernel tree's own files, because they are not in the sysroot:
461    /// one tree serves every Linux target, so it sits in the cache beside the sysroots and
462    /// [`crate::Manifest::kernel`] is what a sysroot says about it.
463    ///
464    /// It is a fact about the target and its inputs rather than about the host, because the render
465    /// is sorted, holds no absolute path and holds no timestamp. That is the same argument
466    /// `spec/cross-compile/02-the-goal.md` claim 5 rests on, applied to the record rather than to
467    /// the output.
468    #[must_use]
469    pub fn digest(&self) -> String {
470        crate::sha256::hex(self.render().as_bytes())
471    }
472
473    /// Read a manifest back.
474    ///
475    /// # Errors
476    ///
477    /// Returns which line was wrong and what was wrong with it. A manifest that fails to parse is
478    /// a cache entry somebody has to decide about, and "invalid manifest" is not enough to decide
479    /// with.
480    pub fn parse(text: &str) -> Result<Self, ManifestError> {
481        let mut lines = text.lines().enumerate().peekable();
482
483        let (_, first) = lines.next().ok_or(ManifestError::NotAManifest)?;
484        if first != HEADER {
485            let Some(version) = first.strip_prefix("rucc sysroot manifest ") else {
486                return Err(ManifestError::NotAManifest);
487            };
488            return Err(ManifestError::UnknownVersion(version.to_string()));
489        }
490
491        let (_, second) = lines.next().ok_or(ManifestError::NotAManifest)?;
492        let spelling = second
493            .strip_prefix("target\t")
494            .ok_or_else(|| ManifestError::BadTarget(second.into()))?;
495        let target = TargetTuple::from_str(spelling)
496            .map_err(|_| ManifestError::BadTarget(spelling.to_string()))?;
497
498        let mut manifest = Manifest::new(target);
499
500        // The kernel line is read only here, immediately after the target, rather than wherever it
501        // turns up. The render order is what makes two manifests comparable with `diff`, and a
502        // reader that took the line anywhere would accept files that do not compare.
503        if let Some(spelling) = lines.peek().and_then(|(_, line)| line.strip_prefix("kernel\t")) {
504            let version = Version::parse(spelling)
505                .ok_or_else(|| ManifestError::BadKernel(spelling.into()))?;
506            manifest.set_kernel(version);
507            lines.next();
508        }
509
510        for (index, line) in lines {
511            if line.is_empty() {
512                continue;
513            }
514            let number = index + 1;
515            let fields: Vec<&str> = line.split('\t').collect();
516            let [path, source, url, sha256, licence, provenance] = fields.as_slice() else {
517                return Err(ManifestError::BadInput { line: number, fields: fields.len() });
518            };
519            if !is_sha256(sha256) {
520                return Err(ManifestError::BadHash { line: number, found: (*sha256).to_string() });
521            }
522            for (value, field) in [(path, "path"), (source, "source"), (url, "url")] {
523                if value.is_empty() {
524                    return Err(ManifestError::EmptyField { line: number, field });
525                }
526            }
527            manifest.push(Input {
528                path: (*path).to_string(),
529                source: (*source).to_string(),
530                url: (*url).to_string(),
531                sha256: (*sha256).to_string(),
532                licence: licence.parse()?,
533                provenance: provenance.parse()?,
534            });
535        }
536        Ok(manifest)
537    }
538}
539
540/// Whether this is sixty four lowercase hex characters.
541///
542/// Checked on the way in rather than assumed, because a manifest with a truncated hash in it is a
543/// manifest that verifies nothing while looking like it does.
544fn is_sha256(s: &str) -> bool {
545    s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
546}