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 and
4//! `spec/cross-compile/02-the-goal.md` claim 5 for the rest.
5//!
6//! # Two jobs, one file
7//!
8//! Claim 5 asks for byte identical output from two hosts for the same target. Checking that over a
9//! sysroot means comparing several thousand files, and the first thing anybody does when the
10//! comparison fails is ask which file and where it came from. A manifest answers both, and comparing
11//! two manifests is a diff of a few hundred lines rather than of a directory tree.
12//!
13//! The other job is the licence wall. Section 8.6 says the macOS SDK and the Windows SDK cannot be
14//! redistributed, and the way that rule gets enforced rather than remembered is that every input
15//! carries its licence and [`Manifest::redistributable`] is a function anything that publishes an
16//! artifact can call. A rule in a document is a rule somebody breaks in eighteen months.
17//!
18//! # The format
19//!
20//! Tab separated lines, sorted by path, with a two line header. Not JSON, because the thing this is
21//! optimized for is a person reading a diff between two of them, and not TOML, because it has no
22//! nesting and a parser for it is thirty lines. Sorted because the order files come out of a
23//! directory walk is a property of the filesystem, and a manifest whose line order depended on that
24//! would report a difference between two identical sysroots.
25//!
26//! ```
27//! use rucc_sysroot::{Input, Licence, Manifest};
28//! use rucc_tuple::TargetTuple;
29//!
30//! let target: TargetTuple = "aarch64-linux-musl".parse().unwrap();
31//! let mut manifest = Manifest::new(target);
32//! manifest.push(Input {
33//!     path: "include/generic/stdio.h".into(),
34//!     source: "musl-1.2.5".into(),
35//!     sha256: "0".repeat(64),
36//!     licence: Licence::Mit,
37//! });
38//!
39//! let text = manifest.render();
40//! assert_eq!(Manifest::parse(&text).unwrap(), manifest);
41//! assert!(manifest.redistributable());
42//! ```
43
44use std::fmt;
45use std::str::FromStr;
46
47use rucc_tuple::TargetTuple;
48
49/// The licence an input arrives under.
50///
51/// The list is section 8.2's table with one variant per row, rather than a free text field, because
52/// the question [`Licence::redistributable`] answers has to have an answer for every input and a
53/// string does not.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
55pub enum Licence {
56    /// musl, which is MIT and the reason it goes first.
57    Mit,
58    /// glibc, which is LGPL. Redistributable, with obligations that
59    /// `spec/cross-compile/13-distribution.md` owns.
60    Lgpl,
61    /// The BSD libcs, which are permissive.
62    Bsd,
63    /// mingw-w64, which is a mix of permissive licences and public domain headers.
64    MingwPermissive,
65    /// Ours. The compiler's own headers and its runtime.
66    Apache2,
67    /// The macOS SDK, restricted by the Xcode agreement to Apple-branded hardware. Never shipped,
68    /// only ever pointed at.
69    AppleSdk,
70    /// The Windows SDK and the universal CRT, which are not redistributable.
71    MicrosoftSdk,
72}
73
74impl Licence {
75    /// Whether an artifact containing this input can be published.
76    ///
77    /// Two of the seven answer false, and they are the two section 8.6 calls legal walls rather
78    /// than engineering. A sysroot containing either is a local thing on the machine of somebody
79    /// who accepted the licence themselves.
80    #[must_use]
81    pub const fn redistributable(self) -> bool {
82        !matches!(self, Licence::AppleSdk | Licence::MicrosoftSdk)
83    }
84
85    /// The spelling in a manifest file.
86    #[must_use]
87    pub const fn as_str(self) -> &'static str {
88        match self {
89            Licence::Mit => "mit",
90            Licence::Lgpl => "lgpl",
91            Licence::Bsd => "bsd",
92            Licence::MingwPermissive => "mingw-permissive",
93            Licence::Apache2 => "apache-2.0",
94            Licence::AppleSdk => "apple-sdk",
95            Licence::MicrosoftSdk => "microsoft-sdk",
96        }
97    }
98}
99
100impl fmt::Display for Licence {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(self.as_str())
103    }
104}
105
106impl FromStr for Licence {
107    type Err = ManifestError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        match s {
111            "mit" => Ok(Licence::Mit),
112            "lgpl" => Ok(Licence::Lgpl),
113            "bsd" => Ok(Licence::Bsd),
114            "mingw-permissive" => Ok(Licence::MingwPermissive),
115            "apache-2.0" => Ok(Licence::Apache2),
116            "apple-sdk" => Ok(Licence::AppleSdk),
117            "microsoft-sdk" => Ok(Licence::MicrosoftSdk),
118            other => Err(ManifestError::UnknownLicence(other.to_string())),
119        }
120    }
121}
122
123/// One file in a sysroot, and everything that has to be true of it.
124#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
125pub struct Input {
126    /// Where it sits, relative to the sysroot root. Relative because an absolute path is a fact
127    /// about the machine that built it, and two hosts have different ones.
128    pub path: String,
129    /// What it came out of, named so that the same manifest can be produced again. A release name
130    /// and version rather than a URL, because a URL moves and a release does not.
131    pub source: String,
132    /// The hash of the file, lowercase hex.
133    pub sha256: String,
134    /// What it may be done with.
135    pub licence: Licence,
136}
137
138/// The record of one produced sysroot.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct Manifest {
141    target: TargetTuple,
142    inputs: Vec<Input>,
143}
144
145/// What went wrong reading a manifest.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum ManifestError {
148    /// The first line was not the one this format starts with.
149    NotAManifest,
150    /// The version in the header is one this build does not read.
151    UnknownVersion(String),
152    /// The second line did not name a target, or named one that does not parse.
153    BadTarget(String),
154    /// A line did not have the four fields an input has.
155    BadInput {
156        /// Which line, counting from one.
157        line: usize,
158        /// How many tab separated fields it had.
159        fields: usize,
160    },
161    /// A hash that is not sixty four lowercase hex characters.
162    BadHash {
163        /// Which line, counting from one.
164        line: usize,
165        /// What was there instead.
166        found: String,
167    },
168    /// A licence spelling nothing here knows.
169    UnknownLicence(String),
170}
171
172impl fmt::Display for ManifestError {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        match self {
175            ManifestError::NotAManifest => write!(f, "this does not start like a sysroot manifest"),
176            ManifestError::UnknownVersion(v) => {
177                write!(f, "manifest format version {v}, which this build does not read")
178            }
179            ManifestError::BadTarget(t) => write!(f, "`{t}` is not a target this understands"),
180            ManifestError::BadInput { line, fields } => {
181                write!(f, "line {line} has {fields} fields where an input has four")
182            }
183            ManifestError::BadHash { line, found } => {
184                write!(f, "line {line} has `{found}` where a sha256 belongs")
185            }
186            ManifestError::UnknownLicence(l) => write!(f, "`{l}` is not a licence this knows"),
187        }
188    }
189}
190
191impl std::error::Error for ManifestError {}
192
193/// The first line of every manifest, which is also how one is recognized.
194const HEADER: &str = "rucc sysroot manifest 1";
195
196impl Manifest {
197    /// An empty manifest for this target.
198    #[must_use]
199    pub const fn new(target: TargetTuple) -> Self {
200        Manifest { target, inputs: Vec::new() }
201    }
202
203    /// The target this sysroot is for.
204    #[must_use]
205    pub const fn target(&self) -> TargetTuple {
206        self.target
207    }
208
209    /// Every input, in the order they were added.
210    #[must_use]
211    pub fn inputs(&self) -> &[Input] {
212        &self.inputs
213    }
214
215    /// Record one input.
216    pub fn push(&mut self, input: Input) {
217        self.inputs.push(input);
218    }
219
220    /// Whether an artifact containing this whole sysroot can be published.
221    ///
222    /// One input under a licence that says no makes the answer no, which is the only reading of a
223    /// licence wall that is worth anything.
224    #[must_use]
225    pub fn redistributable(&self) -> bool {
226        self.inputs.iter().all(|input| input.licence.redistributable())
227    }
228
229    /// Every distinct source in the manifest, sorted.
230    ///
231    /// What a person asks first when two manifests differ, and what a licence notice is generated
232    /// from.
233    #[must_use]
234    pub fn sources(&self) -> Vec<&str> {
235        let mut sources: Vec<&str> =
236            self.inputs.iter().map(|input| input.source.as_str()).collect();
237        sources.sort_unstable();
238        sources.dedup();
239        sources
240    }
241
242    /// The manifest as text, sorted by path.
243    ///
244    /// The sort is what makes two runs comparable. A directory walk returns files in whatever order
245    /// the filesystem keeps them, which differs between ext4 and APFS and sometimes between two
246    /// runs on one of them, and a manifest that carried that order would report a difference
247    /// between two identical sysroots.
248    #[must_use]
249    pub fn render(&self) -> String {
250        let mut sorted = self.inputs.clone();
251        sorted.sort();
252
253        let mut text = String::new();
254        text.push_str(HEADER);
255        text.push('\n');
256        text.push_str("target\t");
257        text.push_str(&self.target.to_canonical_string());
258        text.push('\n');
259        for input in &sorted {
260            text.push_str(&input.path);
261            text.push('\t');
262            text.push_str(&input.source);
263            text.push('\t');
264            text.push_str(&input.sha256);
265            text.push('\t');
266            text.push_str(input.licence.as_str());
267            text.push('\n');
268        }
269        text
270    }
271
272    /// Read a manifest back.
273    ///
274    /// # Errors
275    ///
276    /// Returns which line was wrong and what was wrong with it. A manifest that fails to parse is
277    /// a cache entry somebody has to decide about, and "invalid manifest" is not enough to decide
278    /// with.
279    pub fn parse(text: &str) -> Result<Self, ManifestError> {
280        let mut lines = text.lines().enumerate();
281
282        let (_, first) = lines.next().ok_or(ManifestError::NotAManifest)?;
283        if first != HEADER {
284            let Some(version) = first.strip_prefix("rucc sysroot manifest ") else {
285                return Err(ManifestError::NotAManifest);
286            };
287            return Err(ManifestError::UnknownVersion(version.to_string()));
288        }
289
290        let (_, second) = lines.next().ok_or(ManifestError::NotAManifest)?;
291        let spelling = second
292            .strip_prefix("target\t")
293            .ok_or_else(|| ManifestError::BadTarget(second.into()))?;
294        let target = TargetTuple::from_str(spelling)
295            .map_err(|_| ManifestError::BadTarget(spelling.to_string()))?;
296
297        let mut manifest = Manifest::new(target);
298        for (index, line) in lines {
299            if line.is_empty() {
300                continue;
301            }
302            let number = index + 1;
303            let fields: Vec<&str> = line.split('\t').collect();
304            let [path, source, sha256, licence] = fields.as_slice() else {
305                return Err(ManifestError::BadInput { line: number, fields: fields.len() });
306            };
307            if !is_sha256(sha256) {
308                return Err(ManifestError::BadHash { line: number, found: (*sha256).to_string() });
309            }
310            manifest.push(Input {
311                path: (*path).to_string(),
312                source: (*source).to_string(),
313                sha256: (*sha256).to_string(),
314                licence: licence.parse()?,
315            });
316        }
317        Ok(manifest)
318    }
319}
320
321/// Whether this is sixty four lowercase hex characters.
322///
323/// Checked on the way in rather than assumed, because a manifest with a truncated hash in it is a
324/// manifest that verifies nothing while looking like it does.
325fn is_sha256(s: &str) -> bool {
326    s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
327}