#!/usr/bin/env python3 """Build a byte-reproducible ZIP archive from a release directory.""" from __future__ import annotations import argparse import os from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0) def iter_files(source: Path): for path in sorted(source.rglob("*"), key=lambda item: item.as_posix()): if path.is_file() and not path.is_symlink(): yield path def build_archive(source: Path, destination: Path) -> None: source = source.resolve(strict=True) if not source.is_dir(): raise ValueError(f"Source is not a directory: {source}") destination = destination.resolve() if source == destination or source in destination.parents: raise ValueError("Destination must be outside the source directory.") destination.parent.mkdir(parents=True, exist_ok=True) temporary = destination.with_suffix(destination.suffix + ".tmp") temporary.unlink(missing_ok=True) try: with ZipFile(temporary, "w", compression=ZIP_DEFLATED, compresslevel=9) as archive: for file_path in iter_files(source): relative = file_path.relative_to(source.parent).as_posix() info = ZipInfo(relative, FIXED_TIMESTAMP) info.compress_type = ZIP_DEFLATED info.create_system = 3 mode = file_path.stat().st_mode & 0o777 info.external_attr = (mode & 0xFFFF) << 16 info.flag_bits |= 0x800 archive.writestr(info, file_path.read_bytes(), compress_type=ZIP_DEFLATED, compresslevel=9) os.replace(temporary, destination) finally: temporary.unlink(missing_ok=True) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("source", type=Path) parser.add_argument("destination", type=Path) args = parser.parse_args() build_archive(args.source, args.destination) print(f"Deterministic ZIP created: {args.destination}") return 0 if __name__ == "__main__": raise SystemExit(main())