git / moonbite-network / scripts/release/make-bundle.py · raw

#!/usr/bin/env python3"""Build a byte-for-byte reproducible release zip.Two runs over the same input files, on any OS, produce an identical archive:  - members are written in the order given (sort them yourself, or pass    --sort) and never pick up directory entries;  - every ZipInfo carries the fixed timestamp 1980-01-01 00:00:00 (the zip    epoch, the same value deploy/miner/build-windows-bundle.py has always    used), so neither the build time nor the filesystem mtime leaks in;  - external_attr is 0755 for executables (*.exe, *.sh, extensionless) and    0644 for everything else, independent of the umask on the build host;  - compression is DEFLATE level 9 via the stdlib, no extra fields, no    comments.Usage:  python -I scripts/release/make-bundle.py --out dist/NAME.zip --root moonbite-miner \      [--sums dist/SHA256SUMS.txt] [--sort] SRC[=ARCNAME] ...Each positional is a source path, optionally followed by =ARCNAME to rename itinside the archive. ARCNAME is placed under --root. --sums appends a"<sha256> *<zip basename>" line (and one per member) to the manifest.Run with `python -I` when the inputs came from a download: it stops Pythonfrom importing a planted module from the inputs' directory."""from __future__ import annotationsimport argparseimport hashlibimport osimport sysimport zipfileZIP_EPOCH = (1980, 1, 1, 0, 0, 0)EXEC_SUFFIXES = (".exe", ".sh", ".bat", ".ps1")def sha256_file(path: str) -> str:    h = hashlib.sha256()    with open(path, "rb") as f:        for chunk in iter(lambda: f.read(1 << 20), b""):            h.update(chunk)    return h.hexdigest()def is_executable(arcname: str) -> bool:    base = os.path.basename(arcname)    return base.endswith(EXEC_SUFFIXES) or "." not in basedef parse_member(spec: str) -> tuple[str, str]:    """'path/to/file' or 'path/to/file=arcname' -> (src, arcname)."""    if "=" in spec:        src, arc = spec.split("=", 1)    else:        src, arc = spec, os.path.basename(spec)    arc = arc.replace("\\", "/").lstrip("/")    if not arc or ".." in arc.split("/"):        sys.exit(f"refusing arcname {arc!r} from {spec!r}")    return src, arcdef build(out: str, root: str, members: list[tuple[str, str]], sort: bool) -> list[tuple[str, str]]:    missing = [s for s, _ in members if not os.path.isfile(s)]    if missing:        sys.exit("missing input files: " + ", ".join(missing))    if sort:        members = sorted(members, key=lambda m: m[1])    arcnames = [a for _, a in members]    if len(set(arcnames)) != len(arcnames):        sys.exit("duplicate arcnames: " + ", ".join(sorted(set(a for a in arcnames if arcnames.count(a) > 1))))    os.makedirs(os.path.dirname(os.path.abspath(out)), exist_ok=True)    root = root.strip("/")    with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as z:        for src, arc in members:            full = f"{root}/{arc}" if root else arc            info = zipfile.ZipInfo(full, date_time=ZIP_EPOCH)            info.compress_type = zipfile.ZIP_DEFLATED            info.create_system = 3  # "Unix": makes external_attr the mode bits            info.external_attr = (0o755 if is_executable(arc) else 0o644) << 16            with open(src, "rb") as f:                z.writestr(info, f.read())    return membersdef main() -> None:    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)    ap.add_argument("--out", required=True, help="output .zip path")    ap.add_argument("--root", default="", help="top-level folder inside the zip (may be empty)")    ap.add_argument("--sums", help="SHA256SUMS.txt to append to (created if missing)")    ap.add_argument("--sort", action="store_true", help="sort members by arcname instead of argument order")    ap.add_argument("members", nargs="+", metavar="SRC[=ARCNAME]")    args = ap.parse_args()    members = build(args.out, args.root, [parse_member(m) for m in args.members], args.sort)    zip_name = os.path.basename(args.out)    zip_hash = sha256_file(args.out)    print(f"{zip_hash}  {zip_name}  ({os.path.getsize(args.out)} bytes)")    for src, arc in members:        print(f"  {sha256_file(src)}  {arc}")    if args.sums:        with open(args.sums, "a", encoding="utf-8", newline="\n") as sf:            sf.write(f"{zip_hash} *{zip_name}\n")        print("appended ->", args.sums)if __name__ == "__main__":    main()