Skip to content

Commit 7904e5e

Browse files
xsuchyclaude
authored andcommitted
fix: preserve dnf.conf/yum.conf timestamps when content is unchanged
Mock always rewrote these config files unconditionally, giving them a fresh timestamp on every invocation. Combined with DNF's default check_config_file_age=True, this caused repository metadata to be re-downloaded even when still valid (issue #216). Add file_util.write_if_changed() that skips the write when the file already contains the desired content, and use it in both Dnf.initialize_config() and Yum.initialize_config(). Fixes: #216 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5182969 commit 7904e5e

4 files changed

Lines changed: 69 additions & 4 deletions

File tree

mock/py/mockbuild/file_util.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ def touch(fileName):
3030
open(fileName, 'a').close()
3131

3232

33+
def write_if_changed(filepath, content):
34+
"""Write content to file only if it differs from the current content,
35+
preserving the file timestamp when unchanged."""
36+
try:
37+
with open(filepath, 'r', encoding="utf-8") as f:
38+
if f.read() == content:
39+
return
40+
except FileNotFoundError:
41+
pass
42+
with open(filepath, 'w', encoding="utf-8") as f:
43+
f.write(content)
44+
45+
3346
@traceLog()
3447
def rmtree(path, selinux=False, exclude=()):
3548
"""Version of shutil.rmtree that ignores no-such-file-or-directory errors,

mock/py/mockbuild/package_manager.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,7 @@ def initialize_config(self):
636636
dnfconf_path = os.path.join('etc', 'dnf', 'dnf.conf')
637637
for conf_path in (yumconf_path, dnfconf_path):
638638
chroot_conf_path = self.buildroot.make_chroot_path(conf_path)
639-
with open(chroot_conf_path, 'w+') as conf_file:
640-
conf_file.write(config_content)
639+
file_util.write_if_changed(chroot_conf_path, config_content)
641640
if os.path.exists(conf_path):
642641
shutil.copystat(conf_path, chroot_conf_path)
643642

@@ -704,8 +703,7 @@ def initialize_config(self):
704703
check_yum_config(config_content, self.buildroot.root_log)
705704
file_util.mkdirIfAbsent(self.buildroot.make_chroot_path('etc', 'dnf'))
706705
dnfconf_path = self.buildroot.make_chroot_path('etc', 'dnf', 'dnf.conf')
707-
with open(dnfconf_path, 'w+') as dnfconf_file:
708-
dnfconf_file.write(config_content)
706+
file_util.write_if_changed(dnfconf_path, config_content)
709707
self.initialize_vars()
710708

711709
def builddep(self, *pkgs, **kwargs):

mock/tests/test_file_util.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,53 @@ def chattr_works_or_skip(path: Path):
5656
pytest.skip(e.stderr.rstrip())
5757

5858

59+
class TestWriteIfChanged:
60+
"""Tests for file_util.write_if_changed"""
61+
62+
def test_creates_new_file(self, temp_dir):
63+
"""Should create the file when it does not exist."""
64+
path = temp_dir / "new.conf"
65+
file_util.write_if_changed(str(path), "new content")
66+
assert path.read_text() == "new content"
67+
68+
def test_overwrites_when_content_differs(self, temp_dir):
69+
"""Should overwrite the file when content changes."""
70+
path = temp_dir / "existing.conf"
71+
path.write_text("old content")
72+
file_util.write_if_changed(str(path), "new content")
73+
assert path.read_text() == "new content"
74+
75+
def test_preserves_timestamp_when_unchanged(self, temp_dir):
76+
"""Should not rewrite the file when content is identical."""
77+
path = temp_dir / "stable.conf"
78+
path.write_text("same content")
79+
mtime_before = path.stat().st_mtime_ns
80+
file_util.write_if_changed(str(path), "same content")
81+
mtime_after = path.stat().st_mtime_ns
82+
assert mtime_before == mtime_after
83+
84+
def test_updates_timestamp_when_changed(self, temp_dir):
85+
"""Should update the file timestamp when content differs."""
86+
path = temp_dir / "changing.conf"
87+
path.write_text("version 1")
88+
# Set mtime to the past so we can detect a change
89+
old_time = path.stat().st_mtime_ns - 1_000_000_000
90+
os.utime(str(path), ns=(old_time, old_time))
91+
mtime_before = path.stat().st_mtime_ns
92+
file_util.write_if_changed(str(path), "version 2")
93+
mtime_after = path.stat().st_mtime_ns
94+
assert mtime_after > mtime_before
95+
96+
def test_handles_empty_content(self, temp_dir):
97+
"""Should handle empty string content correctly."""
98+
path = temp_dir / "empty.conf"
99+
file_util.write_if_changed(str(path), "")
100+
assert path.read_text() == ""
101+
mtime_before = path.stat().st_mtime_ns
102+
file_util.write_if_changed(str(path), "")
103+
assert path.stat().st_mtime_ns == mtime_before
104+
105+
59106
class TestRmtree:
60107
"""Integration-style tests for file_util.rmtree using real files and directories"""
61108

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Mock now preserves the timestamp of `dnf.conf` and `yum.conf` inside the
2+
chroot when their content has not changed. Previously, every mock invocation
3+
rewrote these files unconditionally, which — combined with DNF's default
4+
`check_config_file_age=True` — caused repository metadata to be re-downloaded
5+
even when it was still valid ([issue#216][]).
6+
7+
[issue#216]: https://github.com/rpm-software-management/mock/issues/216

0 commit comments

Comments
 (0)