reproducible_archive.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. #!/usr/bin/env python3
  2. """Create a byte-reproducible tar.gz from one directory tree."""
  3. from __future__ import annotations
  4. import argparse
  5. import gzip
  6. import os
  7. import tarfile
  8. from pathlib import Path
  9. def create_archive(source: Path, output: Path, source_date_epoch: int) -> None:
  10. source = source.resolve()
  11. output.parent.mkdir(parents=True, exist_ok=True)
  12. paths = [source, *sorted(source.rglob("*"), key=lambda path: path.relative_to(source).as_posix())]
  13. with output.open("wb") as raw:
  14. with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=source_date_epoch) as compressed:
  15. with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive:
  16. for path in paths:
  17. arcname = source.name if path == source else f"{source.name}/{path.relative_to(source).as_posix()}"
  18. info = archive.gettarinfo(str(path), arcname=arcname)
  19. info.uid = 0
  20. info.gid = 0
  21. info.uname = "root"
  22. info.gname = "root"
  23. info.mtime = source_date_epoch
  24. info.pax_headers = {}
  25. if info.isdir():
  26. info.mode = 0o755
  27. archive.addfile(info)
  28. elif info.isfile():
  29. executable = bool(os.stat(path).st_mode & 0o111)
  30. info.mode = 0o755 if executable else 0o644
  31. with path.open("rb") as handle:
  32. archive.addfile(info, handle)
  33. elif info.issym():
  34. archive.addfile(info)
  35. else:
  36. raise ValueError(f"unsupported archive entry: {path}")
  37. def main() -> int:
  38. parser = argparse.ArgumentParser(description=__doc__)
  39. parser.add_argument("--source", type=Path, required=True)
  40. parser.add_argument("--output", type=Path, required=True)
  41. parser.add_argument("--source-date-epoch", type=int, required=True)
  42. args = parser.parse_args()
  43. create_archive(args.source, args.output, args.source_date_epoch)
  44. return 0
  45. if __name__ == "__main__":
  46. raise SystemExit(main())