1use 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#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Before {
44 Nothing,
46 TheSame,
50 Different(String),
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Installed {
58 pub root: PathBuf,
61 pub digest: String,
63 pub files: usize,
65 pub before: Before,
67}
68
69pub 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
94pub 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
118pub 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
134fn 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
153fn 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
168fn 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 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
205fn 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
227fn 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
239fn 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
260fn 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 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
325fn 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
339fn 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
359fn 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 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 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
412fn 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
422fn 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 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 fn target() -> TargetTuple {
465 "x86_64-linux-musl".parse().expect("a tuple the table knows")
466 }
467
468 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 fn artifact(tree: &Tree, files: &[(&str, &str)], manifest: &Manifest) -> (PathBuf, String) {
489 artifact_with(tree, files, &manifest.render())
490 }
491
492 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 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 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 #[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 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 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 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 assert_eq!(crate::fetch_sysroot(&pinned, Some(&kernel), target(), &cache), 0);
607 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}