Source code for meta_package_manager.managers.dnf

# 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 extra_platforms import UNIX_WITHOUT_MACOS

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 DNF(PackageManager): """Fedora's RPM package manager. `mpm` reads the inventory through `repoquery` rather than the human-facing listing: `--userinstalled` for packages installed on request (dependencies pulled in automatically are skipped) and `--upgrades` for pending updates, both with a `--queryformat` that joins the fields on a private `___MPM___` delimiter so summaries containing spaces stay splittable. Every call is forced `--color=never` and `--quiet` for parseable output. ```{note} `remove` runs `autoremove`, so removing a package also drops the dependencies it leaves orphaned. `search` matches names only, with no exact or extended mode. ``` The `DNF5` and `YUM` subclasses reuse everything here, differing only in the binary and forced arguments. Documentation: - [DNF command reference](https://dnf.readthedocs.io/en/latest/command_ref.html) - [Command equivalences with other managers](https://wiki.archlinux.org/title/Pacman/Rosetta) """ maintenance_note: str | None = ( "DNF 4 is superseded by " "[dnf5](https://github.com/rpm-software-management/dnf5) (Fedora's default " "since Fedora 41) but stays maintained for the RHEL 8/9 family; mpm wraps " "`dnf5` as a separate manager." ) name = "Fedora DNF" homepage_url = "https://github.com/rpm-software-management/dnf" logo = "fedora" platforms = UNIX_WITHOUT_MACOS default_sudo = True requirement = ">=4.0.0" cli_names: tuple[str, ...] = ("dnf", "dnf4") """ ```{code-block} shell-session $ dnf --version 4.9.0 ``` """ pre_args: tuple[str, ...] = ("--color=never", "--quiet") _ORPHANS_REGEXP = re.compile( r"^(?P<package_id>\S+)-(?:\d+:)?(?P<installed_version>[^-\s]+-[^-\s]+)" r"\.(?P<arch>[^.\s]+)$", re.MULTILINE, ) """Split `repoquery`'s NEVRA lines (`name-[epoch:]version-release.arch`). RPM forbids dashes in the version and release fields, so the last two dash-separated fields before the `.arch` suffix are unambiguously `version-release`, dashes in the package name notwithstanding. """ _SEARCH_REGEXP = re.compile(r"(\S+)\.\S+\s:\s(\S+)") DELIMITER = "___MPM___" @property def installed(self) -> Iterator[Package]: """Fetch installed packages. ```{code-block} shell-session $ dnf repoquery --userinstalled --qf FORMAT acl___MPM___2.2.53-1.el8___MPM___Access control list utilities___MPM___x86_64 audit___MPM___3.0.7-4.el9___MPM___User space auditing tools___MPM___x86_64 audit-libs___MPM___3.0.7-4.el9___MPM___Dynamic auditing library___MPM___x86_64 ``` """ qf = ["%{name}", "%{version}", "%{summary}", "%{arch}\n"] output = self.run_cli( "repoquery", "--userinstalled", "--qf", self.DELIMITER.join(qf) ) for line_package in output.splitlines(): # remove empty new line if not line_package: continue package_id, installed_version, summary, arch = line_package.split( self.DELIMITER ) yield self.package( id=package_id, description=summary, installed_version=installed_version, arch=arch, ) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. ```{code-block} shell-session $ dnf repoquery --upgrades --qf FORMAT acl___MPM___2.2.53-1.el8___MPM___2.6.53-1.el8___MPM___Access control list utilities___MPM___x86_64 audit___MPM___2.2.53-1.el8___MPM___2.5.53-1.el8___MPM___User space auditing tools___MPM___x86_64 audit-libs___MPM___2.2.53-1.el8___MPM___2.6.53-1.el8___MPM___Dynamic auditing library___MPM___x86_64 ``` """ qf = ["%{name}", "%{version}", "%{evr}", "%{summary}", "%{arch}\n"] output = self.run_cli( "repoquery", "--upgrades", "--qf", self.DELIMITER.join(qf) ) for line_package in output.splitlines(): # remove empty new line if not line_package: continue package_id, installed_version, last_version, summary, arch = ( line_package.split(self.DELIMITER) ) yield self.package( id=package_id, description=summary, installed_version=installed_version, arch=arch, latest_version=last_version, ) @property def orphans(self) -> Iterator[Package]: """Fetch packages installed as dependencies that nothing requires anymore. ```{code-block} shell-session $ dnf --color=never --quiet repoquery --unneeded libfoo-1.0.2-3.el9.x86_64 python3-extra-0:3.9.18-3.el9.noarch ``` """ output = self.run_cli("repoquery", "--unneeded") yield from self.parse_regex_lines(self._ORPHANS_REGEXP, output)
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. ```{caution} Search does not support extended or exact matching. So we return the best subset of results and let {meth}`meta_package_manager.manager.PackageManager.refiltered_search` refine them. ``` ```{code-block} shell-session $ dnf --color=never search usd Last metadata expiration check: 0:06:37 ago on Sun 03 Apr 2022. =================== Name Exactly Matched: usd ===================== usd.aarch64 : 3D VFX pipeline interchange file format =================== Name & Summary Matched: usd =================== python3-usd.aarch64 : Development files for USD usd-devel.aarch64 : Development files for USD ======================= Name Matched: usd ========================= lvm2-dbusd.noarch : LVM2 D-Bus daemon usd-libs.aarch64 : Universal Scene Description library ``` """ output = self.run_cli("search", query) for line in output.splitlines()[1:]: # Skip section headers. if line.startswith("="): continue # Extract package ID and description. match = self._SEARCH_REGEXP.match(line) if match: package_id, description = match.groups() yield self.package(id=package_id, description=description)
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. ```{code-block} shell-session $ sudo dnf --color=never --quiet --assumeyes install pip ``` """ return self.run_cli("--assumeyes", "install", package_id, sudo=True)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all outdated packages. ```{code-block} shell-session $ sudo dnf --color=never --quiet --assumeyes upgrade ``` """ return self.build_cli("--assumeyes", "upgrade", sudo=True)
[docs] @version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade the provided package. ```{code-block} shell-session $ sudo dnf --color=never --quiet --assumeyes upgrade pip ``` """ return self.build_cli("--assumeyes", "upgrade", package_id, sudo=True)
[docs] def remove(self, package_id: str) -> str: """Remove one package and one only. ```{code-block} shell-session $ sudo dnf --color=never --quiet --assumeyes remove pip ``` """ return self.run_cli("--assumeyes", "remove", package_id, sudo=True)
[docs] def remove_orphan(self, package_id: str) -> str: """Remove one package, dropping dependencies it alone pulled in. `autoremove` targets the package plus the dependencies that were installed to satisfy it and are no longer required by anything else. ```{code-block} shell-session $ sudo dnf --color=never --quiet --assumeyes autoremove pip ``` """ return self.run_cli("--assumeyes", "autoremove", package_id, sudo=True)
[docs] def sync(self) -> None: """Sync package metadata. ```{code-block} shell-session $ dnf --color=never --quiet check-update ``` """ self.run_cli("check-update")
[docs] def cleanup_orphan(self) -> None: """Remove every package installed as a dependency and no longer required. ```{code-block} shell-session $ sudo dnf --color=never --quiet --assumeyes autoremove ``` """ self.run_cli("--assumeyes", "autoremove", sudo=True)
[docs] def cleanup_cache(self) -> None: """Clear the cached packages and repository metadata. ```{code-block} shell-session $ sudo dnf --color=never --quiet clean all ``` """ self.run_cli("clean", "all", sudo=True)
[docs] def doctor_cli(self) -> tuple[str, ...]: """Generates the CLI running the native self-diagnosis. `check` examines the rpm database for problems (duplicates, obsoleted packages, unsatisfied dependencies) and exits non-zero when any is found. ```{code-block} shell-session $ dnf --color=never --quiet check ``` """ return self.build_cli("check")
[docs] class DNF5(DNF): """The `dnf5` rewrite of DNF, Fedora's reference package manager since Fedora 41. Inherits every operation and parser from `DNF`. Its forced arguments drop `--color=never` (`dnf5` rejects that option), keeping only `--quiet`. """ # dnf5 is actively developed: clear the maintenance note inherited from DNF. maintenance_note = None name = "Fedora DNF5" homepage_url = "https://github.com/rpm-software-management/dnf5" logo = "fedora" requirement = ">=5.0.0" """dnf5 is the new reference package manager as of Fedora 41.""" cli_names = ("dnf5",) pre_args = ("--quiet",) """Reset global options inherited from the `DNF` above. `dnf5` does not support `--color=never` parameter. """
[docs] class YUM(DNF): """YUM, the package manager DNF superseded. On current Fedora and RHEL the `yum` binary is a wrapper around `dnf`. `mpm` drives it exactly as `DNF`, only the binary name differs. """ maintenance_note = ( "The standalone [yum project is archived]" "(https://github.com/rpm-software-management/yum); on modern RHEL and Fedora " "the `yum` command is a maintained compatibility alias for " "[dnf](https://github.com/rpm-software-management/dnf)." ) name = "Fedora YUM" homepage_url = "http://yum.baseurl.org" logo = "fedora" cli_names = ("yum",)