Files
RustProxy/make_zip.py
T
2026-10-07 22:22:54 +02:00

113 lines
4.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Create a deterministic complete source-only release without Git or live state."""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import os
from pathlib import Path
import stat
import zipfile
ROOT = Path(__file__).resolve().parent
MANAGED = [
'src', 'tests', 'scripts', 'tools', 'integrations', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'grafana',
'.github', '.cargo', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'UPDATE_CHECK.toml',
'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'README.md', 'ROADMAP.md',
'CHANGELOG.md', 'AGENTS.md', '.env.example', '.gitignore', '.dockerignore',
'Dockerfile', 'make_zip.py',
]
EXCLUDED = {'__pycache__', '.DS_Store', 'node_modules', 'target', 'data', '.git', '.upgrade-backup'}
EXECUTABLE_ROOT = {'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'make_zip.py'}
ZIP_MIN_EPOCH = 315532800 # 1980-01-01T00:00:00Z
ZIP_MAX_EPOCH = 4354819198 # 2107-12-31T23:59:58Z
DEFAULT_SOURCE_DATE_EPOCH = 946684800 # 2000-01-01T00:00:00Z
def files() -> list[Path]:
result: list[Path] = []
for name in MANAGED:
path = ROOT / name
if not path.exists():
continue
if path.is_symlink():
raise ValueError(f'Refusing symlink in source package: {name}')
paths = path.rglob('*') if path.is_dir() else [path]
for file in paths:
rel = file.relative_to(ROOT)
if any(part in EXCLUDED for part in rel.parts) or file.suffix in ('.pyc', '.pyo', '.zip'):
continue
# Never ship local environment files from recursively managed trees.
# Explicit templates such as .env.example remain safe because they are
# named in MANAGED and do not have the exact basename `.env`.
if rel.name == '.env':
continue
if rel.as_posix() == 'docker-agent/docker-agent':
continue
if file.is_symlink():
raise ValueError(f'Refusing symlink in source package: {rel}')
if file.is_file():
result.append(file)
return sorted(set(result), key=lambda path: path.relative_to(ROOT).as_posix())
def release_datetime() -> tuple[int, int, int, int, int, int]:
raw = os.environ.get('SOURCE_DATE_EPOCH')
try:
epoch = int(raw) if raw is not None else DEFAULT_SOURCE_DATE_EPOCH
except ValueError as error:
raise ValueError('SOURCE_DATE_EPOCH must be an integer Unix timestamp') from error
epoch = min(max(epoch, ZIP_MIN_EPOCH), ZIP_MAX_EPOCH)
stamp = dt.datetime.fromtimestamp(epoch, tz=dt.timezone.utc).replace(microsecond=0)
# The classic ZIP timestamp stores seconds in two-second increments.
stamp = stamp.replace(second=stamp.second - stamp.second % 2)
return stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second
def deterministic_mode(rel: Path) -> int:
executable = rel.as_posix() in EXECUTABLE_ROOT or (
rel.parts and rel.parts[0] in {'scripts', 'tools', 'integrations'} and rel.suffix in {'.py', '.sh'}
) or (
rel.parts and rel.parts[0] == 'nagios' and (rel.suffix in {'.py', '.sh'} or rel.name.startswith('check_'))
) or (
rel.as_posix() == 'docker-agent/install.sh'
)
return 0o755 if executable else 0o644
def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[int, int, int, int, int, int]) -> None:
info = zipfile.ZipInfo(f'RustProxy/{rel.as_posix()}', date_time=timestamp)
info.create_system = 3
info.external_attr = (stat.S_IFREG | deterministic_mode(rel)) << 16
info.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(info, data, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.27.3.zip')
output = parser.parse_args().output.resolve()
paths = files()
checksum = ROOT / 'SHA256SUMS'
if output in paths or output == checksum:
raise SystemExit('Output must not replace a source file.')
sums = ''.join(
f'{hashlib.sha256(path.read_bytes()).hexdigest()} {path.relative_to(ROOT).as_posix()}\n'
for path in paths
)
#checksum.write_text(sums, encoding='utf-8', newline='\n')
checksum.write_text(sums, encoding='utf-8')
timestamp = release_datetime()
output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output, 'w') as archive:
for path in paths:
add_file(archive, path.relative_to(ROOT), path.read_bytes(), timestamp)
add_file(archive, Path('SHA256SUMS'), checksum.read_bytes(), timestamp)
print(f'Created {output}; {len(paths) + 1} source files; deterministic SHA256SUMS refreshed.')
if __name__ == '__main__':
main()