# Copyright Kevin Deldycke <[email protected]> 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 version_not_implemented
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Pmerge(PackageManager):
"""pkgcore's `pmerge`, an alternative package manager for Gentoo.
pkgcore reads the same ebuild repositories as Portage and records into the
same database of installed packages. On a host that carries both, this
manager and [`emerge`](emerge.md) list the same packages, and each one sees
what the other installed.
The queries run through `pquery`, the inventory and search tool of pkgcore,
and the maintenance through `pmaint` and `pclean`. All four ship together.
A fresh stage3 ships `/var/cache/distfiles` owned by `root` with mode
`0755`, and every `pmerge` fetch then fails with `Permission denied`.
Give the directory to the `portage` user first:
`chown portage:portage /var/cache/distfiles`.
:::{caution}
Installs and upgrades compile from source, so their duration comes from the
package rather than from the network. A small program finishes well inside
the 500 second ceiling `mpm` puts on a state-changing command, where a
compiler or a browser does not. Raise the ceiling for this manager alone, in
the configuration file:
```toml
[mpm.overrides.pmerge]
timeout = 14400
```
:::
"""
name = "pkgcore pmerge"
documentation_url = "https://pkgcore.github.io/pkgcore/man/pmerge.html"
repository_url = "https://github.com/pkgcore/pkgcore"
logo = "gentoo"
keywords = ("pkgcore",)
platforms = UNIX_WITHOUT_MACOS
default_sudo = True
privileged_operations = frozenset(
{"cleanup", "install", "remove", "sync", "upgrade", "upgrade_all"},
)
# `pquery --format`, which every query below prints its rows with, first
# shipped in pkgcore 0.12.29.
requirement = ">=0.12.29"
# A base option of all four pkgcore tools, so it reaches the siblings too.
pre_args = ("--color", "no")
version_regexes = (r"pkgcore\s+(?P<version>\S+)",)
"""
```{code-block} shell-session
$ pmerge --version
pkgcore 0.12.48
```
"""
_INSTALLED_FORMAT = "{category}/{package}={fullver}"
"""`pquery` row of the inventory, free of spaces so the disclosed command
replays as printed."""
_INSTALLED_REGEXP = re.compile(
r"^(?P<package_id>[^/\s]+/(?P<name>[^=\s]+))=(?P<installed_version>\S+)$",
)
"""The package ID is `category/name`, and the bare `name` is captured too:
it is what an exact query for a package matches, the category aside."""
_SLOTTED_FORMAT = "{category}/{package}:{slot}={fullver}"
"""`pquery` row carrying the slot, the key `outdated` joins its two listings
on: `pquery --upgrade` compares each installed package with the best version
of its own slot."""
_SLOTTED_REGEXP = re.compile(
r"^(?P<package_id>[^:\s]+):(?P<slot>[^=\s]+)=(?P<version>\S+)$",
re.MULTILINE,
)
_SEARCH_FORMAT = "{category}/{package}={fullver}={description}"
_SEARCH_REGEXP = re.compile(
r"^(?P<package_id>[^=\s]+)=(?P<latest_version>[^=\s]+)"
r"=(?P<description>.*)$",
re.MULTILINE,
)
_ORPHANS_REGEXP = re.compile(
r"^Remove installed pkg: (?P<cpv>[^:\s]+)::",
re.MULTILINE,
)
"""`--pretend --clean` prints each package it would remove as `Remove`
followed by the package object, whose `category/name-version` is split
downstream through
{meth}`~meta_package_manager.manager.PackageManager.split_name_version`."""
_GLOBBABLE_QUERY = re.compile(r"^[\w.+*-]+(?:/[\w.+*-]+)?$")
"""A query `pquery` accepts inside a name glob.
Any other character, like a space, makes `pquery` refuse the glob ("globs
must be composed of [\\w-.+], with optional '*'"), and no package name
holds one anyway."""
_REGEX_METACHARACTERS = re.compile(r"([\\.^$*+?{}\[\]|()])")
"""Characters `pquery --description` reads as regular expression syntax.
Narrower than {func}`re.escape`, which also escapes `-` and spaces: a
regular expression reads both literally outside a character class, and the
disclosed command stays as the user typed it."""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
A package installed in several slots is listed once per slot.
```{code-block} shell-session
$ pquery --color no --installed --format {category}/{package}={fullver}
app-text/build-docbook-catalog=2.4
app-text/docbook-xml-dtd=4.1.2-r7
app-text/docbook-xml-dtd=4.2-r3
app-text/docbook-xml-dtd=4.5-r2
app-text/docbook-xsl-ns-stylesheets=1.79.1
app-text/docbook-xsl-stylesheets=1.79.1-r4
app-text/manpager=1
```
"""
output = self.run_cli(
"--installed",
"--format",
self._INSTALLED_FORMAT,
override_cli_path=self.sibling_cli("pquery", same_dir=True),
must_succeed=True,
)
yield from self.parse_regex_lines(self._INSTALLED_REGEXP, output)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
`pquery --upgrade` prints the best version of each slot that has a newer
one than the version installed in it, but not the installed version. The
inventory supplies it, joined on the package and its slot.
```{code-block} shell-session
$ pquery --color no --installed --format {category}/{package}:{slot}={fullver}
app-arch/libarchive:0=3.8.10
app-arch/tar:0=1.35-r1
app-arch/unzip:0=6.0_p31
app-arch/xz-utils:0=5.8.3
app-arch/zstd:0=1.5.7-r1
app-crypt/gnupg:0=2.5.24-r2
app-crypt/gpgme:1=2.2.0
app-crypt/libb2:0=0.98.1-r3
app-crypt/libmd:0=1.2.0
```
```{code-block} shell-session
$ pquery --color no --upgrade --format {category}/{package}:{slot}={fullver}
app-arch/libarchive:0=3.8.11
app-arch/xz-utils:0=5.8.4
app-crypt/libmd:0=1.3.0
```
"""
pquery_path = self.sibling_cli("pquery", same_dir=True)
installed = {
(match["package_id"], match["slot"]): match["version"]
for match in self._SLOTTED_REGEXP.finditer(
self.run_cli(
"--installed",
"--format",
self._SLOTTED_FORMAT,
override_cli_path=pquery_path,
auto_extra_args=False,
must_succeed=True,
)
)
}
output = self.run_cli(
"--upgrade",
"--format",
self._SLOTTED_FORMAT,
override_cli_path=pquery_path,
auto_extra_args=True,
must_succeed=True,
)
for match in self._SLOTTED_REGEXP.finditer(output):
yield self.package(
id=match["package_id"],
name=match["package_id"].rpartition("/")[2],
installed_version=installed.get((match["package_id"], match["slot"])),
latest_version=match["version"],
)
@property
def orphans(self) -> Iterator[Package]:
"""Fetch packages that neither the `@world` nor the `@system` set needs.
`--pretend` turns the `--clean` sweep into a read-only report. With an
empty `@world`, as on a fresh stage3, everything outside `@system` and
its dependencies is reported.
```{code-block} shell-session
$ pmerge --color no --pretend --clean
* Packages to be removed:
Remove installed pkg: app-alternatives/lex-0-r1::vdb, source repo 'gentoo'
Remove installed pkg: app-alternatives/ninja-1::vdb, source repo 'gentoo'
Remove installed pkg: app-alternatives/yacc-1-r2::vdb, source repo 'gentoo'
Remove installed pkg: app-arch/unzip-6.0_p31::vdb, source repo 'gentoo'
```
"""
output = self.run_cli("--pretend", "--clean", must_succeed=True)
for match in self._ORPHANS_REGEXP.finditer(output):
if split := self.split_name_version(match["cpv"]):
package_id, version = split
yield self.package(
id=package_id,
name=package_id.rpartition("/")[2],
installed_version=version,
)
[docs]
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
A query matches package names, and is a glob around the query unless the
search is exact. A query no package name can hold, like one with a
space, skips that call. An extended search adds a second call, matching
the query against descriptions: it reads the metadata of the whole
repository, and is the slow part.
```{code-block} shell-session
$ pquery --color no --max --format {category}/{package}={fullver}={description} nyancat
games-misc/nyancat=1.5.2=Nyancat in your terminal, rendered through ANSI escape sequences
```
```{code-block} shell-session
$ pquery --color no --max --format {category}/{package}={fullver}={description} *cowsay*
games-misc/cowsay=3.7.0=Configurable talking ASCII cow (and other characters)
games-misc/xcowsay=1.6=Displays a cute cow and message on your desktop
```
```{code-block} shell-session
$ pquery --color no --max --format {category}/{package}={fullver}={description} --description 'ANSI escape'
app-misc/color=1.2-r3=Easily add ANSI colouring to shell scripts
app-text/aha=0.5.1=Converts ANSI escape sequences of a unix terminal to HTML code
app-vim/ansiesc=12=vim plugin: ansi escape sequences concealed, but highlighted as specified
dev-java/jansi=2.4.1=A java library for generating and interpreting ANSI escape sequences
dev-libs/libuv=1.52.1=Cross-platform asynchronous I/O
dev-perl/Term-ANSIScreen=1.500.0-r2=Terminal control using ANSI escape sequences
dev-python/colorama=0.4.6=ANSI escape character sequences for colored terminal text & cursor positioning
dev-ruby/term-ansicolor=1.11.3-r1=Small Ruby library that colors strings using ANSI escape sequences
games-misc/nyancat=1.5.2=Nyancat in your terminal, rendered through ANSI escape sequences
```
""" # noqa: E501
pquery_path = self.sibling_cli("pquery", same_dir=True)
targets: list[tuple[str, ...]] = []
if self._GLOBBABLE_QUERY.match(query):
targets.append((query if exact else f"*{query}*",))
if extended:
targets.append((
"--description",
self._REGEX_METACHARACTERS.sub(r"\\\1", query),
))
seen = set()
for target in targets:
output = self.run_cli(
"--max",
"--format",
self._SEARCH_FORMAT,
*target,
override_cli_path=pquery_path,
auto_extra_args=True,
must_succeed=True,
)
for match in self._SEARCH_REGEXP.finditer(output):
if match["package_id"] in seen:
continue
seen.add(match["package_id"])
yield self.package(
id=match["package_id"],
name=match["package_id"].rpartition("/")[2],
description=match["description"],
latest_version=match["latest_version"],
)
[docs]
@version_not_implemented
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package, and record it in the `@world` set.
```{code-block} shell-session
$ sudo pmerge --color no games-misc/nyancat
```
"""
return self.run_cli(package_id, sudo=True)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all outdated packages.
The `@system` set rides along with `@world`: `pmerge` refuses an empty
target list, and a fresh stage3 records nothing in `@world`.
```{code-block} shell-session
$ sudo pmerge --color no --upgrade --deep --newuse @world @system
```
"""
return self.build_cli(
"--upgrade",
"--deep",
"--newuse",
"@world",
"@system",
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.
`--upgrade` leaves a package that is already current alone, and does not
add it to the `@world` set.
```{code-block} shell-session
$ sudo pmerge --color no --upgrade app-crypt/libmd
```
"""
return self.build_cli("--upgrade", package_id, sudo=True)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package, and drop it from the `@world` set.
```{code-block} shell-session
$ sudo pmerge --color no --unmerge games-misc/nyancat
```
"""
return self.run_cli("--unmerge", package_id, sudo=True)
[docs]
def sync(self) -> None:
"""Sync every configured repository.
```{code-block} shell-session
$ sudo pmaint --color no sync
*** syncing gentoo
*** synced gentoo
```
"""
self.run_cli(
"sync",
override_cli_path=self.sibling_cli("pmaint", same_dir=True),
sudo=True,
)
[docs]
def cleanup_orphan(self) -> None:
"""Remove every package outside the `@world` and `@system` sets.
The sweep stops at the first removal that fails, and `pmerge` still
exits `0`. On a stage3 it removed `app-text/build-docbook-catalog`, then
stopped on a DocBook package whose `pkg_postrm` calls that tool: seven
of the hundred orphans went. `mpm` counts what the sweep removed, and
`mpm orphans` lists what is left.
```{code-block} shell-session
$ sudo pmerge --color no --clean
```
"""
self.run_cli("--clean", sudo=True)
[docs]
def cleanup_cache(self) -> None:
"""Remove the source distfiles no installed package uses.
```{code-block} shell-session
$ sudo pclean --color no dist --installed
```
"""
self.run_cli(
"dist",
"--installed",
override_cli_path=self.sibling_cli("pclean", same_dir=True),
sudo=True,
)