Source code for meta_package_manager.managers.guix

# 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
from ..version import parse_version

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

    from ..package import Package


[docs] class Guix(PackageManager): """GNU Guix, GNU's functional package manager. ```{note} All operations target the current user's default profile. Declarative system configuration (Guix System `config.scm`) is not covered. ``` Guix is a rolling release with no upstream semver to pin against: the `guix` of a fresh install reports its release, and once `guix pull` ran, the commit it was built from. No `requirement` floor is enforced, since any working `guix` will do. :::{caution} `sync` runs `guix pull`, which outlasts the 500 second ceiling `mpm` puts on a state-changing command. A first pull authenticates the commit history of the channel, then builds Guix itself when no substitute is available yet: on an `aarch64` virtual machine, it took 28 minutes. Raise the ceiling for this manager alone, in the configuration file: ```toml [mpm.overrides.guix] timeout = 3600 ``` ::: ```{note} `guix pull` builds a cache of the package set, so `search` answers in about a second. A `guix` run from a development checkout has no such cache and evaluates every package definition instead: there a search can outlast the 120 second ceiling of a read-only command, which kills it with no results. ``` """ name = "GNU Guix" homepage_url = "https://guix.gnu.org" documentation_url = ( "https://guix.gnu.org/manual/en/html_node/Invoking-guix-package.html" ) repository_url = "https://codeberg.org/guix/guix" wikipedia_url = "https://en.wikipedia.org/wiki/GNU_Guix" logo = "gnu" keywords = ("gnu guix",) platforms = LINUX_LIKE # Guix is a rolling-release distribution: the only "version" it # exposes for `guix pull`-managed installs and in-tree dev wrappers # (`./pre-inst-env guix`, `./scripts/guix`) is a git commit hash. # No upstream tag/semver is guaranteed to be reachable, so capturing # whatever `guix --version` reports and *not* enforcing a # `requirement` specifier is the only honest option: any working # `guix` is fine. version_regexes = ( # Release, or `git describe` output of an in-tree checkout: # `guix (GNU Guix) 1.5.0` # `guix (GNU Guix) 1.4.0-7-gabc1234` r"guix \(GNU Guix\) (?P<version>\d[\w.\-+]*)", # The full commit hash of a pulled Guix, or the short one of an # in-tree checkout with no tag nearby. A hash opening on a digit # already matched above: # `guix (GNU Guix) 5ef098e1eef807cffba753b1fa335fab07ce4a79` # Restrict to a 7–40 hex run so corrupted output isn't accepted # as a "version". r"guix \(GNU Guix\) (?P<version>[0-9a-f]{7,40})\b", ) """ ```{code-block} shell-session $ guix --version guix (GNU Guix) 1.5.0 Copyright (C) 2025 the Guix authors License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. ``` """ _SEARCH_FIELD_REGEXP = re.compile( r"^(?P<field>\w[\w-]*):(?:\s+(?P<value>.*))?$", ) """Match a recutils field line (`name: value`), whose value may be empty.""" _SEARCH_OUTPUT_REGEXP = re.compile(r"^\+ (?P<output>[^\s:]+): ") """Match an output listed under the `outputs` field of a search record.""" _OUTDATED_REGEXP = re.compile( r"^\s+(?P<package_id>\S+)\s+(?P<installed_version>\S+)\s+(?:→|->)\s+" r"(?P<latest_version>\S+)$", ) """Match an upgrade row of the transaction `guix upgrade --dry-run` reports. Guix pads the name column, names an output other than `out` as `name:output`, and prints `->` in place of the arrow when `<stderr>` cannot encode it. A row whose version did not change reads `(dependencies or package changed)`, and is left out: no newer version is available. """ _REGEXP_SPECIALS = re.compile(r"([.^$*+?()\[\]{}|\\])") """Characters a POSIX extended regular expression reads as operators.""" def _name_regexp(self, name: str) -> str: """A regular expression matching the package name `name`, and no other.""" return "^" + self._REGEXP_SPECIALS.sub(r"\\\1", name) + "$" @property def installed(self) -> Iterator[Package]: """Fetch installed packages. Columns are tab-separated: name, version, output and store path. Guix pads the first three with spaces, which are stripped. An output other than `out` is an entry of its own, reported as `name:output`: the specification that `guix install` and `guix remove` take back. ```{code-block} shell-session $ guix package --list-installed hello 2.12.2 out /gnu/store/8qjzxdk26p8c5nyj98s3321vbkmv9za0-hello-2.12.2 lua 5.1.5 out /gnu/store/sq2sj9gj8bqzgv493249xd8gzhhw1f0x-lua-5.1.5 glib 2.83.3 out /gnu/store/h95r1yvlbrlqap6l8fsdzp7lpxair7sv-glib-2.83.3 glib 2.83.3 bin /gnu/store/06w4p65r8hli2qnsdsnp0l8r1zghz5qr-glib-2.83.3-bin glib-networking 2.78.1 out /gnu/store/n4nicicydz8d24m2qi3ddwlcxlr534qn-glib-networking-2.78.1 ``` """ output = self.run_cli("package", "--list-installed") for line in output.splitlines(): fields = [field.strip() for field in line.split("\t")] if len(fields) < 3 or not fields[0]: continue name, version, package_output = fields[:3] yield self.package( id=name if package_output == "out" else f"{name}:{package_output}", installed_version=version, ) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. `guix upgrade --dry-run` lists every package it would upgrade, and leaves the profile as it is. ```{important} The report is written to `<stderr>` while `<stdout>` stays empty, so it is read from the recorded run. ``` ```{code-block} shell-session $ guix upgrade --dry-run ``` ```{code-block} console The following packages would be upgraded: glib 2.83.3 → 2.86.0 glib-networking 2.78.1 → 2.80.1 glib:bin 2.83.3 → 2.86.0 hello 2.12.2 → 2.12.3 lua 5.1.5 → 5.5.0 22.6 MB would be downloaded ``` """ self.run_cli("upgrade", "--dry-run") last = self._last_run if last is None: return _code, _stdout, stderr = last yield from self.parse_regex_lines(self._OUTDATED_REGEXP, stderr)
[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. ``` Guix keeps some packages in several versions under one name, like `lua` from `5.1.5` to `5.4.8`. A name is reported once, at its newest version: the one `guix install` picks. A query naming an output, like `glib:bin`, reports that output of the package of that exact name. That is the form `installed` reports, and `mpm install` searches for a package before it installs it. Results are printed in recutils format with records separated by blank lines. ```{code-block} shell-session $ guix search cowsay name: cowsay version: 3.8.4 outputs: + out: everything systems: x86_64-linux mips64el-linux aarch64-linux powerpc64le-linux + i686-linux armhf-linux powerpc-linux dependencies: perl@5.36.0 location: gnu/packages/games.scm:1431:2 homepage: https://web.archive.org/web/20071026043648/http://www.nog.net:80/~tony/warez/cowsay.shtml license: GPL 3+ synopsis: Speaking cow text filter description: Cowsay is basically a text filter. Send some text into it, and + you get a cow saying your text. If you think a talking cow isn't enough, cows + can think too: all you have to do is run `cowthink'. If you're tired of cows, + a variety of other ASCII-art messengers are available. relevance: 32 name: python-snakesay version: 0.10.4 outputs: + out: everything systems: x86_64-linux mips64el-linux aarch64-linux powerpc64le-linux + i686-linux armhf-linux powerpc-linux dependencies: python-pytest@8.4.1 python-setuptools@80.9.0 location: gnu/packages/python-xyz.scm:2293:2 homepage: https://github.com/pythonanywhere/snakesay license: Expat synopsis: Like `cowsay' but with Python flavor description: This package provides a simple ASCII art pictures generator of a + Snake with a message. relevance: 3 ``` """ name, _, wanted_output = query.partition(":") listing = self.run_cli( "search", self._name_regexp(name) if wanted_output else query ) newest: dict[str, Package] = {} for record in re.split(r"\n\n+", listing.strip()): fields: dict[str, str] = {} outputs: list[str] = [] field = None for line in record.splitlines(): match = self._SEARCH_FIELD_REGEXP.match(line) if match: field = match.group("field") # Guix ends some values with spaces, like the synopsis. fields[field] = (match.group("value") or "").strip() elif field == "outputs": output_match = self._SEARCH_OUTPUT_REGEXP.match(line) if output_match: outputs.append(output_match.group("output")) package_id = fields.get("name") if not package_id: continue if wanted_output: # The regular expression also matches synopses and descriptions. if package_id != name or wanted_output not in outputs: continue package_id = f"{name}:{wanted_output}" version = fields.get("version") or None held = newest.get(package_id) if ( held is None or held.latest_version is None or (version and parse_version(version) > held.latest_version) ): newest[package_id] = self.package( id=package_id, description=fields.get("synopsis") or None, latest_version=version, ) yield from newest.values()
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. ```{code-block} shell-session $ guix install hello ``` """ return self.run_cli("install", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. ```{code-block} shell-session $ guix upgrade ``` """ return self.build_cli("upgrade")
[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. `guix upgrade` reads its argument as a regular expression, and upgrades every installed package whose name it matches: `guix upgrade glib` also upgrades `glib-networking`. So the name is escaped and anchored, to match itself alone. Entries match by name, so the `:output` suffix is dropped, and every installed output of the package is upgraded. ```{code-block} shell-session $ guix upgrade ^lua$ ``` """ return self.build_cli( "upgrade", self._name_regexp(package_id.partition(":")[0]) )
[docs] def remove(self, package_id: str) -> str: """Remove one package. ```{code-block} shell-session $ guix remove hello ``` """ return self.run_cli("remove", package_id)
[docs] def sync(self) -> None: """Fetch the latest Guix channel revisions. ```{code-block} shell-session $ guix pull ``` """ self.run_cli("pull")
[docs] def cleanup_cache(self) -> None: """Collect garbage in the store. ```{code-block} shell-session $ guix gc ``` """ self.run_cli("gc")