@@ -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+ }
0 commit comments