v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/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', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'debian', '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
|
||||
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] == 'scripts' and rel.suffix in {'.py', '.sh'}
|
||||
) or (
|
||||
rel.parts and rel.parts[0] == 'debian' and (rel.suffix in {'.sh'} or rel.name in {'rules', 'rustproxy.postinst'})
|
||||
) 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.24.5.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()
|
||||
Reference in new issue
Block a user