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::{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,
60 pub digest: String,
62 pub files: usize,
64 pub before: Before,
66}
67
68pub fn verify(archive: &Path, expected: &str) -> Result<(), CliError> {
81 let bytes = fs::read(archive).map_err(|why| err(format!("{}: {why}", archive.display())))?;
82 let found = sha256::hex(&bytes);
83 if found == expected {
84 return Ok(());
85 }
86 Err(err(format!(
87 "{} has sha256 {found} where this release pins {expected}, so it is not the artifact this \
88 build knows about",
89 archive.display()
90 )))
91}
92
93pub fn install(
106 archive: &Path,
107 expected: &str,
108 target: TargetTuple,
109 cache: &Path,
110) -> Result<Installed, CliError> {
111 verify(archive, expected)?;
112
113 let staging = staging_dir(cache, target);
114 fs::create_dir_all(&staging).map_err(|why| err(format!("{}: {why}", staging.display())))?;
115 let outcome = install_staged(archive, target, cache, &staging);
119 if outcome.is_err() {
120 let _ = fs::remove_dir_all(&staging);
121 }
122 outcome
123}
124
125fn install_staged(
127 archive: &Path,
128 target: TargetTuple,
129 cache: &Path,
130 staging: &Path,
131) -> Result<Installed, CliError> {
132 unpack(archive, staging)?;
133
134 let record = staging.join("manifest");
135 let text = fs::read_to_string(&record).map_err(|why| {
136 if why.kind() == io::ErrorKind::NotFound {
137 err(format!(
138 "{} has no manifest in it, so there is nothing to check its files against",
139 archive.display()
140 ))
141 } else {
142 err(format!("{}: {why}", record.display()))
143 }
144 })?;
145 let manifest =
146 Manifest::parse(&text).map_err(|why| err(format!("{}: {why}", archive.display())))?;
147
148 if manifest.target() != target {
149 return Err(err(format!(
150 "{} is a sysroot for {}, which is not {}",
151 archive.display(),
152 manifest.target().to_canonical_string(),
153 target.to_canonical_string()
154 )));
155 }
156
157 check(staging, &manifest).map_err(|why| err(format!("{}: {why}", archive.display())))?;
158
159 let digest = manifest.digest();
160 let root = Sysroot::in_cache(cache, target).root().to_path_buf();
161 let before = swap(staging, &root, &digest)?;
162 Ok(Installed { root, digest, files: manifest.inputs().len(), before })
163}
164
165fn staging_dir(cache: &Path, target: TargetTuple) -> PathBuf {
172 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
173 let unique =
174 format!("{}-{}-{}", target.to_canonical_string(), std::process::id(), now.as_nanos());
175 cache.join("staging").join(unique)
176}
177
178fn unpack(archive: &Path, into: &Path) -> Result<(), CliError> {
186 let output =
187 Command::new("tar").arg("-xzf").arg(archive).arg("-C").arg(into).output().map_err(
188 |why| err(format!("could not run `tar`, which is how an artifact is unpacked: {why}")),
189 )?;
190 if output.status.success() {
191 return Ok(());
192 }
193 let said = String::from_utf8_lossy(&output.stderr);
194 let said = said.trim();
195 let detail = if said.is_empty() { String::new() } else { format!(": {said}") };
196 Err(err(format!("`tar` could not unpack {}{detail}", archive.display())))
197}
198
199fn check(tree: &Path, manifest: &Manifest) -> Result<(), String> {
206 let mut problems: Vec<String> = Vec::new();
207 let mut recorded: Vec<&str> = Vec::new();
208
209 for input in manifest.inputs() {
210 recorded.push(&input.path);
211 let at = match relative(tree, &input.path) {
212 Ok(at) => at,
213 Err(why) => {
214 problems.push(why);
215 continue;
216 }
217 };
218 match fs::read(&at) {
219 Ok(bytes) => {
220 let found = sha256::hex(&bytes);
221 if found != input.sha256 {
222 problems.push(format!(
223 "{} has sha256 {found} where the record says {}",
224 input.path, input.sha256
225 ));
226 }
227 }
228 Err(why) if why.kind() == io::ErrorKind::NotFound => {
229 problems.push(format!("{} is in the record and not in the archive", input.path));
230 }
231 Err(why) => problems.push(format!("{}: {why}", input.path)),
232 }
233 }
234
235 let mut found = Vec::new();
236 walk(tree, String::new(), &mut found).map_err(|why| format!("{}: {why}", tree.display()))?;
237 recorded.sort_unstable();
238 for path in &found {
239 if path == "manifest" {
242 continue;
243 }
244 if recorded.binary_search(&path.as_str()).is_err() {
245 problems.push(format!("{path} is in the archive and not in the record"));
246 }
247 }
248
249 if problems.is_empty() {
250 return Ok(());
251 }
252 problems.sort();
253 let first = &problems[0];
254 if problems.len() == 1 {
255 return Err(format!("the archive does not match its own manifest: {first}"));
256 }
257 Err(format!(
258 "the archive does not match its own manifest: {first}, and {} more files disagree",
259 problems.len() - 1
260 ))
261}
262
263fn relative(tree: &Path, path: &str) -> Result<PathBuf, String> {
269 let candidate = Path::new(path);
270 let ordinary = candidate.components().all(|part| matches!(part, Component::Normal(_)));
271 if !ordinary {
272 return Err(format!("{path} is not a path inside a sysroot"));
273 }
274 Ok(tree.join(candidate))
275}
276
277fn walk(dir: &Path, prefix: String, out: &mut Vec<String>) -> io::Result<()> {
284 for entry in fs::read_dir(dir)? {
285 let entry = entry?;
286 let name = entry.file_name().to_string_lossy().into_owned();
287 let path = if prefix.is_empty() { name } else { format!("{prefix}/{name}") };
288 if entry.file_type()?.is_dir() {
289 walk(&entry.path(), path, out)?;
290 } else {
291 out.push(path);
292 }
293 }
294 Ok(())
295}
296
297fn swap(staging: &Path, root: &Path, digest: &str) -> Result<Before, CliError> {
308 let before = match existing(root) {
309 Some(found) if found == digest => {
310 let _ = fs::remove_dir_all(staging);
311 return Ok(Before::TheSame);
312 }
313 Some(found) => Before::Different(found),
314 None => Before::Nothing,
315 };
316
317 if let Some(parent) = root.parent() {
318 fs::create_dir_all(parent).map_err(|why| err(format!("{}: {why}", parent.display())))?;
319 }
320
321 let aside = root.with_extension(format!("old.{}", std::process::id()));
325 if before != Before::Nothing {
326 let _ = fs::remove_dir_all(&aside);
327 fs::rename(root, &aside)
328 .map_err(|why| err(format!("could not move {} aside: {why}", root.display())))?;
329 }
330 let renamed = fs::rename(staging, root);
331 if let Err(why) = renamed {
332 if before != Before::Nothing {
335 let _ = fs::rename(&aside, root);
336 }
337 return Err(err(format!("could not put {} in place: {why}", root.display())));
338 }
339 if before != Before::Nothing {
340 let _ = fs::remove_dir_all(&aside);
341 }
342 Ok(before)
343}
344
345fn existing(root: &Path) -> Option<String> {
351 let text = fs::read_to_string(root.join("manifest")).ok()?;
352 Manifest::parse(&text).ok().map(|manifest| manifest.digest())
353}
354
355#[cfg(test)]
356mod tests {
357 use super::{Before, Installed, check, install, relative, verify, walk};
358 use rucc_sysroot::{Input, Licence, Manifest, Provenance, sha256};
359 use rucc_tuple::TargetTuple;
360 use std::path::{Path, PathBuf};
361 use std::process::Command;
362
363 struct Tree(PathBuf);
365
366 impl Drop for Tree {
367 fn drop(&mut self) {
368 let _ = std::fs::remove_dir_all(&self.0);
369 }
370 }
371
372 impl Tree {
373 fn new(name: &str) -> Tree {
374 let dir =
375 std::env::temp_dir().join(format!("rucc-install-{}-{name}", std::process::id()));
376 let _ = std::fs::remove_dir_all(&dir);
377 std::fs::create_dir_all(&dir).expect("a temporary directory should be writable");
378 Tree(dir)
379 }
380
381 fn write(&self, path: &str, text: &str) {
382 let at = self.0.join(path);
383 if let Some(parent) = at.parent() {
384 std::fs::create_dir_all(parent).expect("a subdirectory should be creatable");
385 }
386 std::fs::write(&at, text).expect("a temporary file should be writable");
387 }
388 }
389
390 fn target() -> TargetTuple {
392 "x86_64-linux-musl".parse().expect("a tuple the table knows")
393 }
394
395 fn manifest_for(files: &[(&str, &str)]) -> Manifest {
397 let mut manifest = Manifest::new(target());
398 for (path, text) in files {
399 manifest.push(Input {
400 path: (*path).to_owned(),
401 source: "musl-1.2.5".to_owned(),
402 url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_owned(),
403 sha256: sha256::hex(text.as_bytes()),
404 licence: Licence::Mit,
405 provenance: Provenance::Bundled,
406 });
407 }
408 manifest
409 }
410
411 fn artifact(tree: &Tree, files: &[(&str, &str)], manifest: &Manifest) -> (PathBuf, String) {
416 let staged = tree.0.join("staged");
417 std::fs::create_dir_all(&staged).expect("a staging directory should be creatable");
418 for (path, text) in files {
419 let at = staged.join(path);
420 if let Some(parent) = at.parent() {
421 std::fs::create_dir_all(parent).expect("a subdirectory should be creatable");
422 }
423 std::fs::write(&at, text).expect("a file should be writable");
424 }
425 std::fs::write(staged.join("manifest"), manifest.render())
426 .expect("the manifest should be writable");
427
428 let archive = tree.0.join("artifact.tar.gz");
429 let status = Command::new("tar")
430 .arg("-czf")
431 .arg(&archive)
432 .arg("-C")
433 .arg(&staged)
434 .arg(".")
435 .status()
436 .expect("tar should be on a machine that runs these tests");
437 assert!(status.success(), "tar should be able to write an archive");
438 std::fs::remove_dir_all(&staged).expect("the staged tree should be removable");
439
440 let bytes = std::fs::read(&archive).expect("the archive should be readable");
441 let hash = sha256::hex(&bytes);
442 (archive, hash)
443 }
444
445 const FILES: &[(&str, &str)] =
446 &[("include/stdio.h", "int puts(const char *);\n"), ("lib/libc.so", "not really\n")];
447
448 #[test]
449 fn an_artifact_that_matches_its_record_is_installed() {
450 let tree = Tree::new("good");
451 let manifest = manifest_for(FILES);
452 let (archive, hash) = artifact(&tree, FILES, &manifest);
453 let cache = tree.0.join("cache");
454
455 let done = install(&archive, &hash, target(), &cache).expect("this one should install");
456 assert_eq!(done.before, Before::Nothing);
457 assert_eq!(done.files, 2);
458 assert_eq!(done.digest, manifest.digest());
459 assert_eq!(done.root, cache.join("sysroots").join("x86_64-linux-musl"));
460
461 assert!(done.root.join("include/stdio.h").is_file());
464 assert_eq!(
465 std::fs::read_to_string(done.root.join("manifest")).expect("a manifest"),
466 manifest.render()
467 );
468 let left: Vec<PathBuf> = std::fs::read_dir(cache.join("staging"))
471 .expect("the staging directory")
472 .map(|entry| entry.expect("an entry").path())
473 .collect();
474 assert_eq!(left, Vec::<PathBuf>::new(), "a staging tree was left behind");
475 }
476
477 #[test]
488 fn a_fetch_of_an_artifact_that_is_already_on_the_machine_installs_it() {
489 let tree = Tree::new("fetch");
490 let manifest = manifest_for(FILES);
491 let (built, hash) = artifact(&tree, FILES, &manifest);
492 let cache = tree.0.join("cache");
493
494 let pinned = rucc_sysroot::Pinned {
498 tuple: "x86_64-linux-musl",
499 url: "https://example.invalid/rucc-sysroot-x86_64-linux-musl.tar.gz",
500 sha256: String::leak(hash),
501 };
502 let at = pinned.archive_in(&cache);
504 std::fs::create_dir_all(at.parent().expect("a parent")).expect("a downloads directory");
505 std::fs::copy(&built, &at).expect("the artifact should be placeable");
506
507 assert_eq!(crate::fetch_sysroot(&pinned, target(), &cache), 0);
508 let root = cache.join("sysroots").join("x86_64-linux-musl");
509 assert!(root.join("include/stdio.h").is_file());
510 assert_eq!(
511 std::fs::read_to_string(root.join("manifest")).expect("a manifest"),
512 manifest.render()
513 );
514 assert_eq!(crate::fetch_sysroot(&pinned, target(), &cache), 0);
517 }
518
519 #[test]
520 fn the_same_artifact_twice_does_not_move_anything() {
521 let tree = Tree::new("again");
522 let manifest = manifest_for(FILES);
523 let (archive, hash) = artifact(&tree, FILES, &manifest);
524 let cache = tree.0.join("cache");
525
526 let first = install(&archive, &hash, target(), &cache).expect("the first install");
527 let second = install(&archive, &hash, target(), &cache).expect("the second install");
528 assert_eq!(second.before, Before::TheSame);
529 assert_eq!(second.root, first.root);
530 assert_eq!(second.digest, first.digest);
531 }
532
533 #[test]
534 fn a_hash_that_does_not_match_is_refused_before_anything_is_unpacked() {
535 let tree = Tree::new("hash");
536 let manifest = manifest_for(FILES);
537 let (archive, _) = artifact(&tree, FILES, &manifest);
538 let cache = tree.0.join("cache");
539
540 let wrong = "0".repeat(64);
541 let why =
542 install(&archive, &wrong, target(), &cache).expect_err("this is not the artifact");
543 assert!(why.message.contains("where this release pins"), "{}", why.message);
544 assert!(!cache.exists(), "a refused artifact should not have reached the cache");
547 }
548
549 #[test]
550 fn an_artifact_for_another_target_is_refused() {
551 let tree = Tree::new("target");
552 let mut manifest = Manifest::new("aarch64-linux-musl".parse().expect("a tuple"));
553 for (path, text) in FILES {
554 manifest.push(Input {
555 path: (*path).to_owned(),
556 source: "musl-1.2.5".to_owned(),
557 url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_owned(),
558 sha256: sha256::hex(text.as_bytes()),
559 licence: Licence::Mit,
560 provenance: Provenance::Bundled,
561 });
562 }
563 let (archive, hash) = artifact(&tree, FILES, &manifest);
564 let cache = tree.0.join("cache");
565
566 let why = install(&archive, &hash, target(), &cache).expect_err("the wrong target");
567 assert!(why.message.contains("aarch64-linux-musl"), "{}", why.message);
568 assert!(why.message.contains("x86_64-linux-musl"), "{}", why.message);
569 assert!(!cache.join("sysroots").exists(), "nothing should have been installed");
570 }
571
572 #[test]
573 fn an_archive_with_no_record_in_it_is_refused() {
574 let tree = Tree::new("bare");
575 let staged = tree.0.join("staged");
576 std::fs::create_dir_all(&staged).expect("a directory");
577 std::fs::write(staged.join("include.h"), "int x;\n").expect("a file");
578 let archive = tree.0.join("bare.tar.gz");
579 let status = Command::new("tar")
580 .arg("-czf")
581 .arg(&archive)
582 .arg("-C")
583 .arg(&staged)
584 .arg(".")
585 .status()
586 .expect("tar should run");
587 assert!(status.success());
588 let hash = sha256::hex(&std::fs::read(&archive).expect("readable"));
589 let cache = tree.0.join("cache");
590
591 let why = install(&archive, &hash, target(), &cache).expect_err("no manifest");
592 assert!(why.message.contains("has no manifest in it"), "{}", why.message);
593 }
594
595 #[test]
596 fn a_file_the_record_does_not_name_is_refused() {
597 let tree = Tree::new("extra");
600 let manifest = manifest_for(FILES);
601 let mut with_extra: Vec<(&str, &str)> = FILES.to_vec();
602 with_extra.push(("lib/surprise.o", "nobody wrote this down\n"));
603 let (archive, hash) = artifact(&tree, &with_extra, &manifest);
604 let cache = tree.0.join("cache");
605
606 let why = install(&archive, &hash, target(), &cache).expect_err("an unrecorded file");
607 assert!(why.message.contains("lib/surprise.o"), "{}", why.message);
608 assert!(why.message.contains("not in the record"), "{}", why.message);
609 }
610
611 #[test]
612 fn a_file_whose_bytes_changed_is_refused_and_so_is_one_that_is_missing() {
613 let tree = Tree::new("bytes");
614 let manifest = manifest_for(&[("include/stdio.h", "what the record says\n")]);
617 let (archive, hash) = artifact(&tree, &[("include/stdio.h", "what is there\n")], &manifest);
618 let cache = tree.0.join("cache");
619 let why = install(&archive, &hash, target(), &cache).expect_err("changed bytes");
620 assert!(why.message.contains("include/stdio.h has sha256"), "{}", why.message);
621 assert!(why.message.contains("where the record says"), "{}", why.message);
622
623 let gone = Tree::new("gone");
624 let manifest = manifest_for(FILES);
625 let (archive, hash) = artifact(&gone, &FILES[..1], &manifest);
626 let cache = gone.0.join("cache");
627 let why = install(&archive, &hash, target(), &cache).expect_err("a missing file");
628 assert!(why.message.contains("lib/libc.so"), "{}", why.message);
629 assert!(why.message.contains("not in the archive"), "{}", why.message);
630 }
631
632 #[test]
633 fn more_than_one_disagreement_says_how_many() {
634 let tree = Tree::new("count");
636 let manifest = manifest_for(&[("a.h", "one\n"), ("b.h", "two\n"), ("c.h", "three\n")]);
637 let (archive, hash) = artifact(
638 &tree,
639 &[("a.h", "not one\n"), ("b.h", "not two\n"), ("c.h", "three\n")],
640 &manifest,
641 );
642 let cache = tree.0.join("cache");
643
644 let why = install(&archive, &hash, target(), &cache).expect_err("two files disagree");
645 assert!(why.message.contains("and 1 more files disagree"), "{}", why.message);
646 }
647
648 #[test]
649 fn an_install_over_a_different_sysroot_says_what_it_replaced() {
650 let tree = Tree::new("replace");
651 let cache = tree.0.join("cache");
652 let first = manifest_for(&[("include/stdio.h", "the old one\n")]);
653 let (archive, hash) = artifact(&tree, &[("include/stdio.h", "the old one\n")], &first);
654 let done = install(&archive, &hash, target(), &cache).expect("the first install");
655 let was = done.digest.clone();
656
657 let second = Tree::new("replace-second");
658 let manifest = manifest_for(&[("include/stdio.h", "the new one\n")]);
659 let (archive, hash) = artifact(&second, &[("include/stdio.h", "the new one\n")], &manifest);
660 let done = install(&archive, &hash, target(), &cache).expect("the second install");
661
662 assert_eq!(done.before, Before::Different(was));
663 assert_eq!(
664 std::fs::read_to_string(done.root.join("include/stdio.h")).expect("the new file"),
665 "the new one\n"
666 );
667 let kept: Vec<String> = std::fs::read_dir(cache.join("sysroots"))
670 .expect("the sysroots directory")
671 .map(|entry| entry.expect("an entry").file_name().to_string_lossy().into_owned())
672 .collect();
673 assert_eq!(kept, vec!["x86_64-linux-musl".to_owned()]);
674 }
675
676 #[test]
677 fn a_record_that_names_a_path_outside_the_tree_is_refused() {
678 assert!(relative(Path::new("/cache/sysroots/t"), "include/stdio.h").is_ok());
681 for path in ["../outside.h", "/etc/passwd", "include/../../outside.h"] {
682 let why = relative(Path::new("/cache/sysroots/t"), path)
683 .expect_err("this is not a path inside a sysroot");
684 assert!(why.contains(path), "{why}");
685 }
686 }
687
688 #[test]
689 fn the_walk_names_files_the_way_a_manifest_does() {
690 let tree = Tree::new("walk");
693 tree.write("include/sys/types.h", "typedef int t;\n");
694 tree.write("manifest", "rucc sysroot manifest 3\n");
695 let mut found = Vec::new();
696 walk(&tree.0, String::new(), &mut found).expect("the walk should work");
697 found.sort();
698 assert_eq!(found, vec!["include/sys/types.h".to_owned(), "manifest".to_owned()]);
699 }
700
701 #[test]
702 fn verify_is_the_hash_of_the_file_and_says_both_when_it_is_not() {
703 let tree = Tree::new("verify");
704 tree.write("thing", "bytes\n");
705 let at = tree.0.join("thing");
706 let hash = sha256::hex(b"bytes\n");
707 assert!(verify(&at, &hash).is_ok());
708 let why = verify(&at, &"f".repeat(64)).expect_err("a mismatch");
709 assert!(why.message.contains(&hash), "{}", why.message);
710 assert!(why.message.contains(&"f".repeat(64)), "{}", why.message);
711
712 let why = verify(&tree.0.join("absent"), &hash).expect_err("nothing to hash");
714 assert!(!why.message.contains("where this release pins"), "{}", why.message);
715 }
716
717 #[test]
718 fn the_check_passes_a_tree_that_matches() {
719 let tree = Tree::new("check");
721 for (path, text) in FILES {
722 tree.write(path, text);
723 }
724 tree.write("manifest", "rucc sysroot manifest 3\n");
725 let manifest = manifest_for(FILES);
726 assert_eq!(check(&tree.0, &manifest), Ok(()));
727 std::fs::create_dir_all(tree.0.join("lib/empty")).expect("a directory");
730 assert_eq!(check(&tree.0, &manifest), Ok(()));
731 }
732
733 #[test]
734 fn installed_says_where_and_what() {
735 let made = Installed {
738 root: PathBuf::from("/cache/sysroots/x86_64-linux-musl"),
739 digest: "0".repeat(64),
740 files: 3,
741 before: Before::Nothing,
742 };
743 assert_eq!(made.files, 3);
744 assert_eq!(made.before, Before::Nothing);
745 }
746}