| The compiled .pyc files contain time stamp corresponding to the compile time. |
| This prevents binary reproducibility. This patch allows to achieve binary |
| reproducibility by overriding the build time stamp by the value |
| exported via SOURCE_DATE_EPOCH. |
| |
| Upstream-Status: Backport |
| |
| Signed-off-by: Juro Bystricky <juro.bystricky@intel.com> |
| |
| |
| From aeab488630fdb1b56a8d0b0c13fa88706b2afe9b Mon Sep 17 00:00:00 2001 |
| From: "Bernhard M. Wiedemann" <bwiedemann@suse.de> |
| Date: Sat, 25 Feb 2017 06:42:28 +0100 |
| Subject: [PATCH] bpo-29708: support SOURCE_DATE_EPOCH env var in py_compile |
| |
| to allow for reproducible builds of python packages |
| |
| See https://reproducible-builds.org/ for why this is good |
| and https://reproducible-builds.org/specs/source-date-epoch/ |
| for the definition of this variable. |
| |
| Background: |
| In some distributions like openSUSE, binary rpms contain precompiled .pyc files. |
| |
| And packages like amqp or twisted dynamically generate .py files at build time |
| so those have the current time and that timestamp gets embedded |
| into the .pyc file header. |
| When we then adapt file timestamps in rpms to be constant, |
| the timestamp in the .pyc header will no more match |
| the .py timestamp in the filesystem. |
| The software will still work, but it will not use the .pyc file as it should. |
| --- |
| Doc/library/py_compile.rst | 4 ++++ |
| Lib/py_compile.py | 4 ++++ |
| Lib/test/test_py_compile.py | 19 +++++++++++++++++++ |
| 3 files changed, 27 insertions(+) |
| |
| diff --git a/Doc/library/py_compile.rst b/Doc/library/py_compile.rst |
| index 0af8fb1..841f3e8 100644 |
| --- a/Doc/library/py_compile.rst |
| +++ b/Doc/library/py_compile.rst |
| @@ -53,6 +53,10 @@ byte-code cache files in the directory containing the source code. |
| :func:`compile` function. The default of ``-1`` selects the optimization |
| level of the current interpreter. |
| |
| + If the SOURCE_DATE_EPOCH environment variable is set, the .py file mtime |
| + and timestamp entry in .pyc file header, will be limited to this value. |
| + See https://reproducible-builds.org/specs/source-date-epoch/ for more info. |
| + |
| .. versionchanged:: 3.2 |
| Changed default value of *cfile* to be :PEP:`3147`-compliant. Previous |
| default was *file* + ``'c'`` (``'o'`` if optimization was enabled). |
| diff --git a/Lib/py_compile.py b/Lib/py_compile.py |
| index 11c5b50..62dcdc7 100644 |
| --- a/Lib/py_compile.py |
| +++ b/Lib/py_compile.py |
| @@ -137,6 +137,10 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1): |
| except FileExistsError: |
| pass |
| source_stats = loader.path_stats(file) |
| + sde = os.environ.get('SOURCE_DATE_EPOCH') |
| + if sde and source_stats['mtime'] > int(sde): |
| + source_stats['mtime'] = int(sde) |
| + os.utime(file, (source_stats['mtime'], source_stats['mtime'])) |
| bytecode = importlib._bootstrap_external._code_to_bytecode( |
| code, source_stats['mtime'], source_stats['size']) |
| mode = importlib._bootstrap_external._calc_mode(file) |
| diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py |
| index 4a6caa5..3d09963 100644 |
| --- a/Lib/test/test_py_compile.py |
| +++ b/Lib/test/test_py_compile.py |
| @@ -98,6 +98,25 @@ def test_bad_coding(self): |
| self.assertFalse(os.path.exists( |
| importlib.util.cache_from_source(bad_coding))) |
| |
| + def test_source_date_epoch(self): |
| + testtime = 123456789 |
| + orig_sde = os.getenv("SOURCE_DATE_EPOCH") |
| + os.environ["SOURCE_DATE_EPOCH"] = str(testtime) |
| + py_compile.compile(self.source_path, self.pyc_path) |
| + if orig_sde: |
| + os.environ["SOURCE_DATE_EPOCH"] = orig_sde |
| + else: |
| + del os.environ["SOURCE_DATE_EPOCH"] |
| + self.assertTrue(os.path.exists(self.pyc_path)) |
| + self.assertFalse(os.path.exists(self.cache_path)) |
| + statinfo = os.stat(self.source_path) |
| + self.assertEqual(statinfo.st_mtime, testtime) |
| + f = open(self.pyc_path, "rb") |
| + f.read(4) |
| + timebytes = f.read(4) # read timestamp from pyc header |
| + f.close() |
| + self.assertEqual(timebytes, (testtime).to_bytes(4, 'little')) |
| + |
| @unittest.skipIf(sys.flags.optimize > 0, 'test does not work with -O') |
| def test_double_dot_no_clobber(self): |
| # http://bugs.python.org/issue22966 |