Source code for meta_package_manager.managers.pixi

# 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

from typing import ClassVar

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`. ```{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. ``` ```{todo} Declare a `logo` once Simple Icons ships the pixi mark requested in [simple-icons/simple-icons#13796](https://github.com/simple-icons/simple-icons/issues/13796), a request already cleared of trademark concerns (labelled `permission not needed`). Their `pixiv` mark is an unrelated brand, not a stand-in. ``` """ # 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. # # 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. # # `search`: `pixi search` exists, but 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. 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. # # `outdated`: 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 # (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. # # `sync`: `pixi global sync` 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. operation_notes: ClassVar = { "installed": ( "An environment is reported as the single package it is named " "after, with no version once its contents diverge; its extra " "packages are invisible to `mpm`." ), "remove": ( "Removing a package deletes its whole environment, taking any " "extra packages installed beside it." ), "search": ( "The `--json` mode cannot be capped and queries every known " "conda subdir, so a search would stall on it." ), "outdated": ( "Nothing reports upgradable packages without performing the " "upgrade " "([prefix-dev/pixi#6279](https://github.com/prefix-dev/pixi/issues/6279), " "closed pointing at a workspace-scoped dry run); `upgrade --all` " "maps to the native `pixi global update`." ), "sync": ( "The `pixi global sync` command reconciles the manifest by " "installing and removing, so it mutates rather than refreshes." ), } name = "pixi" homepage_url = "https://pixi.sh" documentation_url = "https://pixi.prefix.dev/latest/global_tools/introduction/" repository_url = "https://github.com/prefix-dev/pixi" keywords = ("prefix.dev",) 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.78.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 a capture from `pixi 0.78.0`. Its `ripgrep` entry is the one pixi's own [`pixi global list` reference output](https://pixi.sh/latest/reference/cli/pixi/global/list/) documents. ```{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}`~meta_package_manager.manager.PackageManager.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")