Files
vendoo/tools/build-deterministic-zip.py
T

60 lines
2.1 KiB
Python

#!/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())