diff options
| author | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-11-01 07:13:45 -0600 |
|---|---|---|
| committer | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-11-01 07:13:45 -0600 |
| commit | 9349a2de648e241da8e00ba7ae25cb2a877ff3e9 (patch) | |
| tree | 2fea913ee0aaff4370877f4a91c911db2fe37592 /src/app/files.rs | |
| parent | 5b89c370d5974f057306a52089f14c337565fd1d (diff) | |
feat(guix): carry prebuilt runtime and recover preserved state
Diffstat (limited to 'src/app/files.rs')
| -rw-r--r-- | src/app/files.rs | 77 |
1 files changed, 77 insertions, 0 deletions
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<BTreeMap<String, String>> { let mut files = BTreeMap::new(); for item in WalkDir::new(root).follow_links(false) { @@ -279,6 +307,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"); for name in ["/etc/passwd", "../escape", "stack/../escape", "other/file"] { |
