# 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 LINUX_LIKE
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 XBPS(PackageManager):
"""X Binary Package System used by Void Linux.
```{note}
XBPS is split across several sibling binaries: `xbps-query` for
read-only operations, `xbps-install` for installs, sync and
upgrades, and `xbps-remove` for uninstalls and cache cleanup.
`mpm` resolves the siblings from the same directory as
{attr}`cli_path
<meta_package_manager.execution.CLIExecutor.cli_path>`.
```
"""
name = "Void XBPS"
homepage_url = "https://github.com/void-linux/xbps"
logo = "voidlinux"
platforms = LINUX_LIKE
default_sudo = True
requirement = ">=0.59"
"""Version 0.59 is the first to ship the long-form options
(`--list-pkgs`, `--repository`, `--search`, `--update`,
`--dry-run`, `--sync`, `--yes`, `--clean-cache`,
`--remove-orphans`) that the methods below depend on.
"""
cli_names = ("xbps-install",)
"""Use `xbps-install` as the canonical entry point.
The other XBPS binaries (`xbps-query`, `xbps-remove`) are looked up
in the same directory as `xbps-install` via
{attr}`cli_path
<meta_package_manager.execution.CLIExecutor.cli_path>`.
"""
_INSTALLED_REGEXP = re.compile(
r"^ii\s+(?P<pkgver>\S+)\s+(?P<description>.+)$",
re.MULTILINE,
)
"""Match installed entries from `xbps-query --list-pkgs` output.
The first column is a two-character state code: `ii` (installed),
`uu` (unpacked, awaiting configuration), `hr` (half-removed) or
`??` (unknown). Only `ii` packages are reported as installed.
"""
_OUTDATED_REGEXP = re.compile(
r"^(?P<pkgver>\S+)\s+update\s+\S+\s+\S+\s+\S+\s+\S+\s*$",
re.MULTILINE,
)
"""Match update entries from `xbps-install --update --dry-run` output.
Each line has the format
`<pkgver> <action> <arch> <repository> <installedsize> <downloadsize>`.
Only `update` actions are kept, skipping `install`, `configure` and
`remove` entries that may also appear in a transaction.
"""
_SEARCH_REGEXP = re.compile(
r"^\[[\*\-]\]\s+(?P<pkgver>\S+)\s+(?P<description>.+)$",
re.MULTILINE,
)
"""Match search entries from `xbps-query --repository --search` output.
Each line is prefixed with `[*]` (already installed) or `[-]`
(available in the repository).
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
```{code-block} shell-session
$ xbps-query --list-pkgs
ii base-files-0.144_1 Void Linux base system files
ii cmark-gfm-0.29.0.gfm.13_1 CommonMark parsing and rendering library
ii curl-8.5.0_1 Command line tool for transferring data
```
"""
output = self.run_cli(
"--list-pkgs",
override_cli_path=self.sibling_cli("xbps-query", same_dir=True),
)
for match in self._INSTALLED_REGEXP.finditer(output):
if split := self.split_name_version(match.group("pkgver")):
package_id, version = split
yield self.package(
id=package_id,
description=match.group("description").strip(),
installed_version=version,
)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
```{caution}
Reads from the local repository cache. Run {meth}`sync` first
to refresh the index.
```
```{code-block} shell-session
$ xbps-install --update --dry-run
firefox-120.0_1 update x86_64 https://repo-default.voidlinux.org/current 45MB 12MB
python3-3.11.6_2 update x86_64 https://repo-default.voidlinux.org/current 30MB 8MB
```
"""
installed_versions = self.installed_version_map
output = self.run_cli("--update", "--dry-run")
for match in self._OUTDATED_REGEXP.finditer(output):
if split := self.split_name_version(match.group("pkgver")):
package_id, version = split
yield self.package(
id=package_id,
installed_version=installed_versions.get(package_id),
latest_version=version,
)
@property
def orphans(self) -> Iterator[Package]:
"""Fetch packages installed as dependencies that nothing requires anymore.
```{code-block} shell-session
$ xbps-query --list-orphans
libglvnd-1.7.0_1
orc-0.4.34_1
```
"""
output = self.run_cli(
"--list-orphans",
override_cli_path=self.sibling_cli("xbps-query", same_dir=True),
)
for token in output.splitlines():
if split := self.split_name_version(token.strip()):
package_id, version = split
yield self.package(id=package_id, installed_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.
```{caution}
`xbps-query --search` matches against `pkgver` and
`short_desc` properties at the same time. Extended and exact
matching are not supported, so the best subset of results is
returned and refined later by
{meth}`meta_package_manager.manager.PackageManager.refiltered_search`.
```
```{code-block} shell-session
$ xbps-query --repository --search firefox
[-] firefox-120.0_1 Standalone web browser from mozilla.org
[*] firefox-esr-115.5.0_1 Extended support release of Firefox
```
"""
output = self.run_cli(
"--repository",
"--search",
query,
override_cli_path=self.sibling_cli("xbps-query", same_dir=True),
)
for match in self._SEARCH_REGEXP.finditer(output):
if split := self.split_name_version(match.group("pkgver")):
package_id, version = split
yield self.package(
id=package_id,
description=match.group("description").strip(),
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
$ sudo xbps-install --yes firefox
```
"""
return self.run_cli("--yes", package_id, sudo=True)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all packages.
```{code-block} shell-session
$ sudo xbps-install --sync --update --yes
```
"""
return self.build_cli("--sync", "--update", "--yes", 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 one package.
```{code-block} shell-session
$ sudo xbps-install --update --yes firefox
```
"""
return self.build_cli("--update", "--yes", package_id, sudo=True)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package and one only.
```{code-block} shell-session
$ sudo xbps-remove --yes firefox
```
"""
return self.run_cli(
"--yes",
package_id,
override_cli_path=self.sibling_cli("xbps-remove", same_dir=True),
sudo=True,
)
[docs]
def remove_orphan(self, package_id: str) -> str:
"""Remove one package, recursively dropping orphaned dependencies.
```{code-block} shell-session
$ sudo xbps-remove --recursive --yes firefox
```
"""
return self.run_cli(
"--recursive",
"--yes",
package_id,
override_cli_path=self.sibling_cli("xbps-remove", same_dir=True),
sudo=True,
)
[docs]
def sync(self) -> None:
"""Synchronize remote repository indexes.
```{code-block} shell-session
$ sudo xbps-install --sync --yes
```
"""
self.run_cli("--sync", "--yes", sudo=True)
[docs]
def cleanup_orphan(self) -> None:
"""Remove installed packages no longer required by any other, sparing the cache.
```{code-block} shell-session
$ sudo xbps-remove --remove-orphans --yes
```
"""
self.run_cli(
"--remove-orphans",
"--yes",
override_cli_path=self.sibling_cli("xbps-remove", same_dir=True),
sudo=True,
)
[docs]
def cleanup_cache(self) -> None:
"""Clean the binary package cache, sparing the orphans.
```{code-block} shell-session
$ sudo xbps-remove --clean-cache --yes
```
"""
self.run_cli(
"--clean-cache",
"--yes",
override_cli_path=self.sibling_cli("xbps-remove", same_dir=True),
sudo=True,
)
[docs]
def doctor_cli(self) -> tuple[str, ...]:
"""Generates the CLI running the native self-diagnosis.
`xbps-pkgdb --all` checks the integrity of the package database and of
every installed package, exiting non-zero on errors.
```{code-block} shell-session
$ xbps-pkgdb --all
```
"""
return self.build_cli(
"--all",
override_cli_path=self.sibling_cli("xbps-pkgdb", same_dir=True),
)