Source code for meta_package_manager.managers.zypper

# 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

from itertools import groupby
from operator import itemgetter
from typing import Iterator, TypedDict

import xmltodict
from extra_platforms import UNIX_WITHOUT_MACOS

from meta_package_manager.base import Package, PackageManager
from meta_package_manager.capabilities import version_not_implemented
from meta_package_manager.version import TokenizedString, parse_version


[docs] class Zypper(PackageManager): """Zypper package manager. Documentation: - https://en.opensuse.org/Portal:Zypper - https://documentation.suse.com/smart/linux/html/concept-zypper/index.html - https://opensuse.github.io/openSUSE-docs-revamped-temp/zypper/ See other command equivalences at: https://wiki.archlinux.org/title/Pacman/Rosetta """ homepage_url = "https://en.opensuse.org/Portal:Zypper" platforms = UNIX_WITHOUT_MACOS requirement = "1.14.0" pre_args = ( "--no-color", "--no-abbrev", "--non-interactive", "--no-cd", "--no-refresh", ) version_regex = r"zypper\s+(?P<version>\S+)" """ .. code-block:: shell-session ► zypper --version zypper 1.14.11 """
[docs] class SearchResult(TypedDict): id: str version: TokenizedString
def _search(self, *args: str) -> Iterator[SearchResult]: """Utility method to parse and interpret results of the ``zypper search`` command. This is reused by the ``installed`` and ``search`` operations. .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout search --details --type package [*args] <?xml version='1.0'?> <stream> <message type="info">Loading repository data...</message> <message type="info">Reading installed packages...</message> <search-result version="0.0"> <solvable-list> <solvable status="installed" name="aaa_base" kind="package" edition="12.12-bp12.3.1" arch="x86_64"/> <solvable status="installed" name="adwaita-icon-theme" kind="package" edition="1.0.3-bp153.1.1" arch="x86_64"/> <solvable status="other-version" name="adwaita-icon-theme" kind="package" edition="1.0.1-bp153.1.1" arch="x86_64"/> <solvable status="not-installed" name="kopete-devel" kind="package" edition="20.04.2-bp153.2.5.1" arch="x86_64" repository="Update"/> <solvable status="not-installed" name="kopete-devel" kind="package" edition="20.04.2-bp153.2.2.1" arch="x86_64" repository="Update"/> <solvable status="not-installed" name="kopete-devel" kind="package" edition="20.04.2-bp153.2.2.1" arch="x86_64" repository="Debug"/> <solvable status="not-installed" name="kopete-devel" kind="package" edition="20.04.2-bp153.2.5.1" arch="i586" repository="Update"/> <solvable status="not-installed" name="kopete-devel" kind="package" edition="20.04.2-bp153.2.2.1" arch="i586" repository="Update"/> (...) </solvable-list> </search-result> </stream> """ output = self.run_cli( "--xmlout", "search", # --details is the only option that is providing the package's version... "--details", # ...but comes with duplicate results due to source packages, different arch # and old releases. So we filters them out to only keep proper packages. "--type", "package", # Additional search arguments. *args, ) if not output: return package_list = ( xmltodict.parse(output) .get("stream", {}) .get("search-result", {}) .get("solvable-list", {}) .get("solvable", []) ) # Group packages by ID. key_func = itemgetter("@name") # Skip old packages reported in the results as 'other-version'. fresh_packages = sorted( (p for p in package_list if p.get("@status") != "other-version"), key=key_func, ) # Returns the highest version for a package ID among all repositories and # arch variations. for key, group in groupby(fresh_packages, key_func): yield { "id": key, "version": max(parse_version(p["@edition"]) for p in group), } @property def installed(self) -> Iterator[Package]: """Fetch installed packages. .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout search --details --type package --installed-only """ for package in self._search("--installed-only"): yield self.package(id=package["id"], installed_version=package["version"]) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout list-updates <?xml version='1.0'?> <stream> <message type="info">Loading repository data...</message> <message type="info">Reading installed packages...</message> <update-status version="0.6"> <update-list> <update name="git" kind="package" edition="2.34.1-10.9.1" edition-old="2.26.2-3.34.1" arch="x86_64"> <summary>Fast, scalable revision control system</summary> <description> Blah blah blah... </description> <license/> <source url="http://download.opensuse.org/updata/leap/15.3/sle" alias="repo-sle-update"/> </update> (...) </update-list> </update-status> </stream> """ output = self.run_cli("--xmlout", "list-updates") if not output: return package_list = [] update_list = ( xmltodict.parse(output) .get("stream", {}) .get("update-status", {}) .get("update-list", {}) ) if update_list: package_list = update_list.get("update", []) for package in package_list: yield self.package( id=package["@name"], description=package.get("description"), latest_version=package["@edition"], installed_version=package["@edition-old"], )
[docs] def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout search --details --type package kopete .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout search --details --type package --search-description kopete .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout search --details --type package --match-exact kopete .. code-block:: shell-session ► zypper --no-color --no-abbrev --non-interactive --no-cd --no-refresh \ --xmlout search --details --type package --search-description \ --match-exact kopete """ search_param = [] if extended: search_param.append("--search-description") if exact: search_param.append("--match-exact") for package in self._search(*search_param, query): yield self.package(id=package["id"], installed_version=package["version"])
@version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. .. code-block:: shell-session ► sudo zypper --no-color --no-abbrev --non-interactive --no-cd \ --no-refresh install kopete """ return self.run_cli("install", package_id, sudo=True)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages (default) or only the one provided as parameter. .. code-block:: shell-session ► sudo zypper --no-color --no-abbrev --non-interactive --no-cd \ --no-refresh update """ return self.build_cli("update", sudo=True)
@version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade all packages (default) or only the one provided as parameter. .. code-block:: shell-session ► sudo zypper --no-color --no-abbrev --non-interactive --no-cd \ --no-refresh update kopete """ return self.build_cli("update", package_id, sudo=True)
[docs] def sync(self) -> None: """Sync package metadata. .. code-block:: shell-session ► sudo zypper --no-color --no-abbrev --non-interactive --no-cd \ --no-refresh refresh """ self.run_cli("refresh", sudo=True)
[docs] def cleanup(self) -> None: """Removes things we don't need anymore. .. code-block:: shell-session ► sudo zypper --no-color --no-abbrev --non-interactive --no-cd \ --no-refresh clean """ self.run_cli("clean", sudo=True)