From 9349a2de648e241da8e00ba7ae25cb2a877ff3e9 Mon Sep 17 00:00:00 2001 From: Doan Luu Duc Tai Date: Sun, 1 Nov 2026 07:13:45 -0600 Subject: feat(guix): carry prebuilt runtime and recover preserved state --- src/app/files.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) (limited to 'src/app/files.rs') diff --git a/src/app/files.rs b/src/app/files.rs index 2c612a9..e96e5a4 100644 --- a/src/app/files.rs +++ b/src/app/files.rs @@ -161,6 +161,34 @@ pub(super) fn copy_source(root: &Path, destination: &Path) -> Result<()> { Ok(()) } +pub(super) fn copy_runtime(root: &Path, destination: &Path) -> Result<()> { + let archive = root.join("bin/guix-runtime.tar.gz"); + let channels = root.join("bin/guix-channels.scm"); + ensure!( + archive.is_file() == channels.is_file(), + "Guix runtime archive and channel provenance must both be present" + ); + if archive.is_file() { + let bin = destination.join("bin"); + fs::create_dir_all(&bin)?; + for name in ["guix-runtime.tar.gz", "guix-channels.scm"] { + let source = root.join("bin").join(name); + regular_tree(&source)?; + let pending = tempfile::NamedTempFile::new_in(&bin)?; + fs::copy(source, pending.path())?; + pending.as_file().sync_all()?; + pending + .persist(bin.join(name)) + .map_err(|error| error.error)?; + } + } + let launcher = root.join("infra/guix/run.sh"); + if launcher.is_file() { + write(&destination.join("bin/run"), &fs::read(launcher)?, 0o755)?; + } + Ok(()) +} + pub(super) fn inventory(root: &Path) -> Result> { let mut files = BTreeMap::new(); for item in WalkDir::new(root).follow_links(false) { @@ -278,6 +306,55 @@ mod tests { use super::{copy_source, extract, manifest, member_path, pack, verify}; + #[test] + fn runtime_artifacts_survive_archive_round_trip() -> Result<()> { + let source = tempfile::tempdir()?; + let stage = tempfile::tempdir()?; + fs::create_dir_all(source.path().join("bin"))?; + fs::create_dir_all(source.path().join("infra/guix"))?; + fs::write(source.path().join("infra/guix/run.sh"), "launcher")?; + super::copy_runtime(source.path(), stage.path())?; + anyhow::ensure!(stage.path().join("bin/run").is_file(), "Launcher missing"); + fs::write(source.path().join("bin/guix-runtime.tar.gz"), "opaque pack")?; + anyhow::ensure!( + super::copy_runtime(source.path(), stage.path()).is_err(), + "Missing provenance accepted" + ); + fs::write( + source.path().join("bin/guix-channels.scm"), + "pinned channels", + )?; + super::copy_runtime(source.path(), stage.path())?; + fs::set_permissions( + stage.path().join("bin/guix-runtime.tar.gz"), + std::os::unix::fs::PermissionsExt::from_mode(0o400), + )?; + fs::write( + source.path().join("bin/guix-runtime.tar.gz"), + "updated pack", + )?; + super::copy_runtime(source.path(), stage.path())?; + anyhow::ensure!( + fs::read_to_string(stage.path().join("bin/guix-runtime.tar.gz"))? == "updated pack", + "Read-only runtime was not replaced" + ); + manifest(stage.path(), "backup")?; + let output = source.path().join("backup.tar.gz"); + pack(stage.path(), &output)?; + let restored = source.path().join("restored"); + extract(&output, &restored)?; + verify(&restored)?; + anyhow::ensure!( + fs::read_to_string(restored.join("bin/guix-runtime.tar.gz"))? == "updated pack", + "Runtime changed during recovery" + ); + anyhow::ensure!( + fs::read_to_string(restored.join("bin/guix-channels.scm"))? == "pinned channels", + "Provenance changed during recovery" + ); + Ok(()) + } + #[test] fn rejects_paths_outside_archive_root() -> Result<()> { let destination = Path::new("/restore"); -- cgit v1.2.3