Updater 3.4 – Lifecycle Hardening #28
@@ -0,0 +1,59 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user