Files
ITWorx_MCP_Hub-Public/scripts/check_build_reproducibility.py

105 lines
3.5 KiB
Python

#!/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())