Source code for meta_package_manager.managers.pixi

# 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 extra_platforms import LINUX_LIKE, MACOS, WINDOWS

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 Pixi(PackageManager): """pixi installs conda packages, either into a project workspace or machine-wide as global tools. `mpm` is system-scoped, so this wrapper drives the `pixi global` scope alone and never touches a `pixi.toml` workspace. Global tools resolve from conda channels, `conda-forge` by default, and land in their own prefix under `$PIXI_HOME`. Documentation: [pixi global tools](https://pixi.sh/latest/global_tools/introduction/). ```{important} pixi isolates each global tool in its own *environment*, and `mpm` keys packages on the **environment** name, not on the conda package names inside it. The environment is the unit every mutating `pixi global` verb addresses: `uninstall` deletes one whole environment and `update` refreshes one whole environment, neither taking a package name. `pixi global install <pkg>` names the environment after the package, so for everything `mpm` installs the two are the same string and every operation round-trips. Keying on the inner `dependencies` instead was tried and is unsafe: it reports packages that `remove` cannot address one at a time, so removing one of them deletes the whole environment and silently takes its siblings with it. ``` ```{caution} An environment holding more than the package it is named after, built with `pixi global install <pkg> --with <other>` or `pixi global add`, is therefore reported as the single package `<pkg>`. Its extra packages are invisible to `mpm`, and removing `<pkg>` destroys them along with the environment, which is exactly what a bare `pixi global uninstall <pkg>` does. `mpm` neither widens nor narrows that behavior. ``` ```{note} An environment whose `dependencies` do not include the package it is named after reports no version. That is the same signal pixi's own listing gives: it prints `<name>: <version>` inline only while the environment resolves to its eponymous package, and drops the version once the contents diverge. ``` ```{caution} No `search` operation is declared, though `pixi search` exists. Its `--json` mode cannot be capped: the flag `conflicts_with_all` the `--limit` and `--limit-packages` options that bound the human view, and outside a workspace pixi falls back to `Platform::all()` and queries every known conda subdir, roughly thirty of them, which is exactly how `mpm` runs it. Repodata carries no summary or description either, so the results would be name-only. Declaring nothing lets `mpm` skip the manager during a search rather than stall on it. Bounding it means passing `--platform`, which would put a host-to-conda-subdir mapping in `mpm` that pixi already owns, and an empty result set is an error rather than an empty document, so reviving `search` is a deliberate piece of work rather than a one-line addition. ``` ```{caution} No `outdated` operation is declared: nothing in `pixi global` reports upgradable packages without performing the upgrade. `pixi global update` has no dry-run mode, and the request for a dedicated command ([prefix-dev/pixi#6279](https://github.com/prefix-dev/pixi/issues/6279)) was closed pointing at the workspace-scoped `pixi update --dry-run`, which does not cover the global scope. `upgrade --all` is unaffected and maps to the native bare `pixi global update`. ``` ```{note} No `sync` operation either, despite the name of `pixi global sync`: that command reconciles installed environments against the manifest, installing and removing to match it, rather than refreshing package metadata from the channels. Mapping `mpm sync` onto it would make a read-shaped command mutate the machine. ``` """ name = "pixi" homepage_url = "https://pixi.sh" # No `logo`: Simple Icons carries no pixi mark yet. Their request is open and # already cleared of trademark concerns (labelled `permission not needed`): # https://github.com/simple-icons/simple-icons/issues/13796 # Do not reach for their `pixiv` mark, which is an unrelated brand. platforms = LINUX_LIKE, MACOS, WINDOWS requirement = ">=0.65.0" """[`0.65.0`](https://github.com/prefix-dev/pixi/blob/main/CHANGELOG.md) is where `pixi global list` gained the `--json` flag this wrapper reads ([prefix-dev/pixi#5530](https://github.com/prefix-dev/pixi/pull/5530)), and it is the binding floor. Every other operation is far older: `install` dates to `0.0.4`, `list` to `0.3.0`, and `uninstall` / `update` to the `0.33.0` rewrite that rebuilt `pixi global` around its manifest. That same rewrite is why nothing below `0.33.0` would work anyway: `pixi global remove` meant *uninstall* back then, and now means removing one dependency from an environment. """ pre_args = ("--color=never", "--no-progress") """Both are global options, accepted ahead of any subcommand. `pixi` colorizes its listings semantically and draws progress bars while solving. Each turns itself off when the matching stream is not a terminal, but `mpm` asks explicitly rather than relying on redirection. The version probe runs with `auto_pre_args=False` and so stays a bare `pixi --version`. """ version_regexes = (r"^pixi\s+(?P<version>\S+)",) """ ```{code-block} shell-session $ pixi --version pixi 0.48.0 ``` """ @property def installed(self) -> Iterator[Package]: """Fetch installed packages. The document is an array of *environments*, each carrying the explicit specs it was created from. Transitive dependencies are not listed, which is what makes this an inventory of tools rather than of everything on disk. Each environment yields exactly one package, keyed on the environment name, and its version is read from the dependency of the same name. A `version` is `null` for a package the manifest declares but that is not installed in the prefix, and the optional `platform` key is omitted unless the environment pins one. The block below is source-derived: its layout follows pixi's `serde_json::to_string_pretty` serialization of `GlobalEnvironmentJson`, and its values are those of the `ripgrep` entry in pixi's own [`pixi global list` reference output](https://pixi.sh/latest/reference/cli/pixi/global/list/). ```{code-block} shell-session $ pixi --color=never --no-progress global list --json [ { "name": "ripgrep", "dependencies": [ { "name": "ripgrep", "version": "14.1.0" } ], "exposed": [ { "exposed_name": "rg", "executable": "rg" } ] } ] ``` """ output = self.run_cli("global", "list", "--json", must_succeed=True) data = self.parse_json(output) if not data: return for environment in data: env_name = environment.get("name") if not env_name: continue version = None for dependency in environment.get("dependencies", ()): if dependency.get("name") == env_name: version = dependency.get("version") break yield self.package(id=env_name, installed_version=version)
[docs] def install(self, package_id: str, version: str | None = None) -> str: """Install one package. The positional argument is a conda `MatchSpec`, so a version is pinned by appending `==<version>` to the name rather than through a flag. ```{code-block} shell-session $ pixi --color=never --no-progress global install hyperfine ``` ```{code-block} shell-session $ pixi --color=never --no-progress global install hyperfine==1.20.0 ``` """ spec = f"{package_id}=={version}" if version else package_id return self.run_cli("global", "install", spec)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. Bare `pixi global update` updates every environment: there is no `--all` flag. It also prunes stale environments on the way through. ```{code-block} shell-session $ pixi --color=never --no-progress global update ``` """ return self.build_cli("global", "update")
[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. `pixi global update` takes environment names and accepts no version at all, so a pinned upgrade is not expressible: `mpm` warns and upgrades to whatever the channels resolve to. Routing a pinned upgrade through `pixi global install <pkg>==<version>` instead was tried and rejected. That verb takes a package spec rather than an environment name, and creates the environment when it is missing, so pinning a package that lives inside a differently-named environment forks a second environment holding a second copy and reports success, leaving the original untouched. ```{code-block} shell-session $ pixi --color=never --no-progress global update hyperfine ``` """ return self.build_cli("global", "update", package_id)
[docs] def remove(self, package_id: str) -> str: """Remove one package. `pixi global uninstall` removes a whole environment, deleting its prefix outright, which is the counterpart of the `pixi global install` that created it and matches the unit {meth}`installed` reports. An environment carrying co-installed extras loses those too. The sibling `pixi global remove` is deliberately not used: it drops one dependency from an environment and leaves the environment behind, which would strand an entry `mpm` still reports as installed. ```{code-block} shell-session $ pixi --color=never --no-progress global uninstall hyperfine ``` """ return self.run_cli("global", "uninstall", package_id)
[docs] def cleanup_cache(self) -> None: """Clear pixi's download caches. `--yes` is required: with no cache-type flag, `pixi clean cache` asks for confirmation on `<stdin>` and would otherwise block forever under `mpm`. ```{code-block} shell-session $ pixi --color=never --no-progress clean cache --yes ``` """ self.run_cli("clean", "cache", "--yes")