Source code for meta_package_manager.managers.pkcon

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

from __future__ import annotations

import re
from typing import ClassVar

from extra_platforms import LINUX

from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterator

    from ..package import Package


[docs] class Pkcon(PackageManager): """PackageKit's console client. Documentation: - https://www.freedesktop.org/software/PackageKit/ - https://github.com/PackageKit/PackageKit pkcon is an unprivileged client handing transactions to the ``packagekitd`` daemon over D-Bus, with the distro's native backend (apt, dnf, zypp, alpm, ...) doing the real work. Escalation is polkit's job, so no operation is marked ``sudo``: under a strict polkit policy, unattended mutations need a policy permitting them without interactive authentication. .. note:: pkcon renders packages as a fused ``name-version.arch (repo)`` string and never exposes the raw ``name;version;arch;repo`` ID. Names and versions both legitimately contain dashes, so the name/version split below anchors on the first dash followed by a digit: a documented heuristic, not an exact science. """ name = "PackageKit" homepage_url = "https://www.freedesktop.org/software/PackageKit/" platforms = LINUX requirement = ">=0.7.0" """All the commands and flags used here (``--plain``, ``--noninteractive``, ``--filter``) are present since PackageKit ``0.7.0``.""" # Keep gettext-localized status words and preamble labels in English so the # parsing regexes hold. extra_env: ClassVar = {"LC_ALL": "C"} # --plain forces the machine-readable output branch even on a terminal. post_args = ("--plain",) version_regexes = (r"^(?P<version>[\d.]+)$",) """ .. code-block:: shell-session $ pkcon --version 1.3.6 """ _RESULT_REGEXP = re.compile( r"^(?P<status>\w[\w ]*?) {2,}(?P<blob>\S+) \((?P<repo>[^)]+)\)$", ) """One result line: a status word left-justified to 12 characters, the fused package rendering, then the repository in parentheses. The transient progress preamble (``Transaction:``, ``Package:``, ``Percentage:`` labels) is tab-separated and unparenthesized, so it never matches.""" _NAME_VERSION_REGEXP = re.compile(r"^(?P<package_id>.+?)-(?P<version>\d.*)$") """Split the fused ``name-version`` on the first dash followed by a digit.""" def _parse_results(self, output: str) -> Iterator[tuple[str, str, str | None]]: """Yield ``(status, package_id, version)`` from pkcon result lines. The trailing ``.arch`` component is dropped before splitting the name from the version. """ for line in output.splitlines(): match = self._RESULT_REGEXP.match(line) if not match: continue name_version = match.group("blob").rsplit(".", 1)[0] if split := self.split_name_version(name_version): yield match.group("status"), *split else: yield match.group("status"), name_version, None @property def installed(self) -> Iterator[Package]: """Fetch installed packages. .. code-block:: shell-session $ pkcon get-packages --filter installed --plain Installed gzip-1.12-1.fc38.x86_64 (koji-override-0) Installed hello-2.12.1-2.fc38.x86_64 (fedora) """ output = self.run_cli("get-packages", "--filter", "installed") for status, package_id, version in self._parse_results(output): if status == "Installed": yield self.package(id=package_id, installed_version=version) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. Result lines carry the update *type* as their status word (``Security``, ``Bug fix``, ``Enhancement``, ``Normal``, ...) and the version of the pending update. .. caution:: With nothing to update, pkcon prints ``There are no updates available at this time.`` and exits ``5`` (``PK_EXIT_CODE_NOTHING_USEFUL``): a normal empty result, not a failure, so the error recorded for it is discarded. .. code-block:: shell-session $ pkcon get-updates --plain Security curl-8.0.1-2.fc38.x86_64 (updates) Normal hello-2.12.2-1.fc38.x86_64 (updates) """ errors_before = len(self.cli_errors) output = self.run_cli("get-updates") if len(self.cli_errors) > errors_before: error = self.cli_errors[-1] if error.code == 5: self.cli_errors.pop() for _status, package_id, version in self._parse_results(output): yield self.package(id=package_id, latest_version=version)
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. .. code-block:: shell-session $ pkcon search name hello --plain Available hello-2.12.1-2.fc38.x86_64 (fedora) Installed rubygem-mixlib-shellout-3.2.7-3.fc38.noarch (fedora) """ output = self.run_cli("search", "name", query) for _status, package_id, version in self._parse_results(output): yield self.package(id=package_id, latest_version=version)
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. .. code-block:: shell-session $ pkcon install --noninteractive hello --plain """ return self.run_cli("install", "--noninteractive", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. .. note:: With nothing to upgrade this exits ``5`` ("nothing useful was done"), which the best-effort maintenance flow reports as a failed manager but never as a non-zero mpm exit. .. code-block:: shell-session $ pkcon update --noninteractive --plain """ return self.build_cli("update", "--noninteractive")
[docs] @version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade one package. .. code-block:: shell-session $ pkcon update --noninteractive hello --plain """ return self.build_cli("update", "--noninteractive", package_id)
[docs] def remove(self, package_id: str) -> str: """Remove one package. .. code-block:: shell-session $ pkcon remove --noninteractive hello --plain """ return self.run_cli("remove", "--noninteractive", package_id)
[docs] def sync(self) -> None: """Refresh the cached repository metadata. .. code-block:: shell-session $ pkcon refresh --plain """ self.run_cli("refresh")