mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
fc0f677535
pacman: do not fail if there is nothing to do (#8514)
Do not fail if there is nothing to do.
(cherry picked from commit df7fe19bbe
)
Co-authored-by: Felix Fontein <felix@fontein.de>
872 lines
32 KiB
Python
872 lines
32 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (c) 2012, Afterburn <https://github.com/afterburn>
|
|
# Copyright (c) 2013, Aaron Bull Schaefer <aaron@elasticdog.com>
|
|
# Copyright (c) 2015, Indrajit Raychaudhuri <irc+code@indrajit.com>
|
|
# Copyright (c) 2022, Jean Raby <jean@raby.sh>
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
DOCUMENTATION = """
|
|
---
|
|
module: pacman
|
|
short_description: Manage packages with I(pacman)
|
|
description:
|
|
- Manage packages with the I(pacman) package manager, which is used by Arch Linux and its variants.
|
|
author:
|
|
- Indrajit Raychaudhuri (@indrajitr)
|
|
- Aaron Bull Schaefer (@elasticdog) <aaron@elasticdog.com>
|
|
- Maxime de Roucy (@tchernomax)
|
|
- Jean Raby (@jraby)
|
|
extends_documentation_fragment:
|
|
- community.general.attributes
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
diff_mode:
|
|
support: full
|
|
options:
|
|
name:
|
|
description:
|
|
- Name or list of names of the package(s) or file(s) to install, upgrade, or remove.
|
|
Cannot be used in combination with O(upgrade).
|
|
aliases: [ package, pkg ]
|
|
type: list
|
|
elements: str
|
|
|
|
state:
|
|
description:
|
|
- Whether to install (V(present) or V(installed), V(latest)), or remove (V(absent) or V(removed)) a package.
|
|
- V(present) and V(installed) will simply ensure that a desired package is installed.
|
|
- V(latest) will update the specified package if it is not of the latest available version.
|
|
- V(absent) and V(removed) will remove the specified package.
|
|
default: present
|
|
choices: [ absent, installed, latest, present, removed ]
|
|
type: str
|
|
|
|
force:
|
|
description:
|
|
- When removing packages, forcefully remove them, without any checks.
|
|
Same as O(extra_args="--nodeps --nodeps").
|
|
- When combined with O(update_cache), force a refresh of all package databases.
|
|
Same as O(update_cache_extra_args="--refresh --refresh").
|
|
default: false
|
|
type: bool
|
|
|
|
remove_nosave:
|
|
description:
|
|
- When removing packages, do not save modified configuration files as C(.pacsave) files.
|
|
(passes C(--nosave) to pacman)
|
|
version_added: 4.6.0
|
|
default: false
|
|
type: bool
|
|
|
|
executable:
|
|
description:
|
|
- Path of the binary to use. This can either be C(pacman) or a pacman compatible AUR helper.
|
|
- Pacman compatibility is unfortunately ill defined, in particular, this modules makes
|
|
extensive use of the C(--print-format) directive which is known not to be implemented by
|
|
some AUR helpers (notably, C(yay)).
|
|
- Beware that AUR helpers might behave unexpectedly and are therefore not recommended.
|
|
default: pacman
|
|
type: str
|
|
version_added: 3.1.0
|
|
|
|
extra_args:
|
|
description:
|
|
- Additional option to pass to pacman when enforcing O(state).
|
|
default: ''
|
|
type: str
|
|
|
|
update_cache:
|
|
description:
|
|
- Whether or not to refresh the master package lists.
|
|
- This can be run as part of a package installation or as a separate step.
|
|
- If not specified, it defaults to V(false).
|
|
- Please note that this option only had an influence on the module's C(changed) state
|
|
if O(name) and O(upgrade) are not specified before community.general 5.0.0.
|
|
See the examples for how to keep the old behavior.
|
|
type: bool
|
|
|
|
update_cache_extra_args:
|
|
description:
|
|
- Additional option to pass to pacman when enforcing O(update_cache).
|
|
default: ''
|
|
type: str
|
|
|
|
upgrade:
|
|
description:
|
|
- Whether or not to upgrade the whole system.
|
|
Cannot be used in combination with O(name).
|
|
- If not specified, it defaults to V(false).
|
|
type: bool
|
|
|
|
upgrade_extra_args:
|
|
description:
|
|
- Additional option to pass to pacman when enforcing O(upgrade).
|
|
default: ''
|
|
type: str
|
|
|
|
reason:
|
|
description:
|
|
- The install reason to set for the packages.
|
|
choices: [ dependency, explicit ]
|
|
type: str
|
|
version_added: 5.4.0
|
|
|
|
reason_for:
|
|
description:
|
|
- Set the install reason for V(all) packages or only for V(new) packages.
|
|
- In case of O(state=latest) already installed packages which will be updated to a newer version are not counted as V(new).
|
|
default: new
|
|
choices: [ all, new ]
|
|
type: str
|
|
version_added: 5.4.0
|
|
|
|
notes:
|
|
- When used with a C(loop:) each package will be processed individually,
|
|
it is much more efficient to pass the list directly to the O(name) option.
|
|
- To use an AUR helper (O(executable) option), a few extra setup steps might be required beforehand.
|
|
For example, a dedicated build user with permissions to install packages could be necessary.
|
|
- >
|
|
In the tests, while using C(yay) as the O(executable) option, the module failed to install AUR packages
|
|
with the error: C(error: target not found: <pkg>).
|
|
"""
|
|
|
|
RETURN = """
|
|
packages:
|
|
description:
|
|
- A list of packages that have been changed.
|
|
- Before community.general 4.5.0 this was only returned when O(upgrade=true).
|
|
In community.general 4.5.0, it was sometimes omitted when the package list is empty,
|
|
but since community.general 4.6.0 it is always returned when O(name) is specified or
|
|
O(upgrade=true).
|
|
returned: success and O(name) is specified or O(upgrade=true)
|
|
type: list
|
|
elements: str
|
|
sample: [ package, other-package ]
|
|
|
|
cache_updated:
|
|
description:
|
|
- The changed status of C(pacman -Sy).
|
|
- Useful when O(name) or O(upgrade=true) are specified next to O(update_cache=true).
|
|
returned: success, when O(update_cache=true)
|
|
type: bool
|
|
sample: false
|
|
version_added: 4.6.0
|
|
|
|
stdout:
|
|
description:
|
|
- Output from pacman.
|
|
returned: success, when needed
|
|
type: str
|
|
sample: ":: Synchronizing package databases... core is up to date :: Starting full system upgrade..."
|
|
version_added: 4.1.0
|
|
|
|
stderr:
|
|
description:
|
|
- Error output from pacman.
|
|
returned: success, when needed
|
|
type: str
|
|
sample: "warning: libtool: local (2.4.6+44+gb9b44533-14) is newer than core (2.4.6+42+gb88cebd5-15)\nwarning ..."
|
|
version_added: 4.1.0
|
|
"""
|
|
|
|
EXAMPLES = """
|
|
- name: Install package foo from repo
|
|
community.general.pacman:
|
|
name: foo
|
|
state: present
|
|
|
|
- name: Install package bar from file
|
|
community.general.pacman:
|
|
name: ~/bar-1.0-1-any.pkg.tar.xz
|
|
state: present
|
|
|
|
- name: Install package foo from repo and bar from file
|
|
community.general.pacman:
|
|
name:
|
|
- foo
|
|
- ~/bar-1.0-1-any.pkg.tar.xz
|
|
state: present
|
|
|
|
- name: Install package from AUR using a Pacman compatible AUR helper
|
|
community.general.pacman:
|
|
name: foo
|
|
state: present
|
|
executable: yay
|
|
extra_args: --builddir /var/cache/yay
|
|
|
|
- name: Upgrade package foo
|
|
# The 'changed' state of this call will indicate whether the cache was
|
|
# updated *or* whether foo was installed/upgraded.
|
|
community.general.pacman:
|
|
name: foo
|
|
state: latest
|
|
update_cache: true
|
|
|
|
- name: Remove packages foo and bar
|
|
community.general.pacman:
|
|
name:
|
|
- foo
|
|
- bar
|
|
state: absent
|
|
|
|
- name: Recursively remove package baz
|
|
community.general.pacman:
|
|
name: baz
|
|
state: absent
|
|
extra_args: --recursive
|
|
|
|
- name: Run the equivalent of "pacman -Sy" as a separate step
|
|
community.general.pacman:
|
|
update_cache: true
|
|
|
|
- name: Run the equivalent of "pacman -Su" as a separate step
|
|
community.general.pacman:
|
|
upgrade: true
|
|
|
|
- name: Run the equivalent of "pacman -Syu" as a separate step
|
|
# Since community.general 5.0.0 the 'changed' state of this call
|
|
# will be 'true' in case the cache was updated, or when a package
|
|
# was updated.
|
|
#
|
|
# The previous behavior was to only indicate whether something was
|
|
# upgraded. To keep the old behavior, add the following to the task:
|
|
#
|
|
# register: result
|
|
# changed_when: result.packages | length > 0
|
|
community.general.pacman:
|
|
update_cache: true
|
|
upgrade: true
|
|
|
|
- name: Run the equivalent of "pacman -Rdd", force remove package baz
|
|
community.general.pacman:
|
|
name: baz
|
|
state: absent
|
|
force: true
|
|
|
|
- name: Install foo as dependency and leave reason untouched if already installed
|
|
community.general.pacman:
|
|
name: foo
|
|
state: present
|
|
reason: dependency
|
|
reason_for: new
|
|
|
|
- name: Run the equivalent of "pacman -S --asexplicit", mark foo as explicit and install it if not present
|
|
community.general.pacman:
|
|
name: foo
|
|
state: present
|
|
reason: explicit
|
|
reason_for: all
|
|
"""
|
|
|
|
import re
|
|
import shlex
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from collections import defaultdict, namedtuple
|
|
|
|
|
|
class Package(object):
|
|
def __init__(self, name, source, source_is_URL=False):
|
|
self.name = name
|
|
self.source = source
|
|
self.source_is_URL = source_is_URL
|
|
|
|
def __eq__(self, o):
|
|
return self.name == o.name and self.source == o.source and self.source_is_URL == o.source_is_URL
|
|
|
|
def __lt__(self, o):
|
|
return self.name < o.name
|
|
|
|
def __repr__(self):
|
|
return 'Package("%s", "%s", %s)' % (self.name, self.source, self.source_is_URL)
|
|
|
|
|
|
VersionTuple = namedtuple("VersionTuple", ["current", "latest"])
|
|
|
|
|
|
class Pacman(object):
|
|
def __init__(self, module):
|
|
self.m = module
|
|
|
|
self.m.run_command_environ_update = dict(LC_ALL="C")
|
|
p = self.m.params
|
|
|
|
self._msgs = []
|
|
self._stdouts = []
|
|
self._stderrs = []
|
|
self.changed = False
|
|
self.exit_params = {}
|
|
|
|
self.pacman_path = self.m.get_bin_path(p["executable"], True)
|
|
|
|
self._cached_database = None
|
|
|
|
# Normalize for old configs
|
|
if p["state"] == "installed":
|
|
self.target_state = "present"
|
|
elif p["state"] == "removed":
|
|
self.target_state = "absent"
|
|
else:
|
|
self.target_state = p["state"]
|
|
|
|
def add_exit_infos(self, msg=None, stdout=None, stderr=None):
|
|
if msg:
|
|
self._msgs.append(msg)
|
|
if stdout:
|
|
self._stdouts.append(stdout)
|
|
if stderr:
|
|
self._stderrs.append(stderr)
|
|
|
|
def _set_mandatory_exit_params(self):
|
|
msg = "\n".join(self._msgs)
|
|
stdouts = "\n".join(self._stdouts)
|
|
stderrs = "\n".join(self._stderrs)
|
|
if stdouts:
|
|
self.exit_params["stdout"] = stdouts
|
|
if stderrs:
|
|
self.exit_params["stderr"] = stderrs
|
|
self.exit_params["msg"] = msg # mandatory, but might be empty
|
|
|
|
def fail(self, msg=None, stdout=None, stderr=None, **kwargs):
|
|
self.add_exit_infos(msg, stdout, stderr)
|
|
self._set_mandatory_exit_params()
|
|
if kwargs:
|
|
self.exit_params.update(**kwargs)
|
|
self.m.fail_json(**self.exit_params)
|
|
|
|
def success(self):
|
|
self._set_mandatory_exit_params()
|
|
self.m.exit_json(changed=self.changed, **self.exit_params)
|
|
|
|
def run(self):
|
|
if self.m.params["update_cache"]:
|
|
self.update_package_db()
|
|
|
|
if not (self.m.params["name"] or self.m.params["upgrade"]):
|
|
self.success()
|
|
|
|
self.inventory = self._build_inventory()
|
|
if self.m.params["upgrade"]:
|
|
self.upgrade()
|
|
self.success()
|
|
|
|
if self.m.params["name"]:
|
|
pkgs = self.package_list()
|
|
|
|
if self.target_state == "absent":
|
|
self.remove_packages(pkgs)
|
|
self.success()
|
|
else:
|
|
self.install_packages(pkgs)
|
|
self.success()
|
|
|
|
# This happens if an empty list has been provided for name
|
|
self.add_exit_infos(msg='Nothing to do')
|
|
self.success()
|
|
|
|
def install_packages(self, pkgs):
|
|
pkgs_to_install = []
|
|
pkgs_to_install_from_url = []
|
|
pkgs_to_set_reason = []
|
|
for p in pkgs:
|
|
if self.m.params["reason"] and (
|
|
p.name not in self.inventory["pkg_reasons"]
|
|
or self.m.params["reason_for"] == "all"
|
|
and self.inventory["pkg_reasons"][p.name] != self.m.params["reason"]
|
|
):
|
|
pkgs_to_set_reason.append(p.name)
|
|
if p.source_is_URL:
|
|
# URL packages bypass the latest / upgradable_pkgs test
|
|
# They go through the dry-run to let pacman decide if they will be installed
|
|
pkgs_to_install_from_url.append(p)
|
|
continue
|
|
if (
|
|
p.name not in self.inventory["installed_pkgs"]
|
|
or self.target_state == "latest"
|
|
and p.name in self.inventory["upgradable_pkgs"]
|
|
):
|
|
pkgs_to_install.append(p)
|
|
|
|
if len(pkgs_to_install) == 0 and len(pkgs_to_install_from_url) == 0 and len(pkgs_to_set_reason) == 0:
|
|
self.exit_params["packages"] = []
|
|
self.add_exit_infos("package(s) already installed")
|
|
return
|
|
|
|
cmd_base = [
|
|
self.pacman_path,
|
|
"--noconfirm",
|
|
"--noprogressbar",
|
|
"--needed",
|
|
]
|
|
if self.m.params["extra_args"]:
|
|
cmd_base.extend(self.m.params["extra_args"])
|
|
|
|
def _build_install_diff(pacman_verb, pkglist):
|
|
# Dry run to build the installation diff
|
|
|
|
cmd = cmd_base + [pacman_verb, "--print-format", "%n %v"] + [p.source for p in pkglist]
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
if rc != 0:
|
|
self.fail("Failed to list package(s) to install", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
|
|
name_ver = [l.strip() for l in stdout.splitlines()]
|
|
before = []
|
|
after = []
|
|
to_be_installed = []
|
|
for p in name_ver:
|
|
# With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs "loading packages..." on stdout. strip that.
|
|
# When installing from URLs, pacman can also output a 'nothing to do' message. strip that too.
|
|
if "loading packages" in p or "there is nothing to do" in p or 'Avoid running' in p:
|
|
continue
|
|
name, version = p.split()
|
|
if name in self.inventory["installed_pkgs"]:
|
|
before.append("%s-%s-%s" % (name, self.inventory["installed_pkgs"][name], self.inventory["pkg_reasons"][name]))
|
|
if name in pkgs_to_set_reason:
|
|
after.append("%s-%s-%s" % (name, version, self.m.params["reason"]))
|
|
elif name in self.inventory["pkg_reasons"]:
|
|
after.append("%s-%s-%s" % (name, version, self.inventory["pkg_reasons"][name]))
|
|
else:
|
|
after.append("%s-%s" % (name, version))
|
|
to_be_installed.append(name)
|
|
|
|
return (to_be_installed, before, after)
|
|
|
|
before = []
|
|
after = []
|
|
installed_pkgs = []
|
|
|
|
if pkgs_to_install:
|
|
p, b, a = _build_install_diff("--sync", pkgs_to_install)
|
|
installed_pkgs.extend(p)
|
|
before.extend(b)
|
|
after.extend(a)
|
|
if pkgs_to_install_from_url:
|
|
p, b, a = _build_install_diff("--upgrade", pkgs_to_install_from_url)
|
|
installed_pkgs.extend(p)
|
|
before.extend(b)
|
|
after.extend(a)
|
|
|
|
if len(installed_pkgs) == 0 and len(pkgs_to_set_reason) == 0:
|
|
# This can happen with URL packages if pacman decides there's nothing to do
|
|
self.exit_params["packages"] = []
|
|
self.add_exit_infos("package(s) already installed")
|
|
return
|
|
|
|
self.changed = True
|
|
|
|
self.exit_params["diff"] = {
|
|
"before": "\n".join(sorted(before)) + "\n" if before else "",
|
|
"after": "\n".join(sorted(after)) + "\n" if after else "",
|
|
}
|
|
|
|
changed_reason_pkgs = [p for p in pkgs_to_set_reason if p not in installed_pkgs]
|
|
|
|
if self.m.check_mode:
|
|
self.add_exit_infos("Would have installed %d packages" % (len(installed_pkgs) + len(changed_reason_pkgs)))
|
|
self.exit_params["packages"] = sorted(installed_pkgs + changed_reason_pkgs)
|
|
return
|
|
|
|
# actually do it
|
|
def _install_packages_for_real(pacman_verb, pkglist):
|
|
cmd = cmd_base + [pacman_verb] + [p.source for p in pkglist]
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
if rc != 0:
|
|
self.fail("Failed to install package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
self.add_exit_infos(stdout=stdout, stderr=stderr)
|
|
self._invalidate_database()
|
|
|
|
if pkgs_to_install:
|
|
_install_packages_for_real("--sync", pkgs_to_install)
|
|
if pkgs_to_install_from_url:
|
|
_install_packages_for_real("--upgrade", pkgs_to_install_from_url)
|
|
|
|
# set reason
|
|
if pkgs_to_set_reason:
|
|
cmd = [self.pacman_path, "--noconfirm", "--database"]
|
|
if self.m.params["reason"] == "dependency":
|
|
cmd.append("--asdeps")
|
|
else:
|
|
cmd.append("--asexplicit")
|
|
cmd.extend(pkgs_to_set_reason)
|
|
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
if rc != 0:
|
|
self.fail("Failed to install package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
self.add_exit_infos(stdout=stdout, stderr=stderr)
|
|
|
|
self.exit_params["packages"] = sorted(installed_pkgs + changed_reason_pkgs)
|
|
self.add_exit_infos("Installed %d package(s)" % (len(installed_pkgs) + len(changed_reason_pkgs)))
|
|
|
|
def remove_packages(self, pkgs):
|
|
# filter out pkgs that are already absent
|
|
pkg_names_to_remove = [p.name for p in pkgs if p.name in self.inventory["installed_pkgs"]]
|
|
|
|
if len(pkg_names_to_remove) == 0:
|
|
self.exit_params["packages"] = []
|
|
self.add_exit_infos("package(s) already absent")
|
|
return
|
|
|
|
# There's something to do, set this in advance
|
|
self.changed = True
|
|
|
|
cmd_base = [self.pacman_path, "--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.
|
|
# https://github.com/ansible-collections/community.general/issues/4315
|
|
|
|
# This is a bit of a TOCTOU but it is better than parsing the output of
|
|
# pacman -R, which is different depending on the user config (VerbosePkgLists)
|
|
# Start by gathering what would be removed
|
|
cmd = cmd_base + ["--print-format", "%n-%v"] + pkg_names_to_remove
|
|
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
if rc != 0:
|
|
self.fail("failed to list package(s) to remove", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
|
|
removed_pkgs = stdout.split()
|
|
self.exit_params["packages"] = removed_pkgs
|
|
self.exit_params["diff"] = {
|
|
"before": "\n".join(removed_pkgs) + "\n", # trailing \n to avoid diff complaints
|
|
"after": "",
|
|
}
|
|
|
|
if self.m.check_mode:
|
|
self.exit_params["packages"] = removed_pkgs
|
|
self.add_exit_infos("Would have removed %d packages" % len(removed_pkgs))
|
|
return
|
|
|
|
nosave_args = ["--nosave"] if self.m.params["remove_nosave"] else []
|
|
cmd = cmd_base + nosave_args + pkg_names_to_remove
|
|
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
if rc != 0:
|
|
self.fail("failed to remove package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
self._invalidate_database()
|
|
self.exit_params["packages"] = removed_pkgs
|
|
self.add_exit_infos("Removed %d package(s)" % len(removed_pkgs), stdout=stdout, stderr=stderr)
|
|
|
|
def upgrade(self):
|
|
"""Runs pacman --sync --sysupgrade if there are upgradable packages"""
|
|
|
|
if len(self.inventory["upgradable_pkgs"]) == 0:
|
|
self.add_exit_infos("Nothing to upgrade")
|
|
return
|
|
|
|
self.changed = True # there are upgrades, so there will be changes
|
|
|
|
# Build diff based on inventory first.
|
|
diff = {"before": "", "after": ""}
|
|
for pkg, versions in self.inventory["upgradable_pkgs"].items():
|
|
diff["before"] += "%s-%s\n" % (pkg, versions.current)
|
|
diff["after"] += "%s-%s\n" % (pkg, versions.latest)
|
|
self.exit_params["diff"] = diff
|
|
self.exit_params["packages"] = self.inventory["upgradable_pkgs"].keys()
|
|
|
|
if self.m.check_mode:
|
|
self.add_exit_infos(
|
|
"%d packages would have been upgraded" % (len(self.inventory["upgradable_pkgs"]))
|
|
)
|
|
else:
|
|
cmd = [
|
|
self.pacman_path,
|
|
"--sync",
|
|
"--sysupgrade",
|
|
"--quiet",
|
|
"--noconfirm",
|
|
]
|
|
if self.m.params["upgrade_extra_args"]:
|
|
cmd += self.m.params["upgrade_extra_args"]
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
self._invalidate_database()
|
|
if rc == 0:
|
|
self.add_exit_infos("System upgraded", stdout=stdout, stderr=stderr)
|
|
else:
|
|
self.fail("Could not upgrade", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
|
|
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)
|
|
self._cached_database = packages.splitlines()
|
|
return self._cached_database
|
|
|
|
def _invalidate_database(self):
|
|
"""invalidates the pacman --sync --list cache"""
|
|
self._cached_database = None
|
|
|
|
def update_package_db(self):
|
|
"""runs pacman --sync --refresh"""
|
|
if self.m.check_mode:
|
|
self.add_exit_infos("Would have updated the package db")
|
|
self.changed = True
|
|
self.exit_params["cache_updated"] = True
|
|
return
|
|
|
|
cmd = [
|
|
self.pacman_path,
|
|
"--sync",
|
|
"--refresh",
|
|
]
|
|
if self.m.params["update_cache_extra_args"]:
|
|
cmd += self.m.params["update_cache_extra_args"]
|
|
if self.m.params["force"]:
|
|
cmd += ["--refresh"]
|
|
else:
|
|
# Dump package database to get contents before update
|
|
pre_state = sorted(self._list_database())
|
|
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
self._invalidate_database()
|
|
|
|
if self.m.params["force"]:
|
|
# Always changed when force=true
|
|
self.exit_params["cache_updated"] = True
|
|
else:
|
|
# Dump package database to get contents after update
|
|
post_state = sorted(self._list_database())
|
|
# If contents changed, set changed=true
|
|
self.exit_params["cache_updated"] = pre_state != post_state
|
|
if self.exit_params["cache_updated"]:
|
|
self.changed = True
|
|
|
|
if rc == 0:
|
|
self.add_exit_infos("Updated package db", stdout=stdout, stderr=stderr)
|
|
else:
|
|
self.fail("could not update package db", cmd=cmd, stdout=stdout, stderr=stderr)
|
|
|
|
def package_list(self):
|
|
"""Takes the input package list and resolves packages groups to their package list using the inventory,
|
|
extracts package names from packages given as files or URLs using calls to pacman
|
|
|
|
Returns the expanded/resolved list as a list of Package
|
|
"""
|
|
pkg_list = []
|
|
for pkg in self.m.params["name"]:
|
|
if not pkg:
|
|
continue
|
|
|
|
is_URL = False
|
|
if pkg in self.inventory["available_groups"]:
|
|
# Expand group members
|
|
for group_member in self.inventory["available_groups"][pkg]:
|
|
pkg_list.append(Package(name=group_member, source=group_member))
|
|
elif pkg in self.inventory["available_pkgs"] or pkg in self.inventory["installed_pkgs"]:
|
|
# Just a regular pkg, either available in the repositories,
|
|
# or locally installed, which we need to know for absent state
|
|
pkg_list.append(Package(name=pkg, source=pkg))
|
|
else:
|
|
# Last resort, call out to pacman to extract the info,
|
|
# pkg is possibly in the <repo>/<pkgname> format, or a filename or a URL
|
|
|
|
# Start with <repo>/<pkgname> case
|
|
cmd = [self.pacman_path, "--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]
|
|
rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
|
|
if rc != 0:
|
|
if self.target_state == "absent":
|
|
continue # Don't bark for unavailable packages when trying to remove them
|
|
else:
|
|
self.fail(
|
|
msg="Failed to list package %s" % (pkg),
|
|
cmd=cmd,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
rc=rc,
|
|
)
|
|
# With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs " filename_without_extension downloading..." if the URL is unseen.
|
|
# In all cases, pacman outputs "loading packages..." on stdout. strip both
|
|
stdout = stdout.splitlines()[-1]
|
|
is_URL = True
|
|
pkg_name = stdout.strip()
|
|
pkg_list.append(Package(name=pkg_name, source=pkg, source_is_URL=is_URL))
|
|
|
|
return pkg_list
|
|
|
|
def _build_inventory(self):
|
|
"""Build a cache datastructure used for all pkg lookups
|
|
Returns a dict:
|
|
{
|
|
"installed_pkgs": {pkgname: version},
|
|
"installed_groups": {groupname: set(pkgnames)},
|
|
"available_pkgs": {pkgname: version},
|
|
"available_groups": {groupname: set(pkgnames)},
|
|
"upgradable_pkgs": {pkgname: (current_version,latest_version)},
|
|
"pkg_reasons": {pkgname: reason},
|
|
}
|
|
|
|
Fails the module if a package requested for install cannot be found
|
|
"""
|
|
|
|
installed_pkgs = {}
|
|
dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query"], check_rc=True)
|
|
# Format of a line: "pacman 6.0.1-2"
|
|
query_re = re.compile(r'^\s*(?P<pkg>\S+)\s+(?P<ver>\S+)\s*$')
|
|
for l in stdout.splitlines():
|
|
query_match = query_re.match(l)
|
|
if not query_match:
|
|
continue
|
|
pkg, ver = query_match.groups()
|
|
installed_pkgs[pkg] = ver
|
|
|
|
installed_groups = defaultdict(set)
|
|
dummy, stdout, dummy = self.m.run_command(
|
|
[self.pacman_path, "--query", "--groups"], check_rc=True
|
|
)
|
|
# Format of lines:
|
|
# base-devel file
|
|
# base-devel findutils
|
|
# ...
|
|
query_groups_re = re.compile(r'^\s*(?P<group>\S+)\s+(?P<pkg>\S+)\s*$')
|
|
for l in stdout.splitlines():
|
|
query_groups_match = query_groups_re.match(l)
|
|
if not query_groups_match:
|
|
continue
|
|
group, pkgname = query_groups_match.groups()
|
|
installed_groups[group].add(pkgname)
|
|
|
|
available_pkgs = {}
|
|
database = self._list_database()
|
|
# Format of a line: "core pacman 6.0.1-2"
|
|
for l in database:
|
|
l = l.strip()
|
|
if not l:
|
|
continue
|
|
repo, pkg, ver = l.split()[:3]
|
|
available_pkgs[pkg] = ver
|
|
|
|
available_groups = defaultdict(set)
|
|
dummy, stdout, dummy = self.m.run_command(
|
|
[self.pacman_path, "--sync", "--groups", "--groups"], check_rc=True
|
|
)
|
|
# Format of lines:
|
|
# vim-plugins vim-airline
|
|
# vim-plugins vim-airline-themes
|
|
# vim-plugins vim-ale
|
|
# ...
|
|
sync_groups_re = re.compile(r'^\s*(?P<group>\S+)\s+(?P<pkg>\S+)\s*$')
|
|
for l in stdout.splitlines():
|
|
sync_groups_match = sync_groups_re.match(l)
|
|
if not sync_groups_match:
|
|
continue
|
|
group, pkg = sync_groups_match.groups()
|
|
available_groups[group].add(pkg)
|
|
|
|
upgradable_pkgs = {}
|
|
rc, stdout, stderr = self.m.run_command(
|
|
[self.pacman_path, "--query", "--upgrades"], check_rc=False
|
|
)
|
|
|
|
stdout = stdout.splitlines()
|
|
if stdout and "Avoid running" in stdout[0]:
|
|
stdout = stdout[1:]
|
|
stdout = "\n".join(stdout)
|
|
|
|
# non-zero exit with nothing in stdout -> nothing to upgrade, all good
|
|
# stderr can have warnings, so not checked here
|
|
if rc == 1 and not stdout:
|
|
pass # nothing to upgrade
|
|
elif rc == 0:
|
|
# Format of lines:
|
|
# strace 5.14-1 -> 5.15-1
|
|
# systemd 249.7-1 -> 249.7-2 [ignored]
|
|
for l in stdout.splitlines():
|
|
l = l.strip()
|
|
if not l:
|
|
continue
|
|
if "[ignored]" in l or "Avoid running" in l:
|
|
continue
|
|
s = l.split()
|
|
if len(s) != 4:
|
|
self.fail(msg="Invalid line: %s" % l)
|
|
|
|
pkg = s[0]
|
|
current = s[1]
|
|
latest = s[3]
|
|
upgradable_pkgs[pkg] = VersionTuple(current=current, latest=latest)
|
|
else:
|
|
# stuff in stdout but rc!=0, abort
|
|
self.fail(
|
|
"Couldn't get list of packages available for upgrade",
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
rc=rc,
|
|
)
|
|
|
|
pkg_reasons = {}
|
|
dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--explicit"], check_rc=True)
|
|
# Format of a line: "pacman 6.0.1-2"
|
|
for l in stdout.splitlines():
|
|
l = l.strip()
|
|
if not l:
|
|
continue
|
|
pkg = l.split()[0]
|
|
pkg_reasons[pkg] = "explicit"
|
|
dummy, stdout, dummy = self.m.run_command([self.pacman_path, "--query", "--deps"], check_rc=True)
|
|
# Format of a line: "pacman 6.0.1-2"
|
|
for l in stdout.splitlines():
|
|
l = l.strip()
|
|
if not l:
|
|
continue
|
|
pkg = l.split()[0]
|
|
pkg_reasons[pkg] = "dependency"
|
|
|
|
return dict(
|
|
installed_pkgs=installed_pkgs,
|
|
installed_groups=installed_groups,
|
|
available_pkgs=available_pkgs,
|
|
available_groups=available_groups,
|
|
upgradable_pkgs=upgradable_pkgs,
|
|
pkg_reasons=pkg_reasons,
|
|
)
|
|
|
|
|
|
def setup_module():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
name=dict(type="list", elements="str", aliases=["pkg", "package"]),
|
|
state=dict(
|
|
type="str",
|
|
default="present",
|
|
choices=["present", "installed", "latest", "absent", "removed"],
|
|
),
|
|
force=dict(type="bool", default=False),
|
|
remove_nosave=dict(type="bool", default=False),
|
|
executable=dict(type="str", default="pacman"),
|
|
extra_args=dict(type="str", default=""),
|
|
upgrade=dict(type="bool"),
|
|
upgrade_extra_args=dict(type="str", default=""),
|
|
update_cache=dict(type="bool"),
|
|
update_cache_extra_args=dict(type="str", default=""),
|
|
reason=dict(type="str", choices=["explicit", "dependency"]),
|
|
reason_for=dict(type="str", default="new", choices=["new", "all"]),
|
|
),
|
|
required_one_of=[["name", "update_cache", "upgrade"]],
|
|
mutually_exclusive=[["name", "upgrade"]],
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
# Split extra_args as the shell would for easier handling later
|
|
for str_args in ["extra_args", "upgrade_extra_args", "update_cache_extra_args"]:
|
|
module.params[str_args] = shlex.split(module.params[str_args])
|
|
|
|
return module
|
|
|
|
|
|
def main():
|
|
|
|
Pacman(setup_module()).run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|