From e2c06f2d123d659cb7d7affee6fc16c868b38f93 Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:53:11 +1300 Subject: [PATCH] pacman: add root, cachedir, and config options (#11681) * pacman: add root, cachedir, and config options Add three dedicated options -- O(root), O(cachedir), and O(config) -- so that all pacman commands get the corresponding global flags (--root, --cachedir, --config) prepended, enabling use cases such as installing packages into a chroot or alternative root directory (similar to pacstrap). Co-Authored-By: Claude Sonnet 4.6 * add changelog frag --------- Co-authored-by: Claude Sonnet 4.6 --- .../11681-pacman-root-cachedir-config.yml | 3 + plugins/modules/pacman.py | 74 +++++++--- tests/unit/plugins/modules/test_pacman.py | 132 ++++++++++++++++++ 3 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 changelogs/fragments/11681-pacman-root-cachedir-config.yml diff --git a/changelogs/fragments/11681-pacman-root-cachedir-config.yml b/changelogs/fragments/11681-pacman-root-cachedir-config.yml new file mode 100644 index 0000000000..5e50f29fb5 --- /dev/null +++ b/changelogs/fragments/11681-pacman-root-cachedir-config.yml @@ -0,0 +1,3 @@ +minor_changes: + - pacman - add ``root``, ``cachedir``, and ``config`` options to support installing packages into an alternative root directory + (https://github.com/ansible-collections/community.general/issues/438, https://github.com/ansible-collections/community.general/pull/11681). diff --git a/plugins/modules/pacman.py b/plugins/modules/pacman.py index e1c772d4f6..6dd7a982b1 100644 --- a/plugins/modules/pacman.py +++ b/plugins/modules/pacman.py @@ -76,6 +76,26 @@ options: default: '' type: str + root: + description: + - An alternative installation root directory, passed as C(--root) to all pacman commands. + - Useful for installing packages into a chroot or a new system root, similarly to how C(pacstrap) works. + type: path + version_added: 12.6.0 + + cachedir: + description: + - An alternative package cache directory, passed as C(--cachedir) to all pacman commands. + - Only used when O(root) is also specified. + type: path + version_added: 12.6.0 + + config: + description: + - Path to an alternative pacman configuration file, passed as C(--config) to all pacman commands. + type: path + version_added: 12.6.0 + update_cache: description: - Whether or not to refresh the master package lists. @@ -239,6 +259,18 @@ EXAMPLES = r""" state: present reason: explicit reason_for: all + +- name: Install packages into a new root (similar to pacstrap) + community.general.pacman: + name: + - base + - base-devel + - python + state: present + root: /mnt + cachedir: /mnt/var/cache/pacman/pkg + config: /path/to/another/pacman.conf + update_cache: true """ import re @@ -282,6 +314,14 @@ class Pacman: self.pacman_path = self.m.get_bin_path(p["executable"], True) + self.pacman_cmd = [self.pacman_path] + if p["config"]: + self.pacman_cmd += ["--config", p["config"]] + if p["root"]: + self.pacman_cmd += ["--root", p["root"]] + if p["cachedir"]: + self.pacman_cmd += ["--cachedir", p["cachedir"]] + self._cached_database = None # Normalize for old configs @@ -375,8 +415,7 @@ class Pacman: self.add_exit_infos("package(s) already installed") return - cmd_base = [ - self.pacman_path, + cmd_base = self.pacman_cmd + [ "--noconfirm", "--noprogressbar", "--needed", @@ -469,7 +508,7 @@ class Pacman: # set reason if pkgs_to_set_reason: - cmd = [self.pacman_path, "--noconfirm", "--database"] + cmd = self.pacman_cmd + ["--noconfirm", "--database"] if self.m.params["reason"] == "dependency": cmd.append("--asdeps") else: @@ -496,7 +535,7 @@ class Pacman: # There's something to do, set this in advance self.changed = True - cmd_base = [self.pacman_path, "--remove", "--noconfirm", "--noprogressbar"] + cmd_base = self.pacman_cmd + ["--remove", "--noconfirm", "--noprogressbar"] cmd_base += self.m.params["extra_args"] cmd_base += ["--nodeps", "--nodeps"] if self.m.params["force"] else [] # nosave_args conflicts with --print-format. Added later. @@ -554,8 +593,7 @@ class Pacman: if self.m.check_mode: self.add_exit_infos(f"{len(self.inventory['upgradable_pkgs'])} packages would have been upgraded") else: - cmd = [ - self.pacman_path, + cmd = self.pacman_cmd + [ "--sync", "--sysupgrade", "--quiet", @@ -573,7 +611,7 @@ class Pacman: def _list_database(self): """runs pacman --sync --list with some caching""" if self._cached_database is None: - dummy, packages, dummy = self.m.run_command([self.pacman_path, "--sync", "--list"], check_rc=True) + dummy, packages, dummy = self.m.run_command(self.pacman_cmd + ["--sync", "--list"], check_rc=True) self._cached_database = packages.splitlines() return self._cached_database @@ -589,8 +627,7 @@ class Pacman: self.exit_params["cache_updated"] = True return - cmd = [ - self.pacman_path, + cmd = self.pacman_cmd + [ "--sync", "--refresh", ] @@ -646,11 +683,11 @@ class Pacman: # pkg is possibly in the / format, or a filename or a URL # Start with / case - cmd = [self.pacman_path, "--sync", "--print-format", "%n", pkg] + cmd = self.pacman_cmd + ["--sync", "--print-format", "%n", pkg] rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) if rc != 0: # fallback to filename / URL - cmd = [self.pacman_path, "--upgrade", "--print-format", "%n", pkg] + cmd = self.pacman_cmd + ["--upgrade", "--print-format", "%n", pkg] rc, stdout, stderr = self.m.run_command(cmd, check_rc=False) if rc != 0: if self.target_state == "absent": @@ -688,7 +725,7 @@ class Pacman: """ installed_pkgs = {} - dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query"], check_rc=True) + dummy, stdout, dummy = self.m.run_command(self.pacman_cmd + ["--query"], check_rc=True) # Format of a line: "pacman 6.0.1-2" query_re = re.compile(r"^\s*(?P\S+)\s+(?P\S+)\s*$") for l in stdout.splitlines(): @@ -699,7 +736,7 @@ class Pacman: installed_pkgs[pkg] = ver installed_groups = defaultdict(set) - dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--groups"], check_rc=True) + dummy, stdout, dummy = self.m.run_command(self.pacman_cmd + ["--query", "--groups"], check_rc=True) # Format of lines: # base-devel file # base-devel findutils @@ -723,7 +760,7 @@ class Pacman: available_pkgs[pkg] = ver available_groups = defaultdict(set) - dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--sync", "--groups", "--groups"], check_rc=True) + dummy, stdout, dummy = self.m.run_command(self.pacman_cmd + ["--sync", "--groups", "--groups"], check_rc=True) # Format of lines: # vim-plugins vim-airline # vim-plugins vim-airline-themes @@ -738,7 +775,7 @@ class Pacman: available_groups[group].add(pkg) upgradable_pkgs = {} - rc, stdout, stderr = self.m.run_command([self.pacman_path, "--query", "--upgrades"], check_rc=False) + rc, stdout, stderr = self.m.run_command(self.pacman_cmd + ["--query", "--upgrades"], check_rc=False) stdout = stdout.splitlines() if stdout and "Avoid running" in stdout[0]: @@ -777,7 +814,7 @@ class Pacman: ) pkg_reasons = {} - dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--explicit"], check_rc=True) + dummy, stdout, dummy = self.m.run_command(self.pacman_cmd + ["--query", "--explicit"], check_rc=True) # Format of a line: "pacman 6.0.1-2" for l in stdout.splitlines(): l = l.strip() @@ -785,7 +822,7 @@ class Pacman: continue pkg = l.split()[0] pkg_reasons[pkg] = "explicit" - dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--deps"], check_rc=True) + dummy, stdout, dummy = self.m.run_command(self.pacman_cmd + ["--query", "--deps"], check_rc=True) # Format of a line: "pacman 6.0.1-2" for l in stdout.splitlines(): l = l.strip() @@ -817,6 +854,9 @@ def setup_module(): remove_nosave=dict(type="bool", default=False), executable=dict(type="str", default="pacman"), extra_args=dict(type="str", default=""), + root=dict(type="path"), + cachedir=dict(type="path"), + config=dict(type="path"), upgrade=dict(type="bool"), upgrade_extra_args=dict(type="str", default=""), update_cache=dict(type="bool"), diff --git a/tests/unit/plugins/modules/test_pacman.py b/tests/unit/plugins/modules/test_pacman.py index a1d0439a9a..f19ad85450 100644 --- a/tests/unit/plugins/modules/test_pacman.py +++ b/tests/unit/plugins/modules/test_pacman.py @@ -404,6 +404,92 @@ class TestPacman: ], True, ), + ( + # root + cachedir + config: all commands get prefixed with the global options + {"root": "/mnt", "cachedir": "/mnt/var/cache/pacman/pkg", "config": "/alt/pacman.conf"}, + [ + ( + [ + "pacman", + "--config", + "/alt/pacman.conf", + "--root", + "/mnt", + "--cachedir", + "/mnt/var/cache/pacman/pkg", + "--sync", + "--list", + ], + {"check_rc": True}, + 0, + "a\nb\nc", + "", + ), + ( + [ + "pacman", + "--config", + "/alt/pacman.conf", + "--root", + "/mnt", + "--cachedir", + "/mnt/var/cache/pacman/pkg", + "--sync", + "--refresh", + ], + {"check_rc": False}, + 0, + "stdout", + "stderr", + ), + ( + [ + "pacman", + "--config", + "/alt/pacman.conf", + "--root", + "/mnt", + "--cachedir", + "/mnt/var/cache/pacman/pkg", + "--sync", + "--list", + ], + {"check_rc": True}, + 0, + "a\nb\nc", + "", + ), + ], + False, + ), + ( + # config only (no root/cachedir) + {"config": "/alt/pacman.conf"}, + [ + ( + ["pacman", "--config", "/alt/pacman.conf", "--sync", "--list"], + {"check_rc": True}, + 0, + "a\nb\nc", + "", + ), + ( + ["pacman", "--config", "/alt/pacman.conf", "--sync", "--refresh"], + {"check_rc": False}, + 0, + "stdout", + "stderr", + ), + ( + ["pacman", "--config", "/alt/pacman.conf", "--sync", "--list"], + {"check_rc": True}, + 0, + "a\nb\nc", + "", + ), + ], + False, + ), ( # Test whether pacman --sync --list is not called more than twice {"upgrade": True}, @@ -1006,6 +1092,52 @@ class TestPacman: }, AnsibleExitJson, ), + ( + # install pkg with root: global options come before the operation flags + {"name": ["sudo"], "state": "present", "root": "/mnt", "cachedir": "/mnt/var/cache/pacman/pkg"}, + ["sudo"], + [Package("sudo", "sudo")], + { + "calls": [ + mock.call( + mock.ANY, + [ + "pacman", + "--root", + "/mnt", + "--cachedir", + "/mnt/var/cache/pacman/pkg", + "--noconfirm", + "--noprogressbar", + "--needed", + "--sync", + "--print-format", + "%n %v", + "sudo", + ], + check_rc=False, + ), + mock.call( + mock.ANY, + [ + "pacman", + "--root", + "/mnt", + "--cachedir", + "/mnt/var/cache/pacman/pkg", + "--noconfirm", + "--noprogressbar", + "--needed", + "--sync", + "sudo", + ], + check_rc=False, + ), + ], + "side_effect": [(0, "sudo version", ""), (0, "", "")], + }, + AnsibleExitJson, + ), ( # latest pkg: Check mode {"_ansible_check_mode": True, "name": ["sqlite"], "state": "latest"},