#!/usr/bin/env python3 """Build a reproducible, verifiable Stackchain Dashboard release bundle.""" from __future__ import annotations import argparse import gzip import hashlib import io import json import tarfile from pathlib import Path RUNTIME_DIRECTORIES = ("docs", "frontend", "src") RUNTIME_FILES = ("README.md", "requirements.txt") def sha256(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def runtime_files(root: Path) -> list[Path]: files = [root / name for name in RUNTIME_FILES if (root / name).is_file()] for name in RUNTIME_DIRECTORIES: directory = root / name if directory.is_dir(): files.extend(path for path in directory.rglob("*") if path.is_file()) return sorted(files, key=lambda path: path.relative_to(root).as_posix()) def manifest_for(root: Path, files: list[Path], commit: str) -> dict: return { "schema_version": 1, "commit": commit, "files": { path.relative_to(root).as_posix(): { "sha256": sha256(path.read_bytes()), "size": path.stat().st_size, } for path in files }, } def json_bytes(payload: dict) -> bytes: return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode() def tar_info(name: str, size: int, epoch: int, executable: bool = False) -> tarfile.TarInfo: info = tarfile.TarInfo(name) info.size = size info.mtime = epoch info.mode = 0o755 if executable else 0o644 info.uid = info.gid = 0 info.uname = info.gname = "root" return info def build(root: Path, output_dir: Path, commit: str, epoch: int) -> tuple[Path, Path, Path]: root = root.resolve() files = runtime_files(root) missing = [name for name in RUNTIME_FILES if not (root / name).is_file()] if missing or not (root / "src").is_dir() or not (root / "frontend").is_dir(): raise ValueError("release root is missing required runtime files") embedded_manifest = manifest_for(root, files, commit) archive_members: dict[str, tuple[bytes, bool]] = { path.relative_to(root).as_posix(): ( path.read_bytes(), bool(path.stat().st_mode & 0o111), ) for path in files } archive_members["release-manifest.json"] = (json_bytes(embedded_manifest), False) output_dir.mkdir(parents=True, exist_ok=True) stem = f"stackchain-dashboard-{commit[:12]}" archive_path = output_dir / f"{stem}.tar.gz" with archive_path.open("wb") as raw: with gzip.GzipFile(fileobj=raw, mode="wb", filename="", mtime=epoch) as compressed: with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive: for name in sorted(archive_members): data, executable = archive_members[name] archive.addfile(tar_info(name, len(data), epoch, executable), io.BytesIO(data)) archive_data = archive_path.read_bytes() digest = sha256(archive_data) public_manifest = { **embedded_manifest, "artifact": { "name": archive_path.name, "sha256": digest, "size": len(archive_data), }, } manifest_path = output_dir / f"{stem}.manifest.json" checksum_path = output_dir / f"{stem}.sha256" manifest_path.write_bytes(json_bytes(public_manifest)) checksum_path.write_text(f"{digest} {archive_path.name}\n") return archive_path, manifest_path, checksum_path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path.cwd()) parser.add_argument("--output-dir", type=Path, default=Path("dist")) parser.add_argument("--commit", required=True) parser.add_argument("--source-date-epoch", type=int, required=True) return parser.parse_args() def main() -> None: args = parse_args() for path in build(args.root, args.output_dir, args.commit, args.source_date_epoch): print(path) if __name__ == "__main__": main()