mock/1001-yum-cache-add-cache-key-for-shared-package-cache.patch
Andrew Lukoshko 42a01ccdb3 Add OCI platform support for x86_64 sub-architecture containers
Add cache_key option to yum_cache plugin for shared package cache across configs
2026-04-08 14:21:31 +00:00

268 lines
11 KiB
Diff

From 38842ed9678d974309d879907d1f05fe211e539b Mon Sep 17 00:00:00 2001
From: Andrew Lukoshko <andrew.lukoshko@gmail.com>
Date: Wed, 8 Apr 2026 01:49:00 +0200
Subject: [PATCH] yum_cache: add cache_key option for shared package cache
across configs
When cache_key is set in yum_cache_opts, the plugin redirects the
dnf/yum cache bind mounts and lock file to a shared directory at
<cache_topdir>/yum_cache/<cache_key>/ instead of the per-config
buildroot.cachedir. This allows multiple mock configs with different
root names but the same cache_key to share cached RPM packages and
repo metadata, avoiding redundant downloads.
Without cache_key the behavior is identical to the original plugin.
Usage in mock config:
config_opts['plugin_conf']['yum_cache_opts']['cache_key'] = 'almalinux-kitten-10-x86_64'
The shared cache directory is not removed by --scrub=all (which only
cleans per-root caches), matching the design intent that shared caches
are host-level resources.
---
py/mockbuild/plugins/yum_cache.py | 25 +++-
tests/plugins/test_yum_cache.py | 181 +++++++++++++++++++++++++
2 files changed, 200 insertions(+), 6 deletions(-)
create mode 100644 tests/plugins/test_yum_cache.py
diff --git a/py/mockbuild/plugins/yum_cache.py b/py/mockbuild/plugins/yum_cache.py
index c732b439f..1e8aec494 100644
--- a/py/mockbuild/plugins/yum_cache.py
+++ b/py/mockbuild/plugins/yum_cache.py
@@ -26,11 +26,12 @@ def init(plugins, conf, buildroot):
class CacheDir:
- def __init__(self, buildroot, pkg_manager):
+ def __init__(self, buildroot, pkg_manager, cache_dir=None):
self.buildroot = buildroot
self.cache_path = os.path.join('/var/cache', pkg_manager)
- self.host_cache_path = os.path.join(self.buildroot.cachedir,
- pkg_manager + '_cache')
+ self.host_cache_path = os.path.join(
+ cache_dir if cache_dir else self.buildroot.cachedir,
+ pkg_manager + '_cache')
self.mount_path = self.buildroot.make_chroot_path(self.cache_path)
self.buildroot.mounts.add(BindMountPoint(
srcpath=self.host_cache_path,
@@ -56,9 +57,21 @@ def __init__(self, plugins, conf, buildroot):
self.config = buildroot.config
self.state = buildroot.state
self.yum_cache_opts = conf
+
+ cache_key = conf.get('cache_key')
+ if cache_key:
+ cache_topdir = self.config['cache_topdir']
+ cache_dir = os.path.join(cache_topdir, 'yum_cache', cache_key)
+ mockbuild.file_util.mkdirIfAbsent(cache_dir)
+ lock_dir = cache_dir
+ getLog().info("enabled package manager cache (shared, key=%s)", cache_key)
+ else:
+ cache_dir = None
+ lock_dir = buildroot.cachedir
+
self.cache_dirs = [
- CacheDir(buildroot, 'yum'),
- CacheDir(buildroot, 'dnf'),
+ CacheDir(buildroot, 'yum', cache_dir),
+ CacheDir(buildroot, 'dnf', cache_dir),
]
self.yumSharedCachePath = self.cache_dirs[0].host_cache_path
self.online = self.config['online']
@@ -66,7 +79,7 @@ def __init__(self, plugins, conf, buildroot):
plugins.add_hook("postyum", self._yumCachePostYumHook)
plugins.add_hook("preinit", self._yumCachePreInitHook)
- self.yumCacheLock = open(os.path.join(buildroot.cachedir, "yumcache.lock"), "a+")
+ self.yumCacheLock = open(os.path.join(lock_dir, "yumcache.lock"), "a+")
# =============
diff --git a/tests/plugins/test_yum_cache.py b/tests/plugins/test_yum_cache.py
new file mode 100644
index 000000000..a6aba45d0
--- /dev/null
+++ b/tests/plugins/test_yum_cache.py
@@ -0,0 +1,181 @@
+"""Unit tests for the yum_cache plugin cache_key shared-cache feature."""
+
+from copy import deepcopy
+from unittest import mock
+from unittest.mock import MagicMock, patch
+
+from mockbuild.plugins import yum_cache
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+DEFAULT_CONF = {
+ 'max_age_days': 30,
+ 'max_metadata_age_days': 0.5,
+}
+
+
+def make_buildroot(extra_config=None):
+ br = MagicMock()
+ br.config = {
+ 'cache_topdir': '/var/cache/mock',
+ 'online': False,
+ 'target_arch': 'x86_64',
+ }
+ if extra_config:
+ br.config.update(extra_config)
+ br.make_chroot_path.side_effect = lambda p: '/chroot' + p
+ return br
+
+
+# ---------------------------------------------------------------------------
+# init() entry point
+# ---------------------------------------------------------------------------
+
+@patch('mockbuild.plugins.yum_cache.YumCache')
+def test_init(MockYumCache):
+ plugins, conf, buildroot = object(), object(), object()
+ yum_cache.init(plugins, conf, buildroot)
+ MockYumCache.assert_called_once_with(plugins, conf, buildroot)
+
+
+# ---------------------------------------------------------------------------
+# Without cache_key — legacy behaviour unchanged
+# ---------------------------------------------------------------------------
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_no_cache_key_uses_buildroot_cachedir(mock_file_util):
+ """Without cache_key, host_cache_path must be buildroot.cachedir/dnf_cache."""
+ br = make_buildroot()
+ br.cachedir = '/var/cache/mock/my-root'
+ conf = deepcopy(DEFAULT_CONF)
+
+ with patch('builtins.open', mock.mock_open()):
+ plugin = yum_cache.YumCache(MagicMock(), conf, br)
+
+ dnf_dir = next(d for d in plugin.cache_dirs if d.host_cache_path.endswith('dnf_cache'))
+ assert dnf_dir.host_cache_path == '/var/cache/mock/my-root/dnf_cache'
+
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_no_cache_key_lock_in_buildroot_cachedir(mock_file_util):
+ """Without cache_key, lock file must be in buildroot.cachedir."""
+ br = make_buildroot()
+ br.cachedir = '/var/cache/mock/my-root'
+ conf = deepcopy(DEFAULT_CONF)
+
+ mock_open = mock.mock_open()
+ with patch('builtins.open', mock_open):
+ yum_cache.YumCache(MagicMock(), conf, br)
+
+ opened_paths = [c.args[0] for c in mock_open.call_args_list]
+ assert any('my-root/yumcache.lock' in p for p in opened_paths), \
+ f"lock not in cachedir; opened: {opened_paths}"
+
+
+# ---------------------------------------------------------------------------
+# With cache_key — shared cache behaviour
+# ---------------------------------------------------------------------------
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_cache_key_uses_shared_dir(mock_file_util):
+ """With cache_key, host_cache_path must be cache_topdir/yum_cache/{key}/dnf_cache."""
+ br = make_buildroot({'cache_topdir': '/var/cache/mock'})
+ conf = {**DEFAULT_CONF, 'cache_key': 'centos-stream-10-x86_64'}
+
+ with patch('builtins.open', mock.mock_open()):
+ plugin = yum_cache.YumCache(MagicMock(), conf, br)
+
+ dnf_dir = next(d for d in plugin.cache_dirs if d.host_cache_path.endswith('dnf_cache'))
+ assert dnf_dir.host_cache_path == \
+ '/var/cache/mock/yum_cache/centos-stream-10-x86_64/dnf_cache'
+
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_cache_key_lock_in_shared_dir(mock_file_util):
+ """With cache_key, lock file must be in the shared cache dir, not buildroot.cachedir."""
+ br = make_buildroot({'cache_topdir': '/var/cache/mock'})
+ br.cachedir = '/var/cache/mock/my-root'
+ conf = {**DEFAULT_CONF, 'cache_key': 'centos-stream-10-x86_64'}
+
+ mock_open = mock.mock_open()
+ with patch('builtins.open', mock_open):
+ yum_cache.YumCache(MagicMock(), conf, br)
+
+ opened_paths = [c.args[0] for c in mock_open.call_args_list]
+ assert any('yum_cache/centos-stream-10-x86_64/yumcache.lock' in p
+ for p in opened_paths), f"shared lock not found; opened: {opened_paths}"
+ assert not any('my-root/yumcache.lock' in p
+ for p in opened_paths), "lock must not be in buildroot.cachedir"
+
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_cache_key_shared_dir_created(mock_file_util):
+ """With cache_key, the shared directory must be created via mkdirIfAbsent."""
+ br = make_buildroot({'cache_topdir': '/var/cache/mock'})
+ conf = {**DEFAULT_CONF, 'cache_key': 'centos-stream-10-x86_64'}
+
+ with patch('builtins.open', mock.mock_open()):
+ yum_cache.YumCache(MagicMock(), conf, br)
+
+ created_dirs = [c.args[0] for c in mock_file_util.mkdirIfAbsent.call_args_list]
+ assert any('yum_cache/centos-stream-10-x86_64' in d for d in created_dirs), \
+ f"shared dir not created; mkdirIfAbsent called with: {created_dirs}"
+
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_both_dnf_and_yum_cache_dirs_registered(mock_file_util):
+ """Both /var/cache/dnf and /var/cache/yum bind mounts must be added."""
+ br = make_buildroot()
+ conf = {**DEFAULT_CONF, 'cache_key': 'centos-stream-10-x86_64'}
+
+ with patch('builtins.open', mock.mock_open()):
+ yum_cache.YumCache(MagicMock(), conf, br)
+
+ added_srcs = [c.args[0].srcpath for c in br.mounts.add.call_args_list]
+ assert any('dnf_cache' in p for p in added_srcs), f"dnf_cache not mounted: {added_srcs}"
+ assert any('yum_cache' in p for p in added_srcs), f"yum_cache not mounted: {added_srcs}"
+
+
+# ---------------------------------------------------------------------------
+# Hook registration (both modes)
+# ---------------------------------------------------------------------------
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_hooks_registered(mock_file_util):
+ """Plugin must always register preyum, postyum, preinit hooks."""
+ for conf in [deepcopy(DEFAULT_CONF), {**DEFAULT_CONF, 'cache_key': 'my-key'}]:
+ plugins = MagicMock()
+ br = make_buildroot()
+ br.cachedir = '/var/cache/mock/root'
+ with patch('builtins.open', mock.mock_open()):
+ yum_cache.YumCache(plugins, conf, br)
+ hook_names = [c.args[0] for c in plugins.add_hook.call_args_list]
+ assert 'preyum' in hook_names
+ assert 'postyum' in hook_names
+ assert 'preinit' in hook_names
+
+
+# ---------------------------------------------------------------------------
+# Two configs, same cache_key — both resolve to same path
+# ---------------------------------------------------------------------------
+
+@patch('mockbuild.plugins.yum_cache.mockbuild.file_util')
+def test_two_roots_same_cache_key_share_path(mock_file_util):
+ """Two buildroots with different cachedir but same cache_key must resolve identical paths."""
+ conf = {**DEFAULT_CONF, 'cache_key': 'centos-stream-10-x86_64'}
+
+ br1 = make_buildroot({'cache_topdir': '/var/cache/mock'})
+ br1.cachedir = '/var/cache/mock/build-root-1'
+ br2 = make_buildroot({'cache_topdir': '/var/cache/mock'})
+ br2.cachedir = '/var/cache/mock/build-root-2'
+
+ with patch('builtins.open', mock.mock_open()):
+ p1 = yum_cache.YumCache(MagicMock(), deepcopy(conf), br1)
+ p2 = yum_cache.YumCache(MagicMock(), deepcopy(conf), br2)
+
+ dnf1 = next(d.host_cache_path for d in p1.cache_dirs if d.host_cache_path.endswith('dnf_cache'))
+ dnf2 = next(d.host_cache_path for d in p2.cache_dirs if d.host_cache_path.endswith('dnf_cache'))
+ assert dnf1 == dnf2, f"paths differ: {dnf1} != {dnf2}"