pixi_outdated/
pixi.rs

1use anyhow::{Context, Result};
2use serde::Deserialize;
3use std::process::Command;
4
5#[derive(Debug, Deserialize, Clone)]
6pub struct PixiPackage {
7    pub name: String,
8    pub version: String,
9    #[serde(default)]
10    pub build: Option<String>,
11    #[serde(default)]
12    pub size_bytes: Option<u64>,
13    pub kind: PackageKind,
14    #[serde(default)]
15    pub source: Option<String>,
16    pub is_explicit: bool,
17}
18
19#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
20#[serde(rename_all = "lowercase")]
21pub enum PackageKind {
22    Conda,
23    Pypi,
24}
25
26/// Get the list of packages from `pixi list --json`
27pub fn get_package_list(
28    explicit: bool,
29    environment: Option<&str>,
30    platform: Option<&str>,
31    manifest: Option<&str>,
32    package_names: &[String],
33) -> Result<Vec<PixiPackage>> {
34    let mut cmd = Command::new("pixi");
35    cmd.arg("list").arg("--json");
36
37    if explicit {
38        cmd.arg("--explicit");
39    }
40
41    if let Some(env) = environment {
42        cmd.arg("--environment").arg(env);
43    }
44
45    if let Some(plat) = platform {
46        cmd.arg("--platform").arg(plat);
47    }
48
49    if let Some(man) = manifest {
50        cmd.arg("--manifest-path").arg(man);
51    }
52
53    // If package names are specified, create a regex pattern to match them
54    // For a single package, just use the name directly
55    // For multiple packages, create a pattern like '^(pkg1|pkg2|pkg3)$'
56    if !package_names.is_empty() {
57        let regex_pattern = if package_names.len() == 1 {
58            // For a single package, just use the name (pixi will match it as a regex)
59            format!("^{}$", regex::escape(&package_names[0]))
60        } else {
61            // For multiple packages, create an alternation pattern
62            let escaped_names: Vec<String> = package_names
63                .iter()
64                .map(|name| regex::escape(name))
65                .collect();
66            format!("^({})$", escaped_names.join("|"))
67        };
68        cmd.arg(&regex_pattern);
69    }
70
71    let output = cmd
72        .output()
73        .context("Failed to execute `pixi list`. Is pixi installed?")?;
74
75    if !output.status.success() {
76        let stderr = String::from_utf8_lossy(&output.stderr);
77        anyhow::bail!("pixi list failed: {}", stderr);
78    }
79
80    let stdout =
81        String::from_utf8(output.stdout).context("pixi list output was not valid UTF-8")?;
82
83    let packages: Vec<PixiPackage> = serde_json::from_str(&stdout).with_context(|| {
84        format!(
85            "Failed to parse JSON output from pixi list. Output was:\n{}",
86            stdout
87        )
88    })?;
89
90    Ok(packages)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_package_kind_traits() {
99        // Test that PackageKind has the required traits for HashMap keys
100        use std::collections::HashMap;
101
102        let mut map: HashMap<PackageKind, String> = HashMap::new();
103        map.insert(PackageKind::Conda, "conda_value".to_string());
104        map.insert(PackageKind::Pypi, "pypi_value".to_string());
105
106        assert_eq!(
107            map.get(&PackageKind::Conda),
108            Some(&"conda_value".to_string())
109        );
110        assert_eq!(map.get(&PackageKind::Pypi), Some(&"pypi_value".to_string()));
111        assert_eq!(map.len(), 2);
112
113        // Test Copy trait
114        let kind1 = PackageKind::Conda;
115        let kind2 = kind1; // This should compile because PackageKind implements Copy
116        assert_eq!(kind1, kind2);
117    }
118
119    #[test]
120    fn test_pixi_package_deserialization() {
121        let json = r#"{
122            "name": "python",
123            "version": "3.12.0",
124            "build": "h1234567_0",
125            "size_bytes": 12345678,
126            "kind": "conda",
127            "source": "https://conda.anaconda.org/conda-forge/linux-64/python-3.12.0.tar.bz2",
128            "is_explicit": true
129        }"#;
130
131        let package: PixiPackage = serde_json::from_str(json).unwrap();
132
133        assert_eq!(package.name, "python");
134        assert_eq!(package.version, "3.12.0");
135        assert_eq!(package.build, Some("h1234567_0".to_string()));
136        assert_eq!(package.size_bytes, Some(12345678));
137        assert_eq!(package.kind, PackageKind::Conda);
138        assert!(package.source.is_some());
139        assert!(package.is_explicit);
140    }
141
142    #[test]
143    fn test_pixi_package_deserialization_minimal() {
144        // Test with minimal fields (optional fields missing)
145        let json = r#"{
146            "name": "cowsay",
147            "version": "5.0",
148            "kind": "pypi",
149            "is_explicit": false
150        }"#;
151
152        let package: PixiPackage = serde_json::from_str(json).unwrap();
153
154        assert_eq!(package.name, "cowsay");
155        assert_eq!(package.version, "5.0");
156        assert_eq!(package.build, None);
157        assert_eq!(package.size_bytes, None);
158        assert_eq!(package.kind, PackageKind::Pypi);
159        assert_eq!(package.source, None);
160        assert!(!package.is_explicit);
161    }
162
163    #[test]
164    fn test_pixi_package_clone() {
165        let package = PixiPackage {
166            name: "test-package".to_string(),
167            version: "1.0.0".to_string(),
168            build: Some("build123".to_string()),
169            size_bytes: Some(1000),
170            kind: PackageKind::Conda,
171            source: Some("https://example.com/package.tar.bz2".to_string()),
172            is_explicit: true,
173        };
174
175        let cloned = package.clone();
176
177        assert_eq!(cloned.name, package.name);
178        assert_eq!(cloned.version, package.version);
179        assert_eq!(cloned.build, package.build);
180        assert_eq!(cloned.size_bytes, package.size_bytes);
181        assert_eq!(cloned.kind, package.kind);
182        assert_eq!(cloned.source, package.source);
183        assert_eq!(cloned.is_explicit, package.is_explicit);
184    }
185}