Skip to content

Commit

Permalink
Merge pull request #187 from dalcinl/source-date-epoch
Browse files Browse the repository at this point in the history
Support for SOURCE_DATE_EPOCH (reproducible builds)
  • Loading branch information
HexDecimal authored Nov 14, 2023
2 parents 1c038e8 + 493fc5b commit f639c4d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ Releases

* Fixed `UnicodeDecodeError` when an archive has non-ASCII characters.

* Honor the ``SOURCE_DATE_EPOCH`` environment variable to support
`reproducible builds
<https://reproducible-builds.org/docs/source-date-epoch/>`_.
[#187](https://github.com/matthew-brett/delocate/pull/187)

* [0.10.4] - 2022-12-17

* Dependency paths with ``@rpath``, ``@loader_path`` and ``@executable_path``
Expand Down
18 changes: 18 additions & 0 deletions delocate/tests/env_tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from contextlib import contextmanager
from typing import Iterator

from ..tmpdirs import InTemporaryDirectory

Expand All @@ -24,3 +25,20 @@ def TempDirWithoutEnvVars(*env_vars):
else:
if var in os.environ:
del os.environ[var]


@contextmanager
def _scope_env(**env: str) -> Iterator[None]:
"""Add `env` to the environment and remove them after testing
is complete.
"""
env_save = {key: os.environ.get(key) for key in env}
try:
os.environ.update(env)
yield
finally:
for key, value in env_save.items():
if value is None:
del os.environ[key]
else:
os.environ[key] = value
19 changes: 19 additions & 0 deletions delocate/tests/test_wheelies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import stat
import subprocess
import sys
import zipfile
from glob import glob
from os.path import abspath, basename, exists, isdir
from os.path import join as pjoin
Expand All @@ -30,6 +31,7 @@
zip2dir,
)
from ..wheeltools import InWheel
from .env_tools import _scope_env
from .pytest_tools import assert_equal, assert_false, assert_raises, assert_true
from .test_install_names import DATA_PATH, EXT_LIBS
from .test_tools import ARCH_BOTH, ARCH_M1
Expand Down Expand Up @@ -439,3 +441,20 @@ def test_fix_namespace() -> None:
}

assert delocate_wheel(NAMESPACE_WHEEL, "out.whl") == stray_libs


def test_source_date_epoch() -> None:
with InTemporaryDirectory():
zip2dir(PURE_WHEEL, "package")
for date_time, sde in (
((1980, 1, 1, 0, 0, 0), 42),
((1980, 1, 1, 0, 0, 0), 315532800),
((1980, 1, 1, 0, 0, 2), 315532802),
((2020, 2, 2, 0, 0, 0), 1580601600),
):
with _scope_env(SOURCE_DATE_EPOCH=str(sde)):
dir2zip("package", "package.zip")
with zipfile.ZipFile("package.zip", "r") as zip:
for name in zip.namelist():
member = zip.getinfo(name)
assert_equal(member.date_time, date_time)
38 changes: 38 additions & 0 deletions delocate/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import stat
import subprocess
import time
import warnings
import zipfile
from datetime import datetime
Expand Down Expand Up @@ -844,12 +845,42 @@ def zip2dir(
os.utime(extracted_path, (modified_time, modified_time))


_ZIP_TIMESTAMP_MIN = 315532800 # 1980-01-01 00:00:00 UTC
_DateTuple = Tuple[int, int, int, int, int, int]


def _get_zip_datetime(
date_time: Optional[_DateTuple] = None,
) -> Optional[_DateTuple]:
"""Utility function to support reproducible builds
https://reproducible-builds.org/docs/source-date-epoch/
Parameters
----------
date_time : tuple of int, optional
Datetime tuple ``(Y, m, d, H, M, S)``.
Returns
-------
zip_date_time : tuple of int, optional
Datetime tuple corresponding to the ``SOURCE_DATE_EPOCH``
environment variable if set, otherwise the input `date_time`.
"""
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH")
if source_date_epoch is not None:
timestamp = max(int(source_date_epoch), _ZIP_TIMESTAMP_MIN)
date_time = time.gmtime(timestamp)[0:6]
return date_time


def dir2zip(
in_dir: str | PathLike[str],
zip_fname: str | PathLike[str],
*,
compression: int = zipfile.ZIP_DEFLATED,
compress_level: int = -1,
date_time: Optional[_DateTuple] = None,
) -> None:
"""Make a zip file `zip_fname` with contents of directory `in_dir`
Expand All @@ -867,7 +898,10 @@ def dir2zip(
The zipfile compression type used.
compress_level : int, optional, keyword-only
The compression level used for this archive.
date_time : tuple of int, optional, keyword-only
Datetime tuple ``(Y, m, d, H, M, S)`` for all recorded entries.
"""
date_time = _get_zip_datetime(date_time)
with zipfile.ZipFile(
zip_fname, "w", compression=compression, compresslevel=compress_level
) as zip:
Expand All @@ -876,11 +910,15 @@ def dir2zip(
dir_path = Path(root, dir)
out_dir_name = str(dir_path.relative_to(in_dir)) + "/"
zip_info = zipfile.ZipInfo.from_file(dir_path, out_dir_name)
if date_time is not None:
zip_info.date_time = date_time
zip.writestr(zip_info, b"")
for file in files:
file_path = Path(root, file)
out_file_path = file_path.relative_to(in_dir)
zip_info = zipfile.ZipInfo.from_file(file_path, out_file_path)
if date_time is not None:
zip_info.date_time = date_time
zip.writestr(
zip_info,
file_path.read_bytes(),
Expand Down

0 comments on commit f639c4d

Please sign in to comment.