mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Create a portable release ZIP from a staged directory.
|
|
|
|
PowerShell's Compress-Archive writes Windows backslashes into entry names.
|
|
They are tolerated by Explorer, but are not valid ZIP path separators and
|
|
break several Unix extractors. This helper always emits sorted POSIX names.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import zipfile
|
|
|
|
|
|
def _zip_datetime(path: Path) -> tuple[int, int, int, int, int, int]:
|
|
stamp = dt.datetime.fromtimestamp(path.stat().st_mtime)
|
|
if stamp.year < 1980:
|
|
stamp = stamp.replace(year=1980, month=1, day=1, hour=0, minute=0, second=0)
|
|
return stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second
|
|
|
|
|
|
def create_release_zip(source: Path, output: Path) -> None:
|
|
source = source.resolve()
|
|
output = output.resolve()
|
|
if not source.is_dir():
|
|
raise ValueError(f"source is not a directory: {source}")
|
|
try:
|
|
output.relative_to(source)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise ValueError("output ZIP must not be inside the staged source tree")
|
|
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, temporary_name = tempfile.mkstemp(
|
|
prefix=f".{output.name}.", suffix=".tmp", dir=output.parent
|
|
)
|
|
os.close(fd)
|
|
temporary = Path(temporary_name)
|
|
try:
|
|
with zipfile.ZipFile(
|
|
temporary, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
|
) as archive:
|
|
files = sorted(
|
|
(path for path in source.rglob("*") if path.is_file()),
|
|
key=lambda path: path.relative_to(source).as_posix(),
|
|
)
|
|
for path in files:
|
|
entry_name = path.relative_to(source).as_posix()
|
|
if "\\" in entry_name or entry_name.startswith("/"):
|
|
raise ValueError(f"invalid ZIP entry name: {entry_name!r}")
|
|
info = zipfile.ZipInfo(entry_name, _zip_datetime(path))
|
|
info.compress_type = zipfile.ZIP_DEFLATED
|
|
info.create_system = 3
|
|
info.external_attr = (path.stat().st_mode & 0xFFFF) << 16
|
|
with path.open("rb") as source_file, archive.open(info, "w") as target:
|
|
while chunk := source_file.read(1024 * 1024):
|
|
target.write(chunk)
|
|
os.replace(temporary, output)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--source", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
create_release_zip(args.source, args.output)
|
|
print(args.output.resolve())
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|