#!/usr/bin/env python3 """Build a service's image twice from a clean context and compare the results. Distinguishes full image-ID (byte-for-byte, including embedded metadata like history timestamps) reproducibility from layer-content (diffID) reproducibility, per the WP-200 requirement not to claim byte-identical results without proof. """ from __future__ import annotations import argparse import json import subprocess from datetime import UTC, datetime from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] def _run(command: list[str]) -> str: completed = subprocess.run( # noqa: S603 command, cwd=ROOT, capture_output=True, text=True, check=True ) return completed.stdout.strip() def _build(dockerfile: Path, tag: str, build_args: dict[str, str]) -> None: command = [ "docker", "buildx", "build", "--no-cache", "--output", "type=docker,rewrite-timestamp=true,unpack=false", "-f", str(dockerfile), "-t", tag, ] for name, value in build_args.items(): command.extend(["--build-arg", f"{name}={value}"]) command.append(str(ROOT)) subprocess.run(command, cwd=ROOT, check=True) # noqa: S603 def _image_id(tag: str) -> str: return _run(["docker", "inspect", "--format", "{{.Id}}", tag]) def _layer_diff_ids(tag: str) -> list[str]: raw = _run(["docker", "inspect", "--format", "{{json .RootFS.Layers}}", tag]) return list(json.loads(raw)) def compare_two_builds( dockerfile: Path, build_args: dict[str, str], tag_a: str, tag_b: str ) -> dict[str, Any]: _build(dockerfile, tag_a, build_args) _build(dockerfile, tag_b, build_args) image_id_a, image_id_b = _image_id(tag_a), _image_id(tag_b) layers_a, layers_b = _layer_diff_ids(tag_a), _layer_diff_ids(tag_b) return { "dockerfile": str(dockerfile.relative_to(ROOT)), "buildArgs": {key: value for key, value in build_args.items() if key != "SOURCE_DATE_EPOCH"} | {"SOURCE_DATE_EPOCH": build_args.get("SOURCE_DATE_EPOCH", "")}, "imageIdA": image_id_a, "imageIdB": image_id_b, "imageIdMatch": image_id_a == image_id_b, "layerDiffIdsA": layers_a, "layerDiffIdsB": layers_b, "layerDiffIdsMatch": layers_a == layers_b, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--dockerfile", type=Path, required=True) parser.add_argument("--service-name", required=False) parser.add_argument("--git-revision", required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() args.dockerfile = args.dockerfile.resolve() epoch = _run(["git", "show", "-s", "--format=%ct", args.git_revision]) created = _run(["git", "show", "-s", "--format=%cI", args.git_revision]) build_args = { "GIT_REVISION": args.git_revision, "SOURCE_DATE_EPOCH": epoch, "BUILD_DATE": created, } if args.service_name: build_args["SERVICE_NAME"] = args.service_name stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") tag_a = f"local/wp200-repro-a:{stamp}" tag_b = f"local/wp200-repro-b:{stamp}" report = compare_two_builds(args.dockerfile, build_args, tag_a, tag_b) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(json.dumps(report, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())