use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, run_command_with_timeout};
use anyhow::{Result, anyhow};
use std::fs;
use std::path::Path;
pub struct Pixi;
impl PackageManager for Pixi {
fn name(&self) -> &'static str {
"pixi"
}
fn detect(&self, path: &Path) -> bool {
if path.join("pixi.toml").is_file() || path.join("pixi.lock").is_file() {
return true;
}
let pyproject = path.join("pyproject.toml");
pyproject.is_file()
&& fs::read_to_string(&pyproject)
.map(|content| content.contains("[tool.pixi"))
.unwrap_or(false)
}
fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
let mut dirs = Vec::new();
let pixi_dir = path.join(".pixi");
if pixi_dir.is_dir() {
dirs.push(BloatDir {
name: ".pixi".to_string(),
size_bytes: dir_size(&pixi_dir),
path: pixi_dir,
shared_bytes: 0,
});
}
dirs
}
fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
let lockfile = path.join("pixi.lock");
let content = fs::read_to_string(&lockfile).map_err(|e| {
anyhow!(
"`pixi.lock` could not be read ({e}): nothing proves the environments are \
recoverable. Write one first: `pixi lock`."
)
})?;
let has_version = content
.lines()
.any(|line| line.trim_start().starts_with("version:"));
let has_package = content.lines().any(|line| {
let line = line.trim_start();
line.starts_with("- conda") || line.starts_with("- pypi")
});
if !has_version || !has_package {
return Err(anyhow!(
"`pixi.lock` does not read as a pixi lockfile pinning any packages: \
refusing to treat `.pixi` as recoverable from it. Regenerate it: \
`pixi lock`."
));
}
Ok(())
}
fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
run_command_with_timeout("pixi", &["install"], path, timeout)
}
fn lockfiles(&self) -> &'static [&'static str] {
&["pixi.lock"]
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const LOCKFILE: &str = "version: 6\nenvironments:\n default:\n channels:\n\
- url: https://conda.anaconda.org/conda-forge/\n packages:\n\
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a.conda\n";
#[test]
fn detects_on_manifest_lockfile_or_pyproject_table() {
let dir = tempdir().unwrap();
assert!(!Pixi.detect(dir.path()));
fs::write(dir.path().join("pixi.toml"), "[project]\n").unwrap();
assert!(Pixi.detect(dir.path()));
fs::remove_file(dir.path().join("pixi.toml")).unwrap();
fs::write(dir.path().join("pixi.lock"), LOCKFILE).unwrap();
assert!(Pixi.detect(dir.path()));
fs::remove_file(dir.path().join("pixi.lock")).unwrap();
fs::write(dir.path().join("pyproject.toml"), "[tool.pixi.project]\n").unwrap();
assert!(Pixi.detect(dir.path()));
}
#[test]
fn a_plain_pyproject_is_not_claimed() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("pyproject.toml"), "[tool.poetry]\n").unwrap();
assert!(!Pixi.detect(dir.path()));
}
#[test]
fn the_environment_directory_is_claimed_and_nothing_else() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join(".pixi")).unwrap();
fs::create_dir(dir.path().join("src")).unwrap();
let dirs = Pixi.bloat_dirs(dir.path());
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, ".pixi");
}
#[test]
fn a_missing_or_empty_lockfile_is_refused() {
let dir = tempdir().unwrap();
let policy = EnforcePolicy::default();
assert!(Pixi.enforce_lockfile(dir.path(), policy).is_err());
fs::write(
dir.path().join("pixi.lock"),
"version: 6\nenvironments: {}\n",
)
.unwrap();
assert!(Pixi.enforce_lockfile(dir.path(), policy).is_err());
fs::write(dir.path().join("pixi.lock"), LOCKFILE).unwrap();
assert!(Pixi.enforce_lockfile(dir.path(), policy).is_ok());
}
#[test]
fn a_foreign_file_under_the_lockfile_name_is_refused() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("pixi.lock"), "hello there\n").unwrap();
assert!(
Pixi.enforce_lockfile(dir.path(), EnforcePolicy::default())
.is_err()
);
}
#[test]
fn pixi_is_not_opt_in() {
assert!(!Pixi.opt_in());
}
}