Skip to main content

dev_prune/adapters/
pixi.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// pixi package manager adapter.
5//
6// pixi installs a project's conda and PyPI environments into `.pixi/` beside the
7// manifest, pinned exactly by `pixi.lock`, and `pixi install` rebuilds the whole
8// directory from that lockfile. A regular adapter, not opt-in: `.pixi/` is downloaded
9// dependencies, not compiler output, the same trade as `node_modules` or a uv `.venv`.
10
11use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, run_command_with_timeout};
12use anyhow::{Result, anyhow};
13use std::fs;
14use std::path::Path;
15
16/// Adapter for pixi-managed projects.
17pub struct Pixi;
18
19impl PackageManager for Pixi {
20    fn name(&self) -> &'static str {
21        "pixi"
22    }
23
24    fn detect(&self, path: &Path) -> bool {
25        if path.join("pixi.toml").is_file() || path.join("pixi.lock").is_file() {
26            return true;
27        }
28        // pixi can also live inside pyproject.toml, under its own tool table.
29        let pyproject = path.join("pyproject.toml");
30        pyproject.is_file()
31            && fs::read_to_string(&pyproject)
32                .map(|content| content.contains("[tool.pixi"))
33                .unwrap_or(false)
34    }
35
36    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
37        let mut dirs = Vec::new();
38        let pixi_dir = path.join(".pixi");
39        if pixi_dir.is_dir() {
40            dirs.push(BloatDir {
41                name: ".pixi".to_string(),
42                size_bytes: dir_size(&pixi_dir),
43                path: pixi_dir,
44                shared_bytes: 0,
45            });
46        }
47        dirs
48    }
49
50    /// The check is a static read of `pixi.lock`, not `pixi lock --check`: re-solving
51    /// the environments can reach the network, and the middle of a delete pass is the
52    /// wrong moment to depend on a resolver being reachable. What the read establishes
53    /// is that the lockfile is the format it claims (a `version:` line) and actually
54    /// pins something (at least one conda or PyPI entry) — an empty or foreign file
55    /// proves nothing about `.pixi/` being rebuildable.
56    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
57        let lockfile = path.join("pixi.lock");
58        let content = fs::read_to_string(&lockfile).map_err(|e| {
59            anyhow!(
60                "`pixi.lock` could not be read ({e}): nothing proves the environments are \
61                 recoverable. Write one first: `pixi lock`."
62            )
63        })?;
64        let has_version = content
65            .lines()
66            .any(|line| line.trim_start().starts_with("version:"));
67        let has_package = content.lines().any(|line| {
68            let line = line.trim_start();
69            line.starts_with("- conda") || line.starts_with("- pypi")
70        });
71        if !has_version || !has_package {
72            return Err(anyhow!(
73                "`pixi.lock` does not read as a pixi lockfile pinning any packages: \
74                 refusing to treat `.pixi` as recoverable from it. Regenerate it: \
75                 `pixi lock`."
76            ));
77        }
78        Ok(())
79    }
80
81    fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
82        run_command_with_timeout("pixi", &["install"], path, timeout)
83    }
84
85    fn lockfiles(&self) -> &'static [&'static str] {
86        &["pixi.lock"]
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use tempfile::tempdir;
94
95    /// A plausible `pixi.lock` pinning one conda package.
96    const LOCKFILE: &str = "version: 6\nenvironments:\n  default:\n    channels:\n\
97                            - url: https://conda.anaconda.org/conda-forge/\n    packages:\n\
98                            - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a.conda\n";
99
100    #[test]
101    fn detects_on_manifest_lockfile_or_pyproject_table() {
102        let dir = tempdir().unwrap();
103        assert!(!Pixi.detect(dir.path()));
104
105        fs::write(dir.path().join("pixi.toml"), "[project]\n").unwrap();
106        assert!(Pixi.detect(dir.path()));
107        fs::remove_file(dir.path().join("pixi.toml")).unwrap();
108
109        fs::write(dir.path().join("pixi.lock"), LOCKFILE).unwrap();
110        assert!(Pixi.detect(dir.path()));
111        fs::remove_file(dir.path().join("pixi.lock")).unwrap();
112
113        fs::write(dir.path().join("pyproject.toml"), "[tool.pixi.project]\n").unwrap();
114        assert!(Pixi.detect(dir.path()));
115    }
116
117    #[test]
118    fn a_plain_pyproject_is_not_claimed() {
119        let dir = tempdir().unwrap();
120        fs::write(dir.path().join("pyproject.toml"), "[tool.poetry]\n").unwrap();
121        assert!(!Pixi.detect(dir.path()));
122    }
123
124    #[test]
125    fn the_environment_directory_is_claimed_and_nothing_else() {
126        let dir = tempdir().unwrap();
127        fs::create_dir(dir.path().join(".pixi")).unwrap();
128        fs::create_dir(dir.path().join("src")).unwrap();
129
130        let dirs = Pixi.bloat_dirs(dir.path());
131        assert_eq!(dirs.len(), 1);
132        assert_eq!(dirs[0].name, ".pixi");
133    }
134
135    #[test]
136    fn a_missing_or_empty_lockfile_is_refused() {
137        let dir = tempdir().unwrap();
138        let policy = EnforcePolicy::default();
139        assert!(Pixi.enforce_lockfile(dir.path(), policy).is_err());
140        fs::write(
141            dir.path().join("pixi.lock"),
142            "version: 6\nenvironments: {}\n",
143        )
144        .unwrap();
145        assert!(Pixi.enforce_lockfile(dir.path(), policy).is_err());
146        fs::write(dir.path().join("pixi.lock"), LOCKFILE).unwrap();
147        assert!(Pixi.enforce_lockfile(dir.path(), policy).is_ok());
148    }
149
150    #[test]
151    fn a_foreign_file_under_the_lockfile_name_is_refused() {
152        let dir = tempdir().unwrap();
153        fs::write(dir.path().join("pixi.lock"), "hello there\n").unwrap();
154        assert!(
155            Pixi.enforce_lockfile(dir.path(), EnforcePolicy::default())
156                .is_err()
157        );
158    }
159
160    #[test]
161    fn pixi_is_not_opt_in() {
162        assert!(!Pixi.opt_in());
163    }
164}