Skip to content

Commit 28364e0

Browse files
committed
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.
1 parent f80c0ca commit 28364e0

File tree

4 files changed

+26
-0
lines changed

4 files changed

+26
-0
lines changed

Doc/library/py_compile.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ byte-code cache files in the directory containing the source code.
5757
enum and controls how the generated ``.pyc`` files are invalidated at
5858
runtime.
5959

60+
If the SOURCE_DATE_EPOCH environment variable is set, the timestamp entry in
61+
the .pyc file header, will be limited to this value.
62+
See https://reproducible-builds.org/specs/source-date-epoch/ for more info.
63+
6064
.. versionchanged:: 3.2
6165
Changed default value of *cfile* to be :PEP:`3147`-compliant. Previous
6266
default was *file* + ``'c'`` (``'o'`` if optimization was enabled).

Lib/py_compile.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1,
147147
pass
148148
if invalidation_mode == PycInvalidationMode.TIMESTAMP:
149149
source_stats = loader.path_stats(file)
150+
source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
151+
if source_date_epoch:
152+
try:
153+
source_date_epoch = int(source_date_epoch)
154+
except ValueError:
155+
raise ValueError("SOURCE_DATE_EPOCH is not a valid integer")
156+
if source_stats['mtime'] > source_date_epoch:
157+
source_stats['mtime'] = source_date_epoch
150158
bytecode = importlib._bootstrap_external._code_to_timestamp_pyc(
151159
code, source_stats['mtime'], source_stats['size'])
152160
else:

Lib/test/test_py_compile.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ def test_bad_coding(self):
9898
self.assertFalse(os.path.exists(
9999
importlib.util.cache_from_source(bad_coding)))
100100

101+
def test_source_date_epoch(self):
102+
testtime = 123456789
103+
with support.EnvironmentVarGuard() as env:
104+
env["SOURCE_DATE_EPOCH"] = str(testtime)
105+
py_compile.compile(self.source_path, self.pyc_path)
106+
self.assertTrue(os.path.exists(self.pyc_path))
107+
self.assertFalse(os.path.exists(self.cache_path))
108+
with open(self.pyc_path, "rb") as f:
109+
f.read(4) # Skip the magic number.
110+
timebytes = f.read(4) # Read timestamp.
111+
self.assertEqual(testtime, int.from_bytes(timebytes, 'little'))
112+
101113
@unittest.skipIf(sys.flags.optimize > 0, 'test does not work with -O')
102114
def test_double_dot_no_clobber(self):
103115
# http://bugs.python.org/issue22966
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
If the SOURCE_DATE_EPOCH environment variable is set,
2+
py_compile will use it to override the timestamps it puts into .pyc files.

0 commit comments

Comments
 (0)