diff --git a/PLAN.md b/PLAN.md index 60f2405e..edfd986e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1026,3 +1026,8 @@ Optional delegated cgroup v2 supervision adds aggregate memory, task and CPU bandwidth limits. An independent pipe watcher kills the group on supervisor death. Delegation is an explicit administrator operation; unavailable controllers fail closed and normal per-process rlimits remain in force. + +Receipt comparison reports differences in recipe, sources, image, dependencies, +settings and outputs. Offline replay requires approved commits, retained source +inputs and matching image fingerprints, and uses the recorded SOURCE_DATE_EPOCH. +The result is a local comparison, not an independent reproducibility attestation. diff --git a/crates/pacvamp/src/aur/build.rs b/crates/pacvamp/src/aur/build.rs index 33482bf3..6d62b1cf 100644 --- a/crates/pacvamp/src/aur/build.rs +++ b/crates/pacvamp/src/aur/build.rs @@ -19,6 +19,8 @@ use crate::manifest::Settings; /// How to build. #[derive(Debug, Clone)] pub struct BuildOpts { + pub source_date_epoch: Option, + pub image_sha256: Option, pub cgroup_root: Option, pub cache_lease: std::sync::Arc>, /// Apply the Landlock and seccomp jail to the build phase. @@ -66,7 +68,13 @@ impl BuildOpts { if settings.aur_cgroup_root.is_some() && !settings.aur_jail { bail!("cgroup builds require the filesystem jail"); } + let image_sha256 = chroot + .as_deref() + .map(super::receipt::image_digest) + .transpose()?; Ok(BuildOpts { + source_date_epoch: None, + image_sha256, cgroup_root: settings.aur_cgroup_root.clone(), cache_lease, jail: settings.aur_jail, @@ -138,7 +146,7 @@ pub fn missing_deps(host: &Host, reviewed: &Reviewed, arch: &str) -> Result Result> { - build_with_options(reviewed, opts, false) + build_with_options(reviewed, opts, false, None) } /// Bootstrap a reviewed split pkgbase whose sibling closes a dependency @@ -148,14 +156,49 @@ pub fn build_without_dependency_checks( reviewed: &Reviewed, opts: &BuildOpts, ) -> Result> { - build_with_options(reviewed, opts, true) + build_with_options(reviewed, opts, true, None) +} + +/// Replay retained source inputs in an image matching the original receipt. +pub fn replay( + reviewed: &Reviewed, + opts: &BuildOpts, + reference: &super::receipt::Receipt, + sources: &Path, +) -> Result> { + if reference.pkgbase != reviewed.pkgbase || reference.commit != reviewed.target { + bail!("recipe does not match reference receipt"); + } + if reference.build_network + || reference.source_date_epoch.is_none() + || reference.image_sha256.is_none() + { + bail!("replay needs an offline-build receipt with a pinned image and source date"); + } + if opts.image_sha256 != reference.image_sha256 || opts.dependencies != reference.dependencies { + bail!("build image differs from reference; use aur compare to inspect other builds"); + } + let mut opts = opts.clone(); + opts.network = false; + opts.source_date_epoch = reference.source_date_epoch; + build_with_options(reviewed, &opts, false, Some((reference, sources))) } fn build_with_options( reviewed: &Reviewed, opts: &BuildOpts, without_dependency_checks: bool, + replay: Option<(&super::receipt::Receipt, &Path)>, ) -> Result> { + let mut options = opts.clone(); + if options.source_date_epoch.is_none() { + options.source_date_epoch = reviewed + .checkout + .log(&reviewed.target, 1)? + .first() + .map(|commit| commit.time); + } + let opts = &options; let checkout = &reviewed.checkout; let cache = checkout .dir @@ -176,13 +219,24 @@ fn build_with_options( for dir in [&opts.srcdest, &opts.builddir, &opts.logdest, &verifydir] { std::fs::create_dir_all(dir).wrap_err_with(|| format!("creating {}", dir.display()))?; } + if let Some((reference, sources)) = replay { + if super::receipt::inputs(sources)? != reference.sources + || super::receipt::vcs_refs(sources)? != reference.vcs_refs + { + bail!("retained source inputs no longer match receipt"); + } + copy_tree(sources, &opts.srcdest)?; + } checkout.export(&reviewed.target, &verifydir.join("worktree"))?; checkout.export(&reviewed.target, &opts.builddir.join("worktree"))?; // Phase 1 only downloads and verifies sources. Unlike --nobuild, // --verifysource does not run prepare() or pkgver() outside the jail. - let verify_args = ["--verifysource", "--noconfirm", "--force"]; - let status = run_makepkg(opts, &verify_args, true, true, &verifydir) + let mut verify_args = vec!["--verifysource", "--noconfirm", "--force"]; + if replay.is_some() { + verify_args.push("--holdver"); + } + let status = run_makepkg(opts, &verify_args, replay.is_none(), true, &verifydir) .wrap_err("running makepkg --verifysource")?; if !status.success() { bail!( @@ -196,6 +250,11 @@ fn build_with_options( let sources = super::receipt::inputs(&opts.srcdest)?; let refs = super::receipt::vcs_refs(&opts.srcdest)?; + if let Some((reference, _)) = replay + && (sources != reference.sources || refs != reference.vcs_refs) + { + bail!("verification changed pinned source inputs"); + } // Phase 2 extracts, prepares, builds, and packages inside the jail. // --holdver prevents makepkg from updating VCS sources a second time; // phase 1 already fetched and verified the exact source state. @@ -426,6 +485,9 @@ fn spawn_makepkg( if opts.chroot.is_some() { command.env("PATH", "/usr/bin:/bin"); } + if let Some(epoch) = opts.source_date_epoch { + command.env("SOURCE_DATE_EPOCH", epoch.to_string()); + } set_private_home(&mut command, builddir)?; if opts.chroot.is_some() { let run = opts @@ -448,6 +510,9 @@ fn spawn_makepkg( if capture_output { command.stdout(Stdio::piped()).stderr(Stdio::piped()); + } else { + use std::os::fd::AsFd as _; + command.stdout(Stdio::from(std::io::stderr().as_fd().try_clone_to_owned()?)); } let mut child = crate::build_process::ManagedChild::new(command.spawn().wrap_err("starting makepkg")?)?; diff --git a/crates/pacvamp/src/aur/receipt.rs b/crates/pacvamp/src/aur/receipt.rs index b51ca64d..7be1a042 100644 --- a/crates/pacvamp/src/aur/receipt.rs +++ b/crates/pacvamp/src/aur/receipt.rs @@ -16,11 +16,17 @@ pub struct Reference { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Input { + #[serde(default)] + pub mode: Option, pub sha256: Option, pub link: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct Receipt { + #[serde(default)] + pub source_date_epoch: Option, + #[serde(default)] + pub image_sha256: Option, pub schema: u32, pub claim: String, pub pkgbase: String, @@ -45,6 +51,7 @@ pub fn inputs(root: &Path) -> Result> { out.insert( path.strip_prefix(root)?.into(), Input { + mode: None, sha256: None, link: Some(std::fs::read_link(path)?), }, @@ -53,6 +60,10 @@ pub fn inputs(root: &Path) -> Result> { out.insert( path.strip_prefix(root)?.into(), Input { + mode: Some({ + use std::os::unix::fs::PermissionsExt as _; + meta.permissions().mode() & 0o7777 + }), sha256: Some(packslip::digest_file(path)?.0), link: None, }, @@ -120,7 +131,14 @@ pub fn write( .ok_or_else(|| eyre::eyre!("invalid output filename"))?; outputs.insert(name.into(), packslip::digest_file(file)?.0); } + if let Some(root) = &opts.chroot + && Some(image_digest(root)?) != opts.image_sha256 + { + bail!("build image changed while building; refusing receipt"); + } let receipt = Receipt { + source_date_epoch: opts.source_date_epoch, + image_sha256: opts.image_sha256.clone(), schema: 1, claim: "local observation; not a signed attestation".into(), pkgbase: reviewed.pkgbase.clone(), @@ -182,3 +200,128 @@ pub fn for_artifact(file: &Path) -> Result<(Receipt, Reference)> { }, )) } + +/// Hash the image visible to the builder; private entries retain metadata only. +pub fn image_digest(root: &Path) -> Result { + use sha2::{Digest as _, Sha256}; + use std::{io::Read as _, os::unix::fs::MetadataExt as _}; + fn visit( + root: &Path, + path: &Path, + out: &mut BTreeMap, + ) -> Result<()> { + let meta = std::fs::symlink_metadata(path)?; + let mut value = serde_json::json!({"mode":meta.mode(),"uid":meta.uid(),"gid":meta.gid(),"mtime":meta.mtime(),"mtime_nsec":meta.mtime_nsec()}); + if meta.is_symlink() { + value["link"] = serde_json::to_value(std::fs::read_link(path)?)?; + } else if meta.is_file() { + match std::fs::File::open(path) { + Ok(mut file) => { + let mut hash = Sha256::new(); + let mut buffer = [0; 65536]; + loop { + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + hash.update(&buffer[..n]); + } + value["sha256"] = format!("{:x}", hash.finalize()).into(); + } + Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + value["unreadable"] = true.into(); + value["size"] = meta.len().into(); + } + Err(err) => return Err(err.into()), + } + } else if meta.is_dir() { + match std::fs::read_dir(path) { + Ok(entries) => { + for entry in entries { + visit(root, &entry?.path(), out)?; + } + } + Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + value["unreadable"] = true.into() + } + Err(err) => return Err(err.into()), + } + } else { + // Sockets and device nodes (for example stale GnuPG agent sockets) + // have no stable file contents to hash. Never open them. + value["special"] = true.into(); + value["device"] = meta.rdev().into(); + } + out.insert(path.strip_prefix(root)?.into(), value); + Ok(()) + } + let mut inventory = BTreeMap::new(); + for entry in std::fs::read_dir(root)? { + let entry = entry?; + if [ + "dev", + "proc", + "sys", + "run", + "tmp", + "build", + "pacvamp-helper", + "pacvamp-cgroup", + ] + .iter() + .any(|reserved| entry.file_name() == *reserved) + { + continue; + } + visit(root, &entry.path(), &mut inventory)?; + } + Ok(format!( + "{:x}", + Sha256::digest(serde_json::to_vec(&inventory)?) + )) +} + +#[derive(Debug, Serialize)] +pub struct Difference { + pub component: String, + pub before: serde_json::Value, + pub after: serde_json::Value, +} +#[derive(Debug, Serialize)] +pub struct Comparison { + pub identical: bool, + pub differences: Vec, + pub claim: &'static str, +} +pub fn compare(before: &Receipt, after: &Receipt) -> Result { + let a = serde_json::to_value(before)?; + let b = serde_json::to_value(after)?; + let mut differences = Vec::new(); + for key in [ + "pkgbase", + "commit", + "source_date_epoch", + "image_sha256", + "jail", + "build_network", + "limits", + "makepkg_sha256", + "dependencies", + "sources", + "vcs_refs", + "outputs", + ] { + if a[key] != b[key] { + differences.push(Difference { + component: key.into(), + before: a[key].clone(), + after: b[key].clone(), + }); + } + } + Ok(Comparison { + identical: differences.is_empty(), + differences, + claim: "local comparison of recorded inputs and outputs; not an independent reproducibility attestation", + }) +} diff --git a/crates/pacvamp/src/cli/aur_cmd.rs b/crates/pacvamp/src/cli/aur_cmd.rs index 7e2dcc21..472e7196 100644 --- a/crates/pacvamp/src/cli/aur_cmd.rs +++ b/crates/pacvamp/src/cli/aur_cmd.rs @@ -24,6 +24,8 @@ enum AurCommands { Diff(Diff), Review(Review), Receipt(Receipt), + Compare(Compare), + Rebuild(Rebuild), } /// Build an approved AUR package without installing it @@ -1036,3 +1038,85 @@ impl RunWith<&App> for Receipt { Ok(()) } } + +/// Compare verified local receipts, including sources, image and package outputs +#[derive(Debug, usage_rs::Args)] +pub struct Compare { + first: std::path::PathBuf, + second: std::path::PathBuf, + #[usage(long)] + json: bool, +} +impl RunWith<&App> for Compare { + type Output = Result<()>; + fn run_with(self, _: &App) -> Result<()> { + let (first, _) = crate::aur::receipt::for_artifact(&self.first)?; + let (second, _) = crate::aur::receipt::for_artifact(&self.second)?; + comparison_output(&first, &second, self.json) + } +} +fn comparison_output( + first: &crate::aur::receipt::Receipt, + second: &crate::aur::receipt::Receipt, + json: bool, +) -> Result<()> { + let comparison = crate::aur::receipt::compare(first, second)?; + if json { + print_json(&comparison)?; + } else { + println!("{}", comparison.claim); + for diff in &comparison.differences { + println!( + "{} differs:\n before: {}\n after: {}", + diff.component, diff.before, diff.after + ); + } + if comparison.identical { + println!("recorded inputs and output hashes match"); + } + } + if !comparison.identical { + bail!("build records differ"); + } + Ok(()) +} +/// Rebuild an approved receipt using retained sources and an identical Arch image +#[derive(Debug, usage_rs::Args)] +pub struct Rebuild { + artifact: std::path::PathBuf, + /// Retained image with the same contents as the original build + #[usage(long)] + image: std::path::PathBuf, + #[usage(long)] + json: bool, +} +impl RunWith<&App> for Rebuild { + type Output = Result<()>; + fn run_with(self, app: &App) -> Result<()> { + let _lease = crate::aur::cache::lease(&crate::aur::cache_dir(), false)?; + let (reference, receipt_ref) = crate::aur::receipt::for_artifact(&self.artifact)?; + let mut prepared = + app.prepare_aur(&reference.pkgbase, Some(&reference.commit), true, false)?; + let image = self.image.canonicalize()?; + if prepared.settings.aur_chroot_root_managed + && prepared.settings.aur_chroot_root.canonicalize()? != image + { + bail!("rebuild image differs from the managed image root"); + } + prepared.settings.aur_chroot = true; + prepared.settings.aur_chroot_root = image; + let opts = crate::aur::build::BuildOpts::from_settings( + &prepared.settings, + &prepared.reviewed.pkgbase, + &crate::aur::cache_dir(), + &app.host()?, + )?; + let sources = receipt_ref.path.parent().unwrap().join("sources"); + let files = crate::aur::build::replay(&prepared.reviewed, &opts, &reference, &sources)?; + for file in &files { + eprintln!("rebuilt artifact: {}", file.display()); + } + let (rebuilt, _) = crate::aur::receipt::for_artifact(&files[0])?; + comparison_output(&reference, &rebuilt, self.json) + } +} diff --git a/crates/pacvamp/src/manifest/settings.rs b/crates/pacvamp/src/manifest/settings.rs index 93d9dc36..1e3bdeb1 100644 --- a/crates/pacvamp/src/manifest/settings.rs +++ b/crates/pacvamp/src/manifest/settings.rs @@ -271,6 +271,8 @@ pub struct Settings { pub aur_jail: bool, pub aur_chroot: bool, pub aur_chroot_root: std::path::PathBuf, + #[serde(skip)] + pub aur_chroot_root_managed: bool, pub aur_cgroup_root: Option, pub aur_limits: crate::build_process::Limits, pub aur_allow_network_build: Vec, @@ -304,6 +306,7 @@ impl Default for Settings { aur_min_votes: 10, aur_jail: true, aur_chroot: false, + aur_chroot_root_managed: false, aur_cgroup_root: None, aur_chroot_root: "/var/lib/pacvamp/chroot/root".into(), aur_limits: Default::default(), @@ -431,6 +434,7 @@ impl Settings { true_wins!(aur_jail, managed.aur.jail); true_wins!(aur_chroot, managed.aur.chroot); if let Some(root) = &managed.aur.chroot_root { + self.aur_chroot_root_managed = true; self.aur_chroot_root = root.clone(); } if let Some(root) = &managed.aur.cgroup_root { diff --git a/crates/pacvamp/tests/aur_build.rs b/crates/pacvamp/tests/aur_build.rs index 6d0099fb..c8d3ada5 100644 --- a/crates/pacvamp/tests/aur_build.rs +++ b/crates/pacvamp/tests/aur_build.rs @@ -855,3 +855,97 @@ fn chroot_rejects_the_host_root_and_images_that_link_to_it() { .contains("host root") ); } + +#[test] +fn compare_explains_input_drift_and_checks_artifact_hashes() { + let s = setup(); + no_jail(&s); + assert_eq!(run(&s, &["aur", "approve", "yay", "--force"], "").0, 0); + let build = || { + let (code, out, err) = run(&s, &["aur", "build", "yay", "--json"], ""); + assert_eq!(code, 0, "{err}"); + serde_json::from_str::>(&out) + .unwrap() + .remove(0) + }; + let first = build(); + let second = build(); + assert_eq!( + run( + &s, + &[ + "aur", + "compare", + first.to_str().unwrap(), + second.to_str().unwrap(), + "--json" + ], + "" + ) + .0, + 0 + ); + let receipt_path = second + .parent() + .unwrap() + .parent() + .unwrap() + .join("receipt.json"); + let mut receipt: serde_json::Value = + serde_json::from_slice(&std::fs::read(&receipt_path).unwrap()).unwrap(); + receipt["dependencies"]["pacman"] = "different".into(); + std::fs::write(&receipt_path, serde_json::to_vec(&receipt).unwrap()).unwrap(); + let (code, out, _) = run( + &s, + &[ + "aur", + "compare", + first.to_str().unwrap(), + second.to_str().unwrap(), + "--json", + ], + "", + ); + assert_ne!(code, 0); + assert!(out.contains("dependencies")); + std::fs::write(&second, b"tampered").unwrap(); + assert!( + run( + &s, + &[ + "aur", + "compare", + first.to_str().unwrap(), + second.to_str().unwrap() + ], + "" + ) + .2 + .contains("artifact does not match") + ); +} +#[test] +fn image_fingerprint_covers_contents_and_modes_but_not_runtime_mounts() { + use std::os::unix::fs::PermissionsExt as _; + let root = tempfile::tempdir().unwrap(); + std::fs::create_dir(root.path().join("usr")).unwrap(); + let file = root.path().join("usr/tool"); + std::fs::write(&file, b"original").unwrap(); + let first = pacvamp::aur::receipt::image_digest(root.path()).unwrap(); + std::fs::create_dir(root.path().join("run")).unwrap(); + std::fs::write(root.path().join("run/transient"), b"ignored").unwrap(); + assert_eq!( + first, + pacvamp::aur::receipt::image_digest(root.path()).unwrap() + ); + std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o755)).unwrap(); + assert_ne!( + first, + pacvamp::aur::receipt::image_digest(root.path()).unwrap() + ); + std::fs::write(file, b"changed").unwrap(); + assert_ne!( + first, + pacvamp::aur::receipt::image_digest(root.path()).unwrap() + ); +} diff --git a/docs/build-receipts.md b/docs/build-receipts.md index 2bc8e49d..b5fce2b8 100644 --- a/docs/build-receipts.md +++ b/docs/build-receipts.md @@ -16,3 +16,32 @@ The installed-package inventory describes the available build environment, not proof that every dependency was used. Sources downloaded outside SRCDEST during an explicitly network-enabled build are not captured. The receipt and artifacts remain in the run directory; deleting that directory removes the local evidence. + +## Compare and replay + +`pacvamp aur compare FIRST_ARTIFACT SECOND_ARTIFACT --json` verifies the named +artifacts and compares recipe commits, source inputs, VCS refs, image fingerprints, +dependencies, build settings, and output hashes. Exit 0 means all recorded fields +match; differences produce exit 1 with a structured report. Receipt timestamps and +local storage paths are not compared. + +New builds set SOURCE_DATE_EPOCH to the approved commit's committer timestamp. +Image fingerprints include builder-readable content, symlink targets, ownership, +modification times and permissions. Inaccessible files and directories retain +metadata and an unreadable marker rather than requiring privileged reads. Special +entries such as sockets record metadata only and are never opened; +replaced runtime mounts are excluded. The image is checked again after building. + +`pacvamp aur rebuild ARTIFACT --image ROOT` rechecks recipe approval, requires an +image matching the recorded fingerprint and packages, copies retained sources, +and runs verification and the build offline with the recorded source date. It +compares the new receipt and outputs and retains new artifacts even on mismatch. +A managed image root cannot be overridden. Missing/tampered sources, image drift, +and old receipts without a source date or image fingerprint fail before replay. +Network-enabled reference builds cannot be replayed by this command. + +This is a local experiment, not independent attestation. Matching output hashes +show those two builds produced the same bytes; differing inputs and outputs are +reported without claiming which input caused a difference. Keep the base image or +an explicitly provisioned updated image if future replay is needed: disposable +images are removed after their original build. diff --git a/docs/cli/pacvamp.usage.kdl b/docs/cli/pacvamp.usage.kdl index f9dabcee..722cd48d 100644 --- a/docs/cli/pacvamp.usage.kdl +++ b/docs/cli/pacvamp.usage.kdl @@ -113,6 +113,20 @@ Fetches the package's git history and metadata, evaluates the policy findings ag flag "-h --help" help="Print help" action=help builtin=#true arg } + cmd compare help="Compare verified local receipts, including sources, image and package outputs" { + flag --json + flag "-h --help" help="Print help" action=help builtin=#true + arg + arg + } + cmd rebuild help="Rebuild an approved receipt using retained sources and an identical Arch image" { + flag --image help="Retained image with the same contents as the original build" required=#true { + arg + } + flag --json + flag "-h --help" help="Print help" action=help builtin=#true + arg + } } cmd build-env help="Provision Arch build images with devtools" subcommand_required=#true { flag "-h --help" help="Print help" action=help builtin=#true diff --git a/docs/cli/pacvamp/aur.md b/docs/cli/pacvamp/aur.md index 7473190d..5ffc1a9e 100644 --- a/docs/cli/pacvamp/aur.md +++ b/docs/cli/pacvamp/aur.md @@ -12,6 +12,8 @@ Review, approve, and build AUR packages - [`pacvamp aur approve [FLAGS] `](/cli/pacvamp/aur/approve.md) - [`pacvamp aur build [FLAGS] `](/cli/pacvamp/aur/build.md) +- [`pacvamp aur compare [--json] `](/cli/pacvamp/aur/compare.md) - [`pacvamp aur diff [--commit ] `](/cli/pacvamp/aur/diff.md) +- [`pacvamp aur rebuild <--image > [--json] `](/cli/pacvamp/aur/rebuild.md) - [`pacvamp aur receipt [--json] `](/cli/pacvamp/aur/receipt.md) - [`pacvamp aur review [FLAGS] `](/cli/pacvamp/aur/review.md) diff --git a/docs/cli/pacvamp/aur/compare.md b/docs/cli/pacvamp/aur/compare.md new file mode 100644 index 00000000..36d79a90 --- /dev/null +++ b/docs/cli/pacvamp/aur/compare.md @@ -0,0 +1,14 @@ + +# `pacvamp aur compare` + +- **Usage:** `pacvamp aur compare [--json] ` + +Compare verified local receipts, including sources, image and package outputs + +## Arguments +- **``** +- **``** + +## Flags +- **`--json`** +- **`-h --help`** — Print help diff --git a/docs/cli/pacvamp/aur/rebuild.md b/docs/cli/pacvamp/aur/rebuild.md new file mode 100644 index 00000000..af1e3db4 --- /dev/null +++ b/docs/cli/pacvamp/aur/rebuild.md @@ -0,0 +1,14 @@ + +# `pacvamp aur rebuild` + +- **Usage:** `pacvamp aur rebuild <--image > [--json] ` + +Rebuild an approved receipt using retained sources and an identical Arch image + +## Arguments +- **``** + +## Flags +- **`--image `** — Retained image with the same contents as the original build +- **`--json`** +- **`-h --help`** — Print help diff --git a/docs/cli/pacvamp/index.md b/docs/cli/pacvamp/index.md index edbf01bb..3f78135f 100644 --- a/docs/cli/pacvamp/index.md +++ b/docs/cli/pacvamp/index.md @@ -28,6 +28,8 @@ - [`pacvamp aur diff [--commit ] `](/cli/pacvamp/aur/diff.md) - [`pacvamp aur review [FLAGS] `](/cli/pacvamp/aur/review.md) - [`pacvamp aur receipt [--json] `](/cli/pacvamp/aur/receipt.md) +- [`pacvamp aur compare [--json] `](/cli/pacvamp/aur/compare.md) +- [`pacvamp aur rebuild <--image > [--json] `](/cli/pacvamp/aur/rebuild.md) - [`pacvamp build-env `](/cli/pacvamp/build-env.md) - [`pacvamp build-env init [--package ] `](/cli/pacvamp/build-env/init.md) - [`pacvamp build-env update `](/cli/pacvamp/build-env/update.md) diff --git a/e2e/test_arch_container b/e2e/test_arch_container index e5f6e238..2a0d8631 100644 --- a/e2e/test_arch_container +++ b/e2e/test_arch_container @@ -264,8 +264,9 @@ mkdir -p /srv/pacvamp-image/root/opt/pacvamp-image-tool echo image-only >/srv/pacvamp-image/root/opt/pacvamp-image-tool/marker cat >>/srv/pacvamp-aur/work/PKGBUILD <<'RECIPE' test -f /opt/pacvamp-image-tool/marker || exit 96 -if [[ $BUILDDIR == *.verify ]]; then +if [[ $BUILDDIR == *.verify && ! -f $SRCDEST/network-verified ]]; then curl --noproxy '*' -fsS http://pacvamp-source.test:18763/ >/dev/null || exit 97 + touch "$SRCDEST/network-verified" fi if test -e /usr/share/pacvamp-host-only; then exit 94; fi if touch /usr/share/pacvamp-image-poison; then exit 95; fi @@ -295,6 +296,27 @@ import os images=[r.get("chroot") for r in receipts if r.get("chroot")] assert images and all(p.startswith("/home/pacvamp-builder/.cache/pacvamp/aur/.pacvamp-images/pacvamp-image-") and not os.path.exists(p) for p in images) CHECK +original=$(python - <<'ORIGINAL' +import glob,json,os +for p in glob.glob("/home/pacvamp-builder/.cache/pacvamp/aur/.pacvamp-build/runs/*/receipt.json"): + r=json.load(open(p)) + if r.get("image_sha256"): + print(os.path.join(os.path.dirname(p), "pkgs", next(iter(r["outputs"])))) + break +ORIGINAL +) +(cd /srv/pacvamp-image && runuser -u pacvamp-builder -- env PACVAMP_AUR_RPC_BASE=http://127.0.0.1:18763 PACVAMP_AUR_GIT_BASE=file:///srv/pacvamp-aur /usr/local/bin/pacvamp aur rebuild "$original" --image ./root --json >/tmp/rebuild-comparison.json) +python - <<'COMPARE' +import json +assert json.load(open("/tmp/rebuild-comparison.json"))["identical"] +COMPARE +# Receipt-verified local artifacts are mounted separately from the cloned root. +runuser -u pacvamp-builder -- env PACVAMP_AUR_RPC_BASE=http://127.0.0.1:18763 PACVAMP_AUR_GIT_BASE=file:///srv/pacvamp-aur /usr/local/bin/pacvamp aur build pacvamp-jail-probe --prepare-image --dependency-artifact "$original" -y +if pacman --root /srv/pacvamp-image/root -Q pacvamp-jail-probe >/dev/null 2>&1; then + echo "dependency artifact leaked into base image" >&2 + exit 1 +fi + # Exercise terminal/PTY stdio, including sudo's newline conversion. printf '%s\n' 'Defaults:pacvamp-builder use_pty' 'pacvamp-builder ALL=(root) NOPASSWD: ALL' >/etc/sudoers.d/pacvamp-fixture script -qec 'runuser -u pacvamp-builder -- env PACVAMP_AUR_RPC_BASE=http://127.0.0.1:18763 PACVAMP_AUR_GIT_BASE=file:///srv/pacvamp-aur /usr/local/bin/pacvamp aur build pacvamp-jail-probe --prepare-image -y' /dev/null