Skip to main content

rucc_driver/
install.rs

1//! Checking a sysroot artifact and putting it in the cache.
2//!
3//! Design: `spec/cross-compile/13-distribution.md` section 13.8, which divides a fetch in two. A
4//! downloader the machine already has moves the bytes, and everything that decides whether the
5//! result is correct is here. That division is why this file has no URL in it and runs no network
6//! code: what it is handed is a file on disk and the hash that file is supposed to have.
7//!
8//! # The order, which is the whole of the argument
9//!
10//! The hash of the archive is checked before anything is unpacked. So what `tar` is pointed at is
11//! always a file we have already identified, and an unpacker's behaviour on a file somebody else
12//! chose is not a question this has to have an answer to.
13//!
14//! Then the files that came out are checked against the manifest the producer put in the archive,
15//! in both directions. Every line has to name a file with the sha256 it recorded, and every file
16//! has to be named by a line. The second direction is the one that is easy to leave out and it is
17//! the one that matters: [`rucc_sysroot::Manifest::digest`] is a claim about what is under a
18//! directory, and a file nobody recorded makes it a claim about less than what is there.
19//!
20//! The manifest is the producer's rather than ours because of what is in it. An input carries a
21//! source, a URL and a licence, and a walk of a directory tree knows none of the three. The
22//! producer in `tamnd/rucc-cross` knows all of them, so it writes the record and this checks it
23//! against the bytes, which is the same shape as section 13.6's rule about a generated artifact
24//! being checked against its generator.
25//!
26//! Only then is the result renamed into place, which is section 13.2's concurrency rule. The
27//! staging directory is inside the cache so that the rename is a rename and not a copy, because
28//! the two paths are on one filesystem by construction.
29
30use std::fs;
31use std::io;
32use std::path::{Component, Path, PathBuf};
33use std::process::Command;
34use std::time::{SystemTime, UNIX_EPOCH};
35
36use rucc_sysroot::{Kernel, KernelManifest, Manifest, Sysroot, sha256};
37use rucc_tuple::TargetTuple;
38
39use crate::{CliError, err};
40
41/// What was at the destination before an install put something there.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Before {
44    /// Nothing, which is the first install of this target on this machine.
45    Nothing,
46    /// A sysroot whose manifest has the same digest, so the install was not needed and nothing was
47    /// moved. Two rucc versions share a directory for the reason section 13.2 gives, and a second
48    /// fetch of the same artifact is the ordinary way this happens.
49    TheSame,
50    /// A sysroot with a different digest, which was replaced. The string is the digest that was
51    /// there, so that whatever reports the install can say what it stood on.
52    Different(String),
53}
54
55/// What an install left behind.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Installed {
58    /// Where it is, which is `sysroots/<tuple>` under the cache, or `kernel-headers` for the tree
59    /// every Linux target shares.
60    pub root: PathBuf,
61    /// The digest of its manifest, which is what `rucc -print-sysroot-digest` prints for it.
62    pub digest: String,
63    /// How many files the manifest records, all of which were checked.
64    pub files: usize,
65    /// What the destination held before this.
66    pub before: Before,
67}
68
69/// Check a file against the hash it is supposed to have.
70///
71/// The hash comes from the rucc release rather than from the artifact or from the server that
72/// served it, which is the only arrangement where a check means anything. Section 13.2 says a
73/// mismatch is a hard failure with no override flag, so there is no argument here that could turn
74/// one into a warning.
75///
76/// # Errors
77///
78/// A file that cannot be read, and a hash that does not match. The message names both hashes,
79/// because the first thing anybody does with a mismatch is ask whether they downloaded the wrong
80/// thing or the right thing badly, and the two look different.
81pub fn verify(archive: &Path, expected: &str) -> Result<(), CliError> {
82    let bytes = fs::read(archive).map_err(|why| err(format!("{}: {why}", archive.display())))?;
83    let found = sha256::hex(&bytes);
84    if found == expected {
85        return Ok(());
86    }
87    Err(err(format!(
88        "{} has sha256 {found} where this release pins {expected}, so it is not the artifact this \
89         build knows about",
90        archive.display()
91    )))
92}
93
94/// Check an artifact and install it as the sysroot for a target.
95///
96/// The steps are ยง13.8's, in order: the hash, the unpack, the manifest against the tree and the
97/// tree against the manifest, the rename. Nothing is written outside the cache and nothing outside
98/// the cache is read except the archive.
99///
100/// # Errors
101///
102/// Every step, and each message says which step. An artifact for the wrong target, a missing
103/// manifest, a file whose hash disagrees with the record, a file no line names, and anything the
104/// filesystem or `tar` refuses. A failure leaves the destination as it was: the work happens in a
105/// staging directory and the last thing that happens is the rename.
106pub fn install(
107    archive: &Path,
108    expected: &str,
109    target: TargetTuple,
110    cache: &Path,
111) -> Result<Installed, CliError> {
112    verify(archive, expected)?;
113    staged(cache, &target.to_canonical_string(), |staging| {
114        install_staged(archive, target, cache, staging)
115    })
116}
117
118/// Check the kernel header tree's artifact and install it where every Linux target reads it.
119///
120/// The same four steps as [`install`], against the tree's own record rather than a sysroot's. What
121/// is not checked is that the tree is the release a sysroot's `kernel` line names, for the reason
122/// [`rucc_sysroot::Kernel`] gives: the two are produced by two commands and can be paired either way,
123/// and the records are what make a stale pairing visible.
124///
125/// # Errors
126///
127/// The same as [`install`]'s, with a record that is not a kernel tree's in place of a sysroot for
128/// the wrong target.
129pub fn install_kernel(archive: &Path, expected: &str, cache: &Path) -> Result<Installed, CliError> {
130    verify(archive, expected)?;
131    staged(cache, "kernel-headers", |staging| install_kernel_staged(archive, cache, staging))
132}
133
134/// Run an install in a staging directory of its own, and take the directory away if it fails.
135///
136/// Every exit has to take the staging directory with it, including the ones that are somebody
137/// else's fault, or a machine that fetches a broken artifact twice a day fills its cache with half
138/// unpacked trees.
139fn staged(
140    cache: &Path,
141    name: &str,
142    work: impl FnOnce(&Path) -> Result<Installed, CliError>,
143) -> Result<Installed, CliError> {
144    let staging = staging_dir(cache, name);
145    fs::create_dir_all(&staging).map_err(|why| err(format!("{}: {why}", staging.display())))?;
146    let outcome = work(&staging);
147    if outcome.is_err() {
148        let _ = fs::remove_dir_all(&staging);
149    }
150    outcome
151}
152
153/// The record at the top of an unpacked archive, as text.
154fn record(archive: &Path, staging: &Path) -> Result<String, CliError> {
155    let record = staging.join("manifest");
156    fs::read_to_string(&record).map_err(|why| {
157        if why.kind() == io::ErrorKind::NotFound {
158            err(format!(
159                "{} has no manifest in it, so there is nothing to check its files against",
160                archive.display()
161            ))
162        } else {
163            err(format!("{}: {why}", record.display()))
164        }
165    })
166}
167
168/// The install, with the staging directory already made and cleaned up by the caller.
169fn install_staged(
170    archive: &Path,
171    target: TargetTuple,
172    cache: &Path,
173    staging: &Path,
174) -> Result<Installed, CliError> {
175    unpack(archive, staging)?;
176    let text = record(archive, staging)?;
177    let manifest =
178        Manifest::parse(&text).map_err(|why| err(format!("{}: {why}", archive.display())))?;
179
180    // A pinned glibc release installs the archive of its tuple without the release, which is the
181    // one archive every release of that target shares.
182    let shared = rucc_sysroot::glibc_base(target) == Some(manifest.target());
183    if manifest.target() != target && !shared {
184        return Err(err(format!(
185            "{} is a sysroot for {}, which is not {}",
186            archive.display(),
187            manifest.target().to_canonical_string(),
188            target.to_canonical_string()
189        )));
190    }
191
192    let recorded: Vec<(&str, &str)> = manifest
193        .inputs()
194        .iter()
195        .map(|input| (input.path.as_str(), input.sha256.as_str()))
196        .collect();
197    check(staging, &recorded).map_err(|why| err(format!("{}: {why}", archive.display())))?;
198
199    let digest = manifest.digest();
200    let root = Sysroot::in_cache(cache, target).root().to_path_buf();
201    let before = swap(staging, &root, &digest, existing(&root))?;
202    Ok(Installed { root, digest, files: recorded.len(), before })
203}
204
205/// The kernel tree's install, with the staging directory already made and cleaned up by the
206/// caller.
207fn install_kernel_staged(
208    archive: &Path,
209    cache: &Path,
210    staging: &Path,
211) -> Result<Installed, CliError> {
212    unpack(archive, staging)?;
213    let text = record(archive, staging)?;
214    let manifest =
215        KernelManifest::parse(&text).map_err(|why| err(format!("{}: {why}", archive.display())))?;
216
217    let recorded: Vec<(&str, &str)> =
218        manifest.files().iter().map(|file| (file.path.as_str(), file.sha256.as_str())).collect();
219    check(staging, &recorded).map_err(|why| err(format!("{}: {why}", archive.display())))?;
220
221    let digest = manifest.digest();
222    let root = Kernel::in_cache(cache);
223    let before = swap(staging, &root, &digest, existing_kernel(&root))?;
224    Ok(Installed { root, digest, files: recorded.len(), before })
225}
226
227/// Where this install does its work.
228///
229/// Inside the cache, because the rename at the end is only atomic if the two paths are on one
230/// filesystem, and a name nothing else will pick, because two builds fetching the same target at
231/// the same time is the ordinary case rather than the unlucky one. The process id is not enough on
232/// its own: one process can install the same target twice.
233fn staging_dir(cache: &Path, name: &str) -> PathBuf {
234    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
235    let unique = format!("{name}-{}-{}", std::process::id(), now.as_nanos());
236    cache.join("staging").join(unique)
237}
238
239/// Unpack a verified archive with the platform's own `tar`.
240///
241/// Section 13.8's decision keeps an archive reader out of the compiler for the same reason it keeps
242/// a TLS stack out, and `tar` is on every host in the support table, including Windows since 1803.
243/// The members are unpacked as they are, with no component stripped, because the staging directory
244/// is one we made for this and a single directory inside the archive would only be a name to
245/// disagree about.
246fn unpack(archive: &Path, into: &Path) -> Result<(), CliError> {
247    let output =
248        Command::new("tar").arg("-xzf").arg(archive).arg("-C").arg(into).output().map_err(
249            |why| err(format!("could not run `tar`, which is how an artifact is unpacked: {why}")),
250        )?;
251    if output.status.success() {
252        return Ok(());
253    }
254    let said = String::from_utf8_lossy(&output.stderr);
255    let said = said.trim();
256    let detail = if said.is_empty() { String::new() } else { format!(": {said}") };
257    Err(err(format!("`tar` could not unpack {}{detail}", archive.display())))
258}
259
260/// Check a tree against a record and the record against the tree.
261///
262/// The record is the path and the sha256 of every file, which is the part a sysroot's manifest and
263/// the kernel tree's have in common.
264///
265/// The errors are collected rather than returned one at a time. An artifact that fails this is
266/// either the wrong artifact or a broken producer, and which of the two it is shows in how many
267/// files disagree, so a report that stopped at the first one would hide the thing that tells them
268/// apart.
269fn check(tree: &Path, files: &[(&str, &str)]) -> Result<(), String> {
270    let mut problems: Vec<String> = Vec::new();
271    let mut recorded: Vec<&str> = Vec::new();
272
273    for &(path, sha256) in files {
274        recorded.push(path);
275        let at = match relative(tree, path) {
276            Ok(at) => at,
277            Err(why) => {
278                problems.push(why);
279                continue;
280            }
281        };
282        match fs::read(&at) {
283            Ok(bytes) => {
284                let found = sha256::hex(&bytes);
285                if found != sha256 {
286                    problems
287                        .push(format!("{path} has sha256 {found} where the record says {sha256}"));
288                }
289            }
290            Err(why) if why.kind() == io::ErrorKind::NotFound => {
291                problems.push(format!("{path} is in the record and not in the archive"));
292            }
293            Err(why) => problems.push(format!("{path}: {why}")),
294        }
295    }
296
297    let mut found = Vec::new();
298    walk(tree, String::new(), &mut found).map_err(|why| format!("{}: {why}", tree.display()))?;
299    recorded.sort_unstable();
300    for path in &found {
301        // The manifest is the record and is not a line in itself, which is the one file in a
302        // sysroot that is allowed to be there without being recorded.
303        if path == "manifest" {
304            continue;
305        }
306        if recorded.binary_search(&path.as_str()).is_err() {
307            problems.push(format!("{path} is in the archive and not in the record"));
308        }
309    }
310
311    if problems.is_empty() {
312        return Ok(());
313    }
314    problems.sort();
315    let first = &problems[0];
316    if problems.len() == 1 {
317        return Err(format!("the archive does not match its own manifest: {first}"));
318    }
319    Err(format!(
320        "the archive does not match its own manifest: {first}, and {} more files disagree",
321        problems.len() - 1
322    ))
323}
324
325/// The path a recorded input names, refusing one that is not under the tree.
326///
327/// A manifest is checked before it is trusted and a path is part of a manifest. `..` in a recorded
328/// path, or a path that starts at the root of the filesystem, would write the check against a file
329/// the archive never carried, so neither is a path this reads.
330fn relative(tree: &Path, path: &str) -> Result<PathBuf, String> {
331    let candidate = Path::new(path);
332    let ordinary = candidate.components().all(|part| matches!(part, Component::Normal(_)));
333    if !ordinary {
334        return Err(format!("{path} is not a path inside a sysroot"));
335    }
336    Ok(tree.join(candidate))
337}
338
339/// Every file under a directory, as paths relative to it with `/` between the parts.
340///
341/// `/` whatever the host separator is, because that is the spelling a manifest uses and the
342/// comparison is against a manifest. A symlink is a leaf rather than something to follow: following
343/// one would count a file twice, or walk forever, and what a recorded hash is about is the bytes a
344/// compiler reads through that name, which `fs::read` gets by following it once.
345fn walk(dir: &Path, prefix: String, out: &mut Vec<String>) -> io::Result<()> {
346    for entry in fs::read_dir(dir)? {
347        let entry = entry?;
348        let name = entry.file_name().to_string_lossy().into_owned();
349        let path = if prefix.is_empty() { name } else { format!("{prefix}/{name}") };
350        if entry.file_type()?.is_dir() {
351            walk(&entry.path(), path, out)?;
352        } else {
353            out.push(path);
354        }
355    }
356    Ok(())
357}
358
359/// Put the staged tree where a compiler will look for it.
360///
361/// Section 13.2 asks for an atomic rename and never an in place mutation, which is a rule about
362/// what a parallel build sees. A compile that is reading the old sysroot while this happens keeps
363/// reading the files it has open, and one that starts during the swap finds either the old tree or
364/// the new one.
365///
366/// An existing tree with the same digest is left alone. That is not an optimization: a second fetch
367/// of the same artifact is the ordinary case, and replacing a directory that is already correct
368/// would move files under a build for no reason at all.
369fn swap(
370    staging: &Path,
371    root: &Path,
372    digest: &str,
373    there: Option<String>,
374) -> Result<Before, CliError> {
375    let before = match there {
376        Some(found) if found == digest => {
377            let _ = fs::remove_dir_all(staging);
378            return Ok(Before::TheSame);
379        }
380        Some(found) => Before::Different(found),
381        None => Before::Nothing,
382    };
383
384    if let Some(parent) = root.parent() {
385        fs::create_dir_all(parent).map_err(|why| err(format!("{}: {why}", parent.display())))?;
386    }
387
388    // The old tree is moved aside rather than deleted first. Deleting it first would leave the path
389    // missing for as long as the delete takes, which on a sysroot is thousands of files, and the
390    // window this way is one rename wide.
391    let aside = root.with_extension(format!("old.{}", std::process::id()));
392    if before != Before::Nothing {
393        let _ = fs::remove_dir_all(&aside);
394        fs::rename(root, &aside)
395            .map_err(|why| err(format!("could not move {} aside: {why}", root.display())))?;
396    }
397    let renamed = fs::rename(staging, root);
398    if let Err(why) = renamed {
399        // Put back what was there. A failed install that also took the working sysroot away would
400        // be worse than the failure it started as.
401        if before != Before::Nothing {
402            let _ = fs::rename(&aside, root);
403        }
404        return Err(err(format!("could not put {} in place: {why}", root.display())));
405    }
406    if before != Before::Nothing {
407        let _ = fs::remove_dir_all(&aside);
408    }
409    Ok(before)
410}
411
412/// The digest of the sysroot that is already at this path, if there is one with a manifest.
413///
414/// A directory with no manifest in it answers [`None`] and is replaced like anything else. It is
415/// either a tree somebody assembled by hand, which `--sysroot` is the flag for and the cache is not
416/// the place for, or the wreckage of an install from before this code existed.
417fn existing(root: &Path) -> Option<String> {
418    let text = fs::read_to_string(root.join("manifest")).ok()?;
419    Manifest::parse(&text).ok().map(|manifest| manifest.digest())
420}
421
422/// The same question about the kernel tree, whose record is in its own format.
423fn existing_kernel(root: &Path) -> Option<String> {
424    let text = fs::read_to_string(root.join("manifest")).ok()?;
425    KernelManifest::parse(&text).ok().map(|manifest| manifest.digest())
426}
427
428#[cfg(test)]
429mod tests {
430    use super::{Before, Installed, check, install, install_kernel, relative, verify, walk};
431    use rucc_sysroot::{Input, KernelFile, KernelManifest, Licence, Manifest, Provenance, sha256};
432    use rucc_tuple::{TargetTuple, Version};
433    use std::path::{Path, PathBuf};
434    use std::process::Command;
435
436    /// A directory that goes away with the test, and the files in it.
437    struct Tree(PathBuf);
438
439    impl Drop for Tree {
440        fn drop(&mut self) {
441            let _ = std::fs::remove_dir_all(&self.0);
442        }
443    }
444
445    impl Tree {
446        fn new(name: &str) -> Tree {
447            let dir =
448                std::env::temp_dir().join(format!("rucc-install-{}-{name}", std::process::id()));
449            let _ = std::fs::remove_dir_all(&dir);
450            std::fs::create_dir_all(&dir).expect("a temporary directory should be writable");
451            Tree(dir)
452        }
453
454        fn write(&self, path: &str, text: &str) {
455            let at = self.0.join(path);
456            if let Some(parent) = at.parent() {
457                std::fs::create_dir_all(parent).expect("a subdirectory should be creatable");
458            }
459            std::fs::write(&at, text).expect("a temporary file should be writable");
460        }
461    }
462
463    /// The target every test here uses, and a musl one so that nothing depends on the host.
464    fn target() -> TargetTuple {
465        "x86_64-linux-musl".parse().expect("a tuple the table knows")
466    }
467
468    /// A manifest for these files, with their real hashes in it.
469    fn manifest_for(files: &[(&str, &str)]) -> Manifest {
470        let mut manifest = Manifest::new(target());
471        for (path, text) in files {
472            manifest.push(Input {
473                path: (*path).to_owned(),
474                source: "musl-1.2.5".to_owned(),
475                url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_owned(),
476                sha256: sha256::hex(text.as_bytes()),
477                licence: Licence::Mit,
478                provenance: Provenance::Bundled,
479            });
480        }
481        manifest
482    }
483
484    /// An artifact holding these files and a manifest that describes them, and its sha256.
485    ///
486    /// Built with the same `tar` the install unpacks with, which is the point: a test that wrote its
487    /// own archive format would be testing a reader nobody uses.
488    fn artifact(tree: &Tree, files: &[(&str, &str)], manifest: &Manifest) -> (PathBuf, String) {
489        artifact_with(tree, files, &manifest.render())
490    }
491
492    /// The same, with the record already rendered, which is how the kernel tree's is passed.
493    fn artifact_with(tree: &Tree, files: &[(&str, &str)], record: &str) -> (PathBuf, String) {
494        let staged = tree.0.join("staged");
495        std::fs::create_dir_all(&staged).expect("a staging directory should be creatable");
496        for (path, text) in files {
497            let at = staged.join(path);
498            if let Some(parent) = at.parent() {
499                std::fs::create_dir_all(parent).expect("a subdirectory should be creatable");
500            }
501            std::fs::write(&at, text).expect("a file should be writable");
502        }
503        std::fs::write(staged.join("manifest"), record).expect("the manifest should be writable");
504
505        let archive = tree.0.join("artifact.tar.gz");
506        let status = Command::new("tar")
507            .arg("-czf")
508            .arg(&archive)
509            .arg("-C")
510            .arg(&staged)
511            .arg(".")
512            .status()
513            .expect("tar should be on a machine that runs these tests");
514        assert!(status.success(), "tar should be able to write an archive");
515        std::fs::remove_dir_all(&staged).expect("the staged tree should be removable");
516
517        let bytes = std::fs::read(&archive).expect("the archive should be readable");
518        let hash = sha256::hex(&bytes);
519        (archive, hash)
520    }
521
522    const FILES: &[(&str, &str)] =
523        &[("include/stdio.h", "int puts(const char *);\n"), ("lib/libc.so", "not really\n")];
524
525    #[test]
526    fn an_artifact_that_matches_its_record_is_installed() {
527        let tree = Tree::new("good");
528        let manifest = manifest_for(FILES);
529        let (archive, hash) = artifact(&tree, FILES, &manifest);
530        let cache = tree.0.join("cache");
531
532        let done = install(&archive, &hash, target(), &cache).expect("this one should install");
533        assert_eq!(done.before, Before::Nothing);
534        assert_eq!(done.files, 2);
535        assert_eq!(done.digest, manifest.digest());
536        assert_eq!(done.root, cache.join("sysroots").join("x86_64-linux-musl"));
537
538        // The files are where a compiler looks for them, and the record is beside them, which is
539        // what `-print-sysroot-provenance` reads.
540        assert!(done.root.join("include/stdio.h").is_file());
541        assert_eq!(
542            std::fs::read_to_string(done.root.join("manifest")).expect("a manifest"),
543            manifest.render()
544        );
545        // And nothing is left in the staging area, which is a cache that would otherwise grow a
546        // copy of every sysroot it ever installed.
547        let left: Vec<PathBuf> = std::fs::read_dir(cache.join("staging"))
548            .expect("the staging directory")
549            .map(|entry| entry.expect("an entry").path())
550            .collect();
551        assert_eq!(left, Vec::<PathBuf>::new(), "a staging tree was left behind");
552    }
553
554    /// `--fetch` from end to end, with the artifact already where a downloader would have put it.
555    ///
556    /// No downloader runs, and that is the case rather than a way around one: section 13.8 says a
557    /// machine with none of the three is told the path to put a file at and a second run carries on
558    /// from the check, so this is that machine. What it tests is that the table, the check, the
559    /// unpack and the rename are wired to each other, which is the one thing neither module's own
560    /// tests can see.
561    ///
562    /// It is here rather than beside the parser because the fixtures for an artifact are here, and a
563    /// second copy of them next door is the thing most likely to drift away from this one.
564    #[test]
565    fn a_fetch_of_an_artifact_that_is_already_on_the_machine_installs_it() {
566        let tree = Tree::new("fetch");
567        let manifest = manifest_for(FILES);
568        let (built, hash) = artifact(&tree, FILES, &manifest);
569        let cache = tree.0.join("cache");
570
571        // A table row names an artifact by a URL and a hash, and both are static strings there
572        // because a release is what writes them. A test computes the hash as it goes, so it leaks
573        // two strings into a process that is about to end.
574        let pinned = rucc_sysroot::Pinned {
575            tuple: "x86_64-linux-musl",
576            url: "https://example.invalid/rucc-sysroot-x86_64-linux-musl.tar.gz",
577            sha256: String::leak(hash),
578        };
579        // Where a downloader would have written it, which is what the fetch looks at first.
580        let at = pinned.archive_in(&cache);
581        std::fs::create_dir_all(at.parent().expect("a parent")).expect("a downloads directory");
582        std::fs::copy(&built, &at).expect("the artifact should be placeable");
583
584        // And the kernel tree, which a Linux target is fetched with, placed the same way.
585        let kernel_manifest = kernel_manifest_for(KERNEL_FILES);
586        let (built, hash) = artifact_with(&tree, KERNEL_FILES, &kernel_manifest.render());
587        let kernel = rucc_sysroot::Pinned {
588            tuple: "kernel-headers",
589            url: "https://example.invalid/rucc-kernel-headers.tar.gz",
590            sha256: String::leak(hash),
591        };
592        let at = kernel.archive_in(&cache);
593        std::fs::create_dir_all(at.parent().expect("a parent")).expect("a downloads directory");
594        std::fs::copy(&built, &at).expect("the artifact should be placeable");
595
596        assert_eq!(crate::fetch_sysroot(&pinned, Some(&kernel), target(), &cache), 0);
597        let root = cache.join("sysroots").join("x86_64-linux-musl");
598        assert!(root.join("include/stdio.h").is_file());
599        assert_eq!(
600            std::fs::read_to_string(root.join("manifest")).expect("a manifest"),
601            manifest.render()
602        );
603        assert!(cache.join("kernel-headers/x86/asm/unistd.h").is_file());
604        // And again, which is the ordinary second run: the archives are still there, they still
605        // match, and the trees they would install are the trees that are already installed.
606        assert_eq!(crate::fetch_sysroot(&pinned, Some(&kernel), target(), &cache), 0);
607        // A target with no kernel tree is one artifact, and the fetch does not go looking for one.
608        assert_eq!(crate::fetch_sysroot(&pinned, None, target(), &cache), 0);
609    }
610
611    #[test]
612    fn the_same_artifact_twice_does_not_move_anything() {
613        let tree = Tree::new("again");
614        let manifest = manifest_for(FILES);
615        let (archive, hash) = artifact(&tree, FILES, &manifest);
616        let cache = tree.0.join("cache");
617
618        let first = install(&archive, &hash, target(), &cache).expect("the first install");
619        let second = install(&archive, &hash, target(), &cache).expect("the second install");
620        assert_eq!(second.before, Before::TheSame);
621        assert_eq!(second.root, first.root);
622        assert_eq!(second.digest, first.digest);
623    }
624
625    #[test]
626    fn a_hash_that_does_not_match_is_refused_before_anything_is_unpacked() {
627        let tree = Tree::new("hash");
628        let manifest = manifest_for(FILES);
629        let (archive, _) = artifact(&tree, FILES, &manifest);
630        let cache = tree.0.join("cache");
631
632        let wrong = "0".repeat(64);
633        let why =
634            install(&archive, &wrong, target(), &cache).expect_err("this is not the artifact");
635        assert!(why.message.contains("where this release pins"), "{}", why.message);
636        // Nothing was unpacked, which is the order section 13.8 asks for: the cache does not even
637        // have the directories in it.
638        assert!(!cache.exists(), "a refused artifact should not have reached the cache");
639    }
640
641    #[test]
642    fn an_artifact_for_another_target_is_refused() {
643        let tree = Tree::new("target");
644        let mut manifest = Manifest::new("aarch64-linux-musl".parse().expect("a tuple"));
645        for (path, text) in FILES {
646            manifest.push(Input {
647                path: (*path).to_owned(),
648                source: "musl-1.2.5".to_owned(),
649                url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_owned(),
650                sha256: sha256::hex(text.as_bytes()),
651                licence: Licence::Mit,
652                provenance: Provenance::Bundled,
653            });
654        }
655        let (archive, hash) = artifact(&tree, FILES, &manifest);
656        let cache = tree.0.join("cache");
657
658        let why = install(&archive, &hash, target(), &cache).expect_err("the wrong target");
659        assert!(why.message.contains("aarch64-linux-musl"), "{}", why.message);
660        assert!(why.message.contains("x86_64-linux-musl"), "{}", why.message);
661        assert!(!cache.join("sysroots").exists(), "nothing should have been installed");
662    }
663
664    /// A pinned glibc release is served by the archive of its tuple without the release, and it is
665    /// installed under the pinned tuple, which is the directory the compiler will look in.
666    #[test]
667    fn a_pinned_glibc_release_installs_the_archive_of_its_tuple() {
668        let tree = Tree::new("pinned");
669        let mut manifest = Manifest::new("x86_64-linux-gnu".parse().expect("a tuple"));
670        for (path, text) in FILES {
671            manifest.push(Input {
672                path: (*path).to_owned(),
673                source: "glibc-merged".to_owned(),
674                url: "https://ftp.gnu.org/gnu/glibc/glibc-2.44.tar.xz".to_owned(),
675                sha256: sha256::hex(text.as_bytes()),
676                licence: Licence::Lgpl,
677                provenance: Provenance::Generated,
678            });
679        }
680        let (archive, hash) = artifact(&tree, FILES, &manifest);
681        let cache = tree.0.join("cache");
682
683        let pinned = "x86_64-linux-gnu.2.28".parse().expect("a tuple");
684        let done = install(&archive, &hash, pinned, &cache).expect("the shared archive");
685        assert_eq!(done.root, cache.join("sysroots").join("x86_64-linux-gnu.2.28"));
686
687        // A release of some other tuple is still some other tuple.
688        let other = "aarch64-linux-gnu.2.28".parse().expect("a tuple");
689        let why = install(&archive, &hash, other, &cache).expect_err("the wrong target");
690        assert!(why.message.contains("aarch64-linux-gnu.2.28"), "{}", why.message);
691    }
692
693    #[test]
694    fn an_archive_with_no_record_in_it_is_refused() {
695        let tree = Tree::new("bare");
696        let staged = tree.0.join("staged");
697        std::fs::create_dir_all(&staged).expect("a directory");
698        std::fs::write(staged.join("include.h"), "int x;\n").expect("a file");
699        let archive = tree.0.join("bare.tar.gz");
700        let status = Command::new("tar")
701            .arg("-czf")
702            .arg(&archive)
703            .arg("-C")
704            .arg(&staged)
705            .arg(".")
706            .status()
707            .expect("tar should run");
708        assert!(status.success());
709        let hash = sha256::hex(&std::fs::read(&archive).expect("readable"));
710        let cache = tree.0.join("cache");
711
712        let why = install(&archive, &hash, target(), &cache).expect_err("no manifest");
713        assert!(why.message.contains("has no manifest in it"), "{}", why.message);
714    }
715
716    #[test]
717    fn a_file_the_record_does_not_name_is_refused() {
718        // The direction that is easy to leave out. A digest is a claim about what is under a
719        // directory, so an unrecorded file makes it a claim about less than what is there.
720        let tree = Tree::new("extra");
721        let manifest = manifest_for(FILES);
722        let mut with_extra: Vec<(&str, &str)> = FILES.to_vec();
723        with_extra.push(("lib/surprise.o", "nobody wrote this down\n"));
724        let (archive, hash) = artifact(&tree, &with_extra, &manifest);
725        let cache = tree.0.join("cache");
726
727        let why = install(&archive, &hash, target(), &cache).expect_err("an unrecorded file");
728        assert!(why.message.contains("lib/surprise.o"), "{}", why.message);
729        assert!(why.message.contains("not in the record"), "{}", why.message);
730    }
731
732    #[test]
733    fn a_file_whose_bytes_changed_is_refused_and_so_is_one_that_is_missing() {
734        let tree = Tree::new("bytes");
735        // The manifest describes what the files were supposed to be and the archive holds something
736        // else, which is what a producer with a bug in it looks like from here.
737        let manifest = manifest_for(&[("include/stdio.h", "what the record says\n")]);
738        let (archive, hash) = artifact(&tree, &[("include/stdio.h", "what is there\n")], &manifest);
739        let cache = tree.0.join("cache");
740        let why = install(&archive, &hash, target(), &cache).expect_err("changed bytes");
741        assert!(why.message.contains("include/stdio.h has sha256"), "{}", why.message);
742        assert!(why.message.contains("where the record says"), "{}", why.message);
743
744        let gone = Tree::new("gone");
745        let manifest = manifest_for(FILES);
746        let (archive, hash) = artifact(&gone, &FILES[..1], &manifest);
747        let cache = gone.0.join("cache");
748        let why = install(&archive, &hash, target(), &cache).expect_err("a missing file");
749        assert!(why.message.contains("lib/libc.so"), "{}", why.message);
750        assert!(why.message.contains("not in the archive"), "{}", why.message);
751    }
752
753    #[test]
754    fn more_than_one_disagreement_says_how_many() {
755        // Which of the two failures this is shows in the count, so the count is in the message.
756        let tree = Tree::new("count");
757        let manifest = manifest_for(&[("a.h", "one\n"), ("b.h", "two\n"), ("c.h", "three\n")]);
758        let (archive, hash) = artifact(
759            &tree,
760            &[("a.h", "not one\n"), ("b.h", "not two\n"), ("c.h", "three\n")],
761            &manifest,
762        );
763        let cache = tree.0.join("cache");
764
765        let why = install(&archive, &hash, target(), &cache).expect_err("two files disagree");
766        assert!(why.message.contains("and 1 more files disagree"), "{}", why.message);
767    }
768
769    #[test]
770    fn an_install_over_a_different_sysroot_says_what_it_replaced() {
771        let tree = Tree::new("replace");
772        let cache = tree.0.join("cache");
773        let first = manifest_for(&[("include/stdio.h", "the old one\n")]);
774        let (archive, hash) = artifact(&tree, &[("include/stdio.h", "the old one\n")], &first);
775        let done = install(&archive, &hash, target(), &cache).expect("the first install");
776        let was = done.digest.clone();
777
778        let second = Tree::new("replace-second");
779        let manifest = manifest_for(&[("include/stdio.h", "the new one\n")]);
780        let (archive, hash) = artifact(&second, &[("include/stdio.h", "the new one\n")], &manifest);
781        let done = install(&archive, &hash, target(), &cache).expect("the second install");
782
783        assert_eq!(done.before, Before::Different(was));
784        assert_eq!(
785            std::fs::read_to_string(done.root.join("include/stdio.h")).expect("the new file"),
786            "the new one\n"
787        );
788        // And the tree that was moved aside is gone rather than left beside the one that replaced
789        // it, since a cache that keeps every version it ever had is a cache nobody can size.
790        let kept: Vec<String> = std::fs::read_dir(cache.join("sysroots"))
791            .expect("the sysroots directory")
792            .map(|entry| entry.expect("an entry").file_name().to_string_lossy().into_owned())
793            .collect();
794        assert_eq!(kept, vec!["x86_64-linux-musl".to_owned()]);
795    }
796
797    /// The kernel tree's record for these files, which is four fields a line and no target.
798    fn kernel_manifest_for(files: &[(&str, &str)]) -> KernelManifest {
799        let mut manifest = KernelManifest::new(Version::new(6, 19));
800        for (path, text) in files {
801            manifest.push(KernelFile {
802                path: (*path).to_owned(),
803                source: "linux-6.19".to_owned(),
804                sha256: sha256::hex(text.as_bytes()),
805                licence: Licence::LinuxUapi,
806            });
807        }
808        manifest
809    }
810
811    const KERNEL_FILES: &[(&str, &str)] = &[
812        ("generic/linux/types.h", "#define _LINUX_TYPES_H\n"),
813        ("x86/asm/unistd.h", "#define __NR_read 0\n"),
814    ];
815
816    #[test]
817    fn the_kernel_tree_is_installed_beside_the_sysroots_and_not_under_them() {
818        let tree = Tree::new("kernel");
819        let manifest = kernel_manifest_for(KERNEL_FILES);
820        let (archive, hash) = artifact_with(&tree, KERNEL_FILES, &manifest.render());
821        let cache = tree.0.join("cache");
822
823        let done = install_kernel(&archive, &hash, &cache).expect("this one should install");
824        assert_eq!(done.root, cache.join("kernel-headers"));
825        assert_eq!(done.files, 2);
826        assert_eq!(done.digest, manifest.digest());
827        assert_eq!(done.before, Before::Nothing);
828        // Where `rucc_sysroot::Kernel` looks for the two directories a Linux compile searches.
829        let x86 = rucc_sysroot::Kernel::for_target(&cache, target()).expect("a Linux target");
830        assert!(x86.arch_include().join("asm/unistd.h").is_file());
831        assert!(x86.generic_include().join("linux/types.h").is_file());
832
833        let again = install_kernel(&archive, &hash, &cache).expect("the second install");
834        assert_eq!(again.before, Before::TheSame);
835    }
836
837    #[test]
838    fn a_sysroot_is_not_a_kernel_tree_and_a_kernel_tree_is_not_a_sysroot() {
839        // The two archives look alike from outside, a manifest and some directories, and each
840        // install reads only its own record, so handing one to the other is refused by the header
841        // before any file is looked at.
842        let tree = Tree::new("crossed");
843        let sysroot = manifest_for(FILES);
844        let (archive, hash) = artifact(&tree, FILES, &sysroot);
845        let cache = tree.0.join("cache");
846        let why = install_kernel(&archive, &hash, &cache).expect_err("a sysroot");
847        assert!(why.message.contains("kernel tree's record"), "{}", why.message);
848        assert!(!cache.join("kernel-headers").exists());
849
850        let other = Tree::new("crossed-kernel");
851        let kernel = kernel_manifest_for(KERNEL_FILES);
852        let (archive, hash) = artifact_with(&other, KERNEL_FILES, &kernel.render());
853        let cache = other.0.join("cache");
854        install(&archive, &hash, target(), &cache).expect_err("a kernel tree");
855        assert!(!cache.join("sysroots").exists());
856    }
857
858    #[test]
859    fn a_kernel_tree_with_a_file_its_record_does_not_name_is_refused() {
860        let tree = Tree::new("kernel-extra");
861        let manifest = kernel_manifest_for(KERNEL_FILES);
862        let mut with_extra: Vec<(&str, &str)> = KERNEL_FILES.to_vec();
863        with_extra.push(("arm64/asm/surprise.h", "nobody wrote this down\n"));
864        let (archive, hash) = artifact_with(&tree, &with_extra, &manifest.render());
865        let cache = tree.0.join("cache");
866        let why = install_kernel(&archive, &hash, &cache).expect_err("an unrecorded file");
867        assert!(why.message.contains("arm64/asm/surprise.h"), "{}", why.message);
868    }
869
870    #[test]
871    fn a_record_that_names_a_path_outside_the_tree_is_refused() {
872        // Not a likely producer bug, and the check is here because a manifest is data that has not
873        // been checked yet at the moment its paths are read.
874        assert!(relative(Path::new("/cache/sysroots/t"), "include/stdio.h").is_ok());
875        for path in ["../outside.h", "/etc/passwd", "include/../../outside.h"] {
876            let why = relative(Path::new("/cache/sysroots/t"), path)
877                .expect_err("this is not a path inside a sysroot");
878            assert!(why.contains(path), "{why}");
879        }
880    }
881
882    #[test]
883    fn the_walk_names_files_the_way_a_manifest_does() {
884        // With `/` between the parts whatever the host separator is, because the comparison is
885        // against a manifest and a manifest has one spelling.
886        let tree = Tree::new("walk");
887        tree.write("include/sys/types.h", "typedef int t;\n");
888        tree.write("manifest", "rucc sysroot manifest 3\n");
889        let mut found = Vec::new();
890        walk(&tree.0, String::new(), &mut found).expect("the walk should work");
891        found.sort();
892        assert_eq!(found, vec!["include/sys/types.h".to_owned(), "manifest".to_owned()]);
893    }
894
895    #[test]
896    fn verify_is_the_hash_of_the_file_and_says_both_when_it_is_not() {
897        let tree = Tree::new("verify");
898        tree.write("thing", "bytes\n");
899        let at = tree.0.join("thing");
900        let hash = sha256::hex(b"bytes\n");
901        assert!(verify(&at, &hash).is_ok());
902        let why = verify(&at, &"f".repeat(64)).expect_err("a mismatch");
903        assert!(why.message.contains(&hash), "{}", why.message);
904        assert!(why.message.contains(&"f".repeat(64)), "{}", why.message);
905
906        // A file that is not there is not a mismatch and does not read as one.
907        let why = verify(&tree.0.join("absent"), &hash).expect_err("nothing to hash");
908        assert!(!why.message.contains("where this release pins"), "{}", why.message);
909    }
910
911    #[test]
912    fn the_check_passes_a_tree_that_matches() {
913        // The unit underneath the install, so that a failure in the install tells you which half.
914        let tree = Tree::new("check");
915        for (path, text) in FILES {
916            tree.write(path, text);
917        }
918        tree.write("manifest", "rucc sysroot manifest 3\n");
919        let manifest = manifest_for(FILES);
920        let recorded: Vec<(&str, &str)> =
921            manifest.inputs().iter().map(|i| (i.path.as_str(), i.sha256.as_str())).collect();
922        assert_eq!(check(&tree.0, &recorded), Ok(()));
923        // An empty directory is not a file and is not a disagreement, which is what a tree that
924        // went through `tar` on one host and not another looks like.
925        std::fs::create_dir_all(tree.0.join("lib/empty")).expect("a directory");
926        assert_eq!(check(&tree.0, &recorded), Ok(()));
927    }
928
929    #[test]
930    fn installed_says_where_and_what() {
931        // The type the caller reports from, asserted once so that a change to it is a change to a
932        // test rather than to a message nobody reads.
933        let made = Installed {
934            root: PathBuf::from("/cache/sysroots/x86_64-linux-musl"),
935            digest: "0".repeat(64),
936            files: 3,
937            before: Before::Nothing,
938        };
939        assert_eq!(made.files, 3);
940        assert_eq!(made.before, Before::Nothing);
941    }
942}