Source code for projspec.proj.pixi

import os
import toml

from projspec.proj import ParseFailed, ProjectSpec
from projspec.utils import AttrDict, PickleableTomlDecoder

# pixi supports extensions, e.g., ``pixi global install``,
# which is how you get access to pixi-pack, for instance.

# https://github.com/conda/conda/blob/main/conda/base/context.py
_platform_map = {
    "freebsd13": "freebsd",
    "linux2": "linux",
    "linux": "linux",
    "darwin": "osx",
    "win32": "win",
    "zos": "zos",
}
non_x86_machines = {
    "armv6l",
    "armv7l",
    "aarch64",
    "arm64",
    "ppc64",
    "ppc64le",
    "riscv64",
    "s390x",
}
_arch_names = {
    32: "x86",
    64: "x86_64",
}


def this_platform():
    """Name of the current platform as a conda channel"""
    import platform
    import struct
    import sys

    base = _platform_map.get(sys.platform, "unknown")
    bits = 8 * struct.calcsize("P")
    m = platform.machine()
    platform = m if m in non_x86_machines else bits
    return f"{base}-{platform}"


[docs] class Pixi(ProjectSpec): """A project using https://pixi.sh/ pixi is a conda-stack, project-oriented (aka "workspace") env and execution manager. """ icon = "🗃️" spec_doc = "https://pixi.sh/latest/reference/pixi_manifest" # some example projects: # https://github.com/prefix-dev/pixi/tree/main/examples # spec docs # https://pixi.sh/dev/reference/pixi_manifest/ def match(self) -> bool: meta = self.proj.pyproject.get("tools", {}).get("pixi", {}) return bool(meta) or "pixi.toml" in self.proj.basenames def parse(self) -> None: from projspec.artifact.installable import CondaPackage from projspec.artifact.python_env import CondaEnv, LockFile from projspec.content.environment import Environment, Precision, Stack meta = self.proj.pyproject.get("tools", {}).get("pixi", {}) if "pixi.toml" in self.proj.basenames: try: with self.proj.fs.open(self.proj.basenames["pixi.toml"], "rb") as f: meta.update( toml.loads(f.read().decode(), decoder=PickleableTomlDecoder()) ) except (OSError, ValueError, UnicodeDecodeError, FileNotFoundError): pass if not meta: raise ParseFailed arts = AttrDict() conts = AttrDict() # Can categorize metadata into "features", each of which is an independent # set of deps, tasks ,etc. However, projects may have only one, # the implicit "default" feature. Often, environments map to features. # target.*.activation run when starting an env for given platform procs = AttrDict() commands = AttrDict() extract_feature(meta, procs, commands, self) if "environments" in meta and "feature" in meta: for env_name, details in meta["environments"].items(): feat = {} feats = set( details if isinstance(details, list) else details["features"] ) for feat_name in feats: feat.update(meta["feature"][feat_name]) if isinstance(details, list) or not details.get("no-default-feature"): feat.update(meta) extract_feature(feat, procs, commands, self, env=env_name) if procs: arts["process"] = procs if commands: conts["commands"] = commands if "pixi.lock" in self.proj.basenames: conts["environments"] = AttrDict() arts["conda_env"] = AttrDict() with self.proj.fs.open(self.proj.basenames["pixi.lock"], "rb") as f: lock_envs = envs_from_lock(f) for env_name, details in lock_envs.items(): art = CondaEnv( proj=self.proj, fn=f"{self.proj.url}/.pixi/envs/{env_name}", cmd=["pixi", "install", "-e", env_name], ) arts["conda_env"][env_name] = art conts["environments"][env_name] = Environment( proj=self.proj, packages=details["packages"], stack=Stack.CONDA, precision=Precision.LOCK, channels=details["channels"], ) arts["lock_file"] = LockFile( proj=self.proj, fn=f"{self.proj.url}/pixi.lock", cmd=["pixi", "lock"], # --no-install ? ) if pkg := meta.get("package", {}): arts["conda_package"] = CondaPackage( proj=self.proj, name=pkg["name"], fn=f"{self.proj.url}/{pkg['name']}-{pkg['version']}*.conda", cmd=["pixi", "build"], ) # Any environment can be packed if we have access to pixi-pack; this currently (v0.6.5) # fails if there is any source-install in the env, which there normally is! # pixi supports conda/pypi split envs with [pypi-dependencies], which # can include local paths, git, URL # <https://pixi.sh/latest/reference/project_configuration/#full-specification>. # Pixi also allows for requiring sub-packages by including them in # package.run-dependencies with local or remote paths. In such cases, # we can know of projects in the tree without walking the directory. # environments built by pixi will contain a conda-meta/pixi file with the meta file, # pixi version, and lockfile hash detailed. # TODO: add built environment(s) as artifact with `pixi install -e <env>` self._artifacts = arts self._contents = conts @staticmethod def _create(path: str) -> None: name = os.path.basename(path) with open(f"{path}/pixi.toml", "wt") as f: f.write( f""" [workspace] authors = ["projspec <projscpec@example.com>"] channels = ["conda-forge"] name = "{name}" platforms = ["osx-arm64", "linux-64", "win-64"] version = "0.1.0" [tasks] hello = "echo 'hello world'" [dependencies] python = ">=3.10" """ )
def extract_feature( meta: dict, procs: AttrDict, commands: AttrDict, pixi: Pixi, env: str | None = None, ): """Consolidate metadata from 'features' to create commands and processes""" from projspec.artifact.process import Process from projspec.content.executable import Command for name, task in meta.get("tasks", {}).items(): if env: name = f"{name}.{env}" cmd = ["pixi", "run", name] if env: cmd.extend(["--environment", env]) art = Process(proj=pixi.proj, cmd=cmd) procs[name] = art # tasks without a command are aliases cmd = task.get("cmd", "") if isinstance(task, dict) else task # NB: these may have dependencies on other tasks and envs, but pixi # manages those. commands[name] = Command(proj=pixi.proj, cmd=cmd) for platform, v in meta.get("target", {}).items(): for name, task in v.get("tasks", {}).items(): if env: name = f"{name}.{env}" cmd = ( task.get("cmd", str(task.get("depends-on"))) if isinstance(task, dict) else task ) commands[name] = Command(proj=pixi.proj, cmd=cmd) if platform == this_platform(): # only commands on the current platform can be executed cmd = ["pixi", "run", name] if env: cmd.extend(["--environment", env]) art = Process(proj=pixi.proj, cmd=cmd) procs[name] = art commands[name].artifacts.add(art) def envs_from_lock(infile) -> dict: """Extract the environments info from a pixi (yaml) lock file""" # Developed for pixi format format 6 import yaml data = yaml.safe_load(infile) pkgs = {} for pkg in data["packages"]: # TODO: include build/hashes in conda explicit format? if "conda" in pkg: basename = pkg["conda"].rsplit("/", 1)[-1] name, version, _hash = basename.rsplit("-", 2) pkgs[pkg["conda"]] = f"{name} =={version}" else: pkgs[pkg["pypi"]] = f"{pkg['name']} =={pkg['version']}" out = {} for env_name, env in data["environments"].items(): req = { "packages": [ pkgs[entry.get("conda", entry.get("pypi"))] for entry in next(iter(env["packages"].values())) ] if env.get("packages") else [], "channels": [ _ if isinstance(_, str) else _.get("url", "") for _ in env["channels"] ] + env.get("indexes", []), } out[env_name] = req return out