Skip to content

Commit 4468c3f

Browse files
committed
xtask: Add validate-composefs-digest for debugging mtime issues
Add a command to build a sealed image and compare composefs dumpfiles between the build-time view (mounted filesystem) and install-time view (OCI tar layer processing). This helps debug mtime and metadata discrepancies that cause sealed boot failures. Usage: cargo xtask validate-composefs-digest cargo xtask validate-composefs-digest --no-cache # force clean build Output files are written to target/validate-digest/: - build.dumpfile: from mounted filesystem (what seal-uki sees) - storage.dumpfile: from containers-storage (what bootc upgrade sees) - diff.txt: differences if any Signed-off-by: Colin Walters <walters@verbum.org> Assisted-by: OpenCode (Opus 4.5) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 52a83ed commit 4468c3f

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

crates/xtask/src/xtask.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ enum Commands {
5454
TmtProvision(TmtProvisionArgs),
5555
/// Check build system properties (e.g., reproducible builds)
5656
CheckBuildsys,
57+
/// Validate composefs digests match between build-time and install-time views
58+
ValidateComposefsDigest(ValidateComposefsDigestArgs),
59+
}
60+
61+
/// Arguments for validate-composefs-digest command
62+
#[derive(Debug, Args)]
63+
pub(crate) struct ValidateComposefsDigestArgs {
64+
/// Force a clean build (no podman cache)
65+
#[arg(long)]
66+
pub(crate) no_cache: bool,
67+
68+
/// Base image to use
69+
#[arg(long, default_value = "quay.io/centos-bootc/centos-bootc:stream10")]
70+
pub(crate) base: String,
5771
}
5872

5973
/// Arguments for run-tmt command
@@ -139,6 +153,7 @@ fn try_main() -> Result<()> {
139153
Commands::RunTmt(args) => tmt::run_tmt(&sh, &args),
140154
Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args),
141155
Commands::CheckBuildsys => buildsys::check_buildsys(&sh, "Dockerfile".into()),
156+
Commands::ValidateComposefsDigest(args) => validate_composefs_digest(&sh, &args),
142157
}
143158
}
144159

@@ -406,3 +421,110 @@ fn update_generated(sh: &Shell) -> Result<()> {
406421

407422
Ok(())
408423
}
424+
425+
/// Validate that composefs digests match between build-time and install-time views.
426+
///
427+
/// This builds a sealed image and compares dumpfiles generated from:
428+
/// 1. The mounted filesystem (what seal-uki sees at build time)
429+
/// 2. The OCI tar layers in containers-storage (what bootc upgrade sees)
430+
///
431+
/// This helps debug mtime and metadata discrepancies that cause sealed boot failures.
432+
#[context("Validating composefs digest")]
433+
fn validate_composefs_digest(sh: &Shell, args: &ValidateComposefsDigestArgs) -> Result<()> {
434+
let variant = "composefs-sealeduki-sdboot";
435+
let out_dir = Utf8Path::new("target/validate-digest");
436+
437+
// Ensure packages are built
438+
if !Utf8Path::new("target/packages").exists() {
439+
anyhow::bail!("target/packages not found. Run 'just package' first.");
440+
}
441+
442+
// Ensure secureboot keys exist
443+
if !Utf8Path::new("target/test-secureboot/db.key").exists() {
444+
cmd!(sh, "./hack/generate-secureboot-keys").run()?;
445+
}
446+
447+
std::fs::create_dir_all(out_dir)?;
448+
let pkg_path = std::fs::canonicalize("target/packages")?;
449+
let out_dir_abs = std::fs::canonicalize(out_dir)?;
450+
451+
// Build the base-penultimate stage
452+
println!("=== Building base-penultimate stage ===");
453+
let no_cache = args.no_cache.then_some("--no-cache");
454+
let base = &args.base;
455+
let pkg_vol = format!("{}:/run/packages:ro,z", pkg_path.display());
456+
cmd!(
457+
sh,
458+
"podman build {no_cache...}
459+
-v {pkg_vol}
460+
--build-arg=base={base}
461+
--build-arg=variant={variant}
462+
--cap-add=all
463+
--security-opt=label=type:container_runtime_t
464+
--device=/dev/fuse
465+
--secret=id=secureboot_key,src=target/test-secureboot/db.key
466+
--secret=id=secureboot_cert,src=target/test-secureboot/db.crt
467+
--target=base-penultimate
468+
-t localhost/bootc-penultimate
469+
."
470+
)
471+
.run()?;
472+
473+
// Generate dumpfile from mounted filesystem (build-time view)
474+
println!("=== Generating build-time dumpfile (from mounted filesystem) ===");
475+
let build_dumpfile = out_dir_abs.join("build.dumpfile");
476+
let out_vol = format!("{}:/out:z", out_dir_abs.display());
477+
cmd!(
478+
sh,
479+
"podman run --rm --privileged
480+
-v {out_vol}
481+
--mount=type=image,source=localhost/bootc-penultimate,target=/target
482+
localhost/bootc-penultimate
483+
bootc container compute-composefs-digest /target
484+
--write-dumpfile-to /out/build.dumpfile"
485+
)
486+
.run()?;
487+
println!("Build-time dumpfile: {}", build_dumpfile.display());
488+
489+
// Generate dumpfile from containers-storage (install-time view)
490+
println!("=== Generating install-time dumpfile (from containers-storage) ===");
491+
let format_arg = "{{.Store.GraphRoot}}";
492+
let graphroot = cmd!(sh, "podman system info -f {format_arg}").read()?;
493+
let graphroot = graphroot.trim();
494+
let storage_dumpfile = out_dir_abs.join("storage.dumpfile");
495+
let storage_vol = format!("{graphroot}:/run/host-container-storage:ro");
496+
cmd!(
497+
sh,
498+
"podman run --rm --privileged --security-opt=label=disable
499+
-v {out_vol}
500+
-v {storage_vol}
501+
-v /sys:/sys:ro
502+
--tmpfs=/var
503+
localhost/bootc-penultimate
504+
bootc container compute-composefs-digest-from-storage
505+
--write-dumpfile-to /out/storage.dumpfile"
506+
)
507+
.run()?;
508+
println!("Install-time dumpfile: {}", storage_dumpfile.display());
509+
510+
// Compare dumpfiles
511+
println!("=== Comparing dumpfiles ===");
512+
let diff_result = cmd!(sh, "diff -u {build_dumpfile} {storage_dumpfile}")
513+
.ignore_status()
514+
.output()?;
515+
516+
if diff_result.status.success() {
517+
println!("SUCCESS: Dumpfiles match!");
518+
Ok(())
519+
} else {
520+
let diff_file = out_dir_abs.join("diff.txt");
521+
std::fs::write(&diff_file, &diff_result.stdout)?;
522+
println!("MISMATCH: Dumpfiles differ! See {}", diff_file.display());
523+
println!("First 50 lines of diff:");
524+
let stdout = String::from_utf8_lossy(&diff_result.stdout);
525+
for line in stdout.lines().take(50) {
526+
println!("{line}");
527+
}
528+
anyhow::bail!("Composefs digest mismatch");
529+
}
530+
}

usr/bin/bootc

12.1 MB
Binary file not shown.

0 commit comments

Comments
 (0)