Artifact 94158fe8e853f8e56f6b50cbefacf3b29fd29c17207317cecce55d7ce99b5f58:
- Executable file
scripts/assemble-release-manifest.py
— part of check-in
[9033c13f21]
at
2026-09-17 17:04:25
on branch trunk
— Remove issue-tracker numbers from source and documents
The tracker is disposable and does not live with the source, so a number is a dangling pointer. Every zit#NN and #NN reference in code comments, test names, help text, smoke labels, identifiers, release notes and errata is replaced by enough context to stand alone, or by a short plain statement of the problem. No behaviour changed. (user: dmestas, size: 5689) [annotate] [blame] [check-ins using]
#!/usr/bin/env python3 """Assemble release-manifest.json from a publication candidate. The release workflow emits per-artifact `*.metadata.json` fragments and a SHA256SUMS file, but not the combined manifest that gets published beside the tarballs. This assembles it, so the file that is the authoritative record of a release is produced by something reviewable rather than by hand. `source_tree_sha256` is sha256 over "<file-sha256> <relative-path>\\n" lines SORTED BY LINE — that is, by hash, since the hash starts the line. It is the `sha256sum <files> | sort | sha256sum` idiom. Verified bit-for-bit against the published 0.1.4 manifest. (0.1.3's value does not reproduce, and its own file count is wrong by 11 against its published tarball, so 0.1.4 is the reference.) Usage: assemble-manifest.py CANDIDATE_DIR VERSION FOSSIL_CHECKIN [--date YYYY-MM-DD] """ import argparse import hashlib import json import pathlib import re import sys import tarfile import tempfile FOSSIL_VERSION = "2.28" ZIG_VERSION = "0.16.0" OPTIMIZATION = "ReleaseFast" SCHEMA_VERSION = 1 # Per-target qualification, following 0.1.4's wording. The last entry differs # per target because it names the *independent* execution that proves the # packaged binary runs below the CPU that built it. QUALIFICATION = { "linux-aarch64": [ "format", "portable ReleaseFast build", "unit", "e2e", "dst seed 1 x1000", "27 fuzz targets seed 1 x100", "Fossil S1-S8 x10", "packaged binary smoke on independent native Linux ARM64 host", ], "linux-x86_64": [ "format", "portable ReleaseFast build", "unit", "e2e", "dst seed 1 x1000", "27 fuzz targets seed 1 x100", "Fossil S1-S8 x10", "packaged binary smoke under QEMU qemu64 on GitHub-hosted Linux x86_64", ], "macos-aarch64": [ "format", "portable ReleaseFast build", "unit", "e2e", "dst seed 1 x1000", "27 fuzz targets seed 1 x100", "Fossil S1-S8 x3", "packaged binary smoke on macOS ARM64", ], } ARTIFACT_KEYS = ("binary_sha256", "bytes", "name", "qualification", "sha256", "target") def sha256_file(path: pathlib.Path) -> str: h = hashlib.sha256() with path.open("rb") as fh: for chunk in iter(lambda: fh.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def source_tree_digest(tarball: pathlib.Path) -> tuple[str, int]: """Return (source_tree_sha256, file_count) for a source tarball.""" with tempfile.TemporaryDirectory() as tmp: with tarfile.open(tarball) as tf: tf.extractall(tmp) root = pathlib.Path(tmp) top = next(root.iterdir()) files = [p for p in top.rglob("*") if p.is_file()] lines = sorted( f"{sha256_file(p)} {p.relative_to(top)}\n" for p in files ) return hashlib.sha256("".join(lines).encode()).hexdigest(), len(files) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("candidate") ap.add_argument("version") ap.add_argument("checkin") ap.add_argument("--date", required=True) args = ap.parse_args() cand = pathlib.Path(args.candidate) version, checkin, tag = args.version, args.checkin, f"version-{args.version}" artifacts = [] for frag in sorted(cand.glob("*.metadata.json")): data = json.loads(frag.read_text()) artifacts.append({k: data[k] for k in ARTIFACT_KEYS}) artifacts.sort(key=lambda a: a["name"]) if not artifacts: print("no *.metadata.json fragments found", file=sys.stderr) return 1 src = cand / f"zit-{version}-source.tar.gz" if not src.is_file(): print(f"missing source tarball: {src}", file=sys.stderr) return 1 tree_sha, file_count = source_tree_digest(src) src_sha, src_bytes = sha256_file(src), src.stat().st_size # Every published archive must actually verify against its own recorded # digest before it is described as qualified. for a in artifacts: p = cand / a["name"] if not p.is_file(): print(f"missing archive named in a fragment: {p}", file=sys.stderr) return 1 actual = sha256_file(p) if actual != a["sha256"]: print(f"digest mismatch for {a['name']}: {actual} != {a['sha256']}", file=sys.stderr) return 1 manifest = { "artifacts": artifacts, "omitted_targets": [], "qualification": QUALIFICATION, "qualification_source": { "bytes": src_bytes, "files": file_count, "name": src.name, "sha256": src_sha, "source_tree_sha256": tree_sha, }, "release": { "date": args.date, "fossil_check_in": checkin, "fossil_tag": tag, "optimization": OPTIMIZATION, "version": version, "zig_version": ZIG_VERSION, }, "schema_version": SCHEMA_VERSION, "source": { "bytes": src_bytes, "files": file_count, "fossil_check_in": checkin, "fossil_tag": tag, "matches_qualified": True, "name": src.name, "qualification": ( f"generated by Fossil {FOSSIL_VERSION} from the exact release " "check-in; every target's build and qualification battery ran " "against this exact archive" ), "sha256": src_sha, "source_tree_sha256": tree_sha, "target": "source", }, } print(json.dumps(manifest, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())