#!/usr/bin/env python3 """Create a byte-reproducible tar.gz from one directory tree.""" from __future__ import annotations import argparse import gzip import os import tarfile from pathlib import Path def create_archive(source: Path, output: Path, source_date_epoch: int) -> None: source = source.resolve() output.parent.mkdir(parents=True, exist_ok=True) paths = [source, *sorted(source.rglob("*"), key=lambda path: path.relative_to(source).as_posix())] with output.open("wb") as raw: with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=source_date_epoch) as compressed: with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive: for path in paths: arcname = source.name if path == source else f"{source.name}/{path.relative_to(source).as_posix()}" info = archive.gettarinfo(str(path), arcname=arcname) info.uid = 0 info.gid = 0 info.uname = "root" info.gname = "root" info.mtime = source_date_epoch info.pax_headers = {} if info.isdir(): info.mode = 0o755 archive.addfile(info) elif info.isfile(): executable = bool(os.stat(path).st_mode & 0o111) info.mode = 0o755 if executable else 0o644 with path.open("rb") as handle: archive.addfile(info, handle) elif info.issym(): archive.addfile(info) else: raise ValueError(f"unsupported archive entry: {path}") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--source-date-epoch", type=int, required=True) args = parser.parse_args() create_archive(args.source, args.output, args.source_date_epoch) return 0 if __name__ == "__main__": raise SystemExit(main())