Skip to content
zit Artifact [8d7c51fb27]
Login

Artifact [8d7c51fb27]

Artifact 8d7c51fb27138f8322d1ef38f66c87f653f0f53cc9bc3fdbc6abbb9e06d6d49c:


#!/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.17.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", "32 fuzz targets seed 1 x100", "Fossil S1-S19 x10",
        "packaged binary run on the qualifying native Linux ARM64 host (hetzner-vps)",
    ],
    "linux-x86_64": [
        "format", "portable ReleaseFast build", "unit", "e2e",
        "dst seed 1 x1000", "32 fuzz targets seed 1 x100", "Fossil S1-S19 x10",
        "packaged binary run on the qualifying native Linux x86_64 host (hetzner-big-eu); no QEMU low-CPU check",
    ],
    "macos-aarch64": [
        "format", "portable ReleaseFast build", "unit", "e2e",
        "dst seed 1 x1000", "32 fuzz targets seed 1 x100", "Fossil S1-S19 x3",
        "packaged binary run on the qualifying macOS ARM64 host (mac-mini)",
    ],
}

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())