Skip to content

Commit 344a5a4

Browse files
committed
Issue #13863: fix incorrect .pyc timestamps on Windows / NTFS (apparently due to buggy fstat)
1 parent 16cc9a7 commit 344a5a4

3 files changed

Lines changed: 91 additions & 6 deletions

File tree

Lib/test/test_import.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import py_compile
66
import random
77
import stat
8+
import struct
89
import sys
910
import unittest
1011
import textwrap
@@ -350,6 +351,46 @@ def test_timestamp_overflow(self):
350351
del sys.path[0]
351352
remove_files(TESTFN)
352353

354+
def test_pyc_mtime(self):
355+
# Test for issue #13863: .pyc timestamp sometimes incorrect on Windows.
356+
sys.path.insert(0, os.curdir)
357+
try:
358+
# Jan 1, 2012; Jul 1, 2012.
359+
mtimes = 1325376000, 1341100800
360+
361+
# Different names to avoid running into import caching.
362+
tails = "spam", "eggs"
363+
for mtime, tail in zip(mtimes, tails):
364+
module = TESTFN + tail
365+
source = module + ".py"
366+
compiled = source + ('c' if __debug__ else 'o')
367+
368+
# Create a new Python file with the given mtime.
369+
with open(source, 'w') as f:
370+
f.write("# Just testing\nx=1, 2, 3\n")
371+
os.utime(source, (mtime, mtime))
372+
373+
# Generate the .pyc/o file; if it couldn't be created
374+
# for some reason, skip the test.
375+
m = __import__(module)
376+
if not os.path.exists(compiled):
377+
unlink(source)
378+
self.skipTest("Couldn't create .pyc/.pyo file.")
379+
380+
# Actual modification time of .py file.
381+
mtime1 = int(os.stat(source).st_mtime) & 0xffffffff
382+
383+
# mtime that was encoded in the .pyc file.
384+
with open(compiled, 'rb') as f:
385+
mtime2 = struct.unpack('<L', f.read(8)[4:])[0]
386+
387+
unlink(compiled)
388+
unlink(source)
389+
390+
self.assertEqual(mtime1, mtime2)
391+
finally:
392+
sys.path.pop(0)
393+
353394

354395
class PycRewritingTests(unittest.TestCase):
355396
# Test that the `co_filename` attribute on code objects always points

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ What's New in Python 2.7.4
99
Core and Builtins
1010
-----------------
1111

12+
- Issue #13863: Work around buggy 'fstat' implementation on Windows / NTFS that
13+
lead to incorrect timestamps (off by one hour) being stored in .pyc files on
14+
some systems.
15+
1216
- Issue #16602: When a weakref's target was part of a long deallocation
1317
chain, the object could remain reachable through its weakref even though
1418
its refcount had dropped to zero.

Python/import.c

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -904,10 +904,9 @@ open_exclusive(char *filename, mode_t mode)
904904
remove the file. */
905905

906906
static void
907-
write_compiled_module(PyCodeObject *co, char *cpathname, struct stat *srcstat)
907+
write_compiled_module(PyCodeObject *co, char *cpathname, struct stat *srcstat, time_t mtime)
908908
{
909909
FILE *fp;
910-
time_t mtime = srcstat->st_mtime;
911910
#ifdef MS_WINDOWS /* since Windows uses different permissions */
912911
mode_t mode = srcstat->st_mode & ~S_IEXEC;
913912
/* Issue #6074: We ensure user write access, so we can delete it later
@@ -993,6 +992,38 @@ update_compiled_module(PyCodeObject *co, char *pathname)
993992
return 1;
994993
}
995994

995+
#ifdef MS_WINDOWS
996+
997+
/* Seconds between 1.1.1601 and 1.1.1970 */
998+
static __int64 secs_between_epochs = 11644473600;
999+
1000+
/* Get mtime from file pointer. */
1001+
1002+
static time_t
1003+
win32_mtime(FILE *fp, char *pathname)
1004+
{
1005+
__int64 filetime;
1006+
HANDLE fh;
1007+
BY_HANDLE_FILE_INFORMATION file_information;
1008+
1009+
fh = (HANDLE)_get_osfhandle(fileno(fp));
1010+
if (fh == INVALID_HANDLE_VALUE ||
1011+
!GetFileInformationByHandle(fh, &file_information)) {
1012+
PyErr_Format(PyExc_RuntimeError,
1013+
"unable to get file status from '%s'",
1014+
pathname);
1015+
return -1;
1016+
}
1017+
/* filetime represents the number of 100ns intervals since
1018+
1.1.1601 (UTC). Convert to seconds since 1.1.1970 (UTC). */
1019+
filetime = (__int64)file_information.ftLastWriteTime.dwHighDateTime << 32 |
1020+
file_information.ftLastWriteTime.dwLowDateTime;
1021+
return filetime / 10000000 - secs_between_epochs;
1022+
}
1023+
1024+
#endif /* #ifdef MS_WINDOWS */
1025+
1026+
9961027
/* Load a source module from a given file and return its module
9971028
object WITH INCREMENTED REFERENCE COUNT. If there's a matching
9981029
byte-compiled file, use that instead. */
@@ -1006,20 +1037,29 @@ load_source_module(char *name, char *pathname, FILE *fp)
10061037
char *cpathname;
10071038
PyCodeObject *co = NULL;
10081039
PyObject *m;
1040+
time_t mtime;
10091041

10101042
if (fstat(fileno(fp), &st) != 0) {
10111043
PyErr_Format(PyExc_RuntimeError,
10121044
"unable to get file status from '%s'",
10131045
pathname);
10141046
return NULL;
10151047
}
1016-
if (sizeof st.st_mtime > 4) {
1048+
1049+
#ifdef MS_WINDOWS
1050+
mtime = win32_mtime(fp, pathname);
1051+
if (mtime == (time_t)-1 && PyErr_Occurred())
1052+
return NULL;
1053+
#else
1054+
mtime = st.st_mtime;
1055+
#endif
1056+
if (sizeof mtime > 4) {
10171057
/* Python's .pyc timestamp handling presumes that the timestamp fits
10181058
in 4 bytes. Since the code only does an equality comparison,
10191059
ordering is not important and we can safely ignore the higher bits
10201060
(collisions are extremely unlikely).
10211061
*/
1022-
st.st_mtime &= 0xFFFFFFFF;
1062+
mtime &= 0xFFFFFFFF;
10231063
}
10241064
buf = PyMem_MALLOC(MAXPATHLEN+1);
10251065
if (buf == NULL) {
@@ -1028,7 +1068,7 @@ load_source_module(char *name, char *pathname, FILE *fp)
10281068
cpathname = make_compiled_pathname(pathname, buf,
10291069
(size_t)MAXPATHLEN + 1);
10301070
if (cpathname != NULL &&
1031-
(fpc = check_compiled_module(pathname, st.st_mtime, cpathname))) {
1071+
(fpc = check_compiled_module(pathname, mtime, cpathname))) {
10321072
co = read_compiled_module(cpathname, fpc);
10331073
fclose(fpc);
10341074
if (co == NULL)
@@ -1053,7 +1093,7 @@ load_source_module(char *name, char *pathname, FILE *fp)
10531093
if (b < 0)
10541094
goto error_exit;
10551095
if (!b)
1056-
write_compiled_module(co, cpathname, &st);
1096+
write_compiled_module(co, cpathname, &st, mtime);
10571097
}
10581098
}
10591099
m = PyImport_ExecCodeModuleEx(name, (PyObject *)co, pathname);

0 commit comments

Comments
 (0)