win32: archive the file creation time as birthtime, not as ctime, see #8730

On Windows, stat().st_ctime is the file *creation* time and not the POSIX
"metadata change time", and Python 3.12 deprecated it for that reason.
Thus, do not archive it as item.ctime any more (there simply is no ctime
on Windows), but archive it as item.birthtime - which is also what
Python >= 3.12 gives us via st_birthtime_ns.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Thomas Waldmann
2026-08-22 13:32:00 +02:00
co-authored by Claude Opus 5
parent 95fe6ccf7c
commit c99d03a209
3 changed files with 13 additions and 7 deletions
+2 -1
View File
@@ -1227,7 +1227,8 @@ class MetadataCollector:
attrs["mtime"] = safe_ns(st.st_mtime_ns)
if not self.noatime:
attrs["atime"] = safe_ns(st.st_atime_ns)
if not self.noctime:
if not self.noctime and not is_win32:
# win32: st_ctime is the file creation time, that is archived as birthtime, see #8730.
attrs["ctime"] = safe_ns(st.st_ctime_ns)
if not self.nobirthtime:
birthtime_ns = get_birthtime_ns(st, path, fd=fd)
+3
View File
@@ -96,6 +96,9 @@ def get_birthtime_ns(st, path, fd=None):
if hasattr(st, "st_birthtime_ns"):
# Added in Python 3.12, but not always available.
return st.st_birthtime_ns
elif is_win32:
# Python < 3.12 on win32: st_ctime is the file creation time, see #8730.
return st.st_ctime_ns
elif is_darwin and is_darwin_feature_64_bit_inode:
return _get_birthtime_ns(fd or path, follow_symlinks=False)
elif hasattr(st, "st_birthtime"):
+8 -6
View File
@@ -175,7 +175,7 @@ def test_basic_functionality(archivers, request):
unexpected = {"type": "modified", "added": 0, "removed": 0}
assert unexpected not in get_changes("input/file_touched", joutput)
if not content_only:
# on win32, ctime is the file creation time and does not change.
# on win32, no ctime is archived, see #8730.
expected = {"mtime"} if is_win32 else {"mtime", "ctime"}
assert expected.issubset({c["type"] for c in get_changes("input/file_touched", joutput)})
else:
@@ -274,18 +274,20 @@ def test_time_diffs(archivers, request):
cmd(archiver, "create", "archive2", "input")
output = cmd(archiver, "diff", "archive1", "archive2", "--format", "'{mtime}{ctime} {path}{NL}'")
assert "mtime" in output
assert "ctime" in output # Should show up on Windows as well since it is a new file.
if is_win32:
assert "ctime" not in output # win32: no ctime is archived, see #8730
else:
assert "ctime" in output
granularity_sleep()
os.chmod("input/test_file", 0o777)
cmd(archiver, "create", "archive3", "input")
output = cmd(archiver, "diff", "archive2", "archive3", "--format", "'{mtime}{ctime} {path}{NL}'")
assert "mtime" not in output
# Checking platform because ctime should not be shown on Windows since it wasn't recreated.
if not is_win32:
assert "ctime" in output
if is_win32:
assert "ctime" not in output # win32: no ctime is archived, see #8730
else:
assert "ctime" not in output
assert "ctime" in output
def test_sort_by_option(archivers, request):