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
26pub 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.is_empty() {
57 let regex_pattern = if package_names.len() == 1 {
58 format!("^{}$", regex::escape(&package_names[0]))
60 } else {
61 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(®ex_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 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 let kind1 = PackageKind::Conda;
115 let kind2 = kind1; 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 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}