60 lines
2.1 KiB
Python
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())
|