1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum Platform {
8 #[serde(rename = "linux/amd64")]
9 LinuxAmd64,
10 #[serde(rename = "linux/arm64")]
11 LinuxArm64,
12}
13
14impl std::fmt::Display for Platform {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 Platform::LinuxAmd64 => write!(f, "linux/amd64"),
18 Platform::LinuxArm64 => write!(f, "linux/arm64"),
19 }
20 }
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct VersionSpec(pub String);
28
29impl std::fmt::Display for VersionSpec {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 write!(f, "{}", self.0)
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PackageSpec {
40 pub name: String,
41 pub version_spec: VersionSpec,
42}
43
44impl PackageSpec {
45 pub fn parse(s: &str) -> anyhow::Result<Self> {
47 let s = s.trim();
48 if let Some((name, spec)) = s.split_once(' ') {
49 Ok(Self {
50 name: name.trim().to_string(),
51 version_spec: VersionSpec(spec.trim().to_string()),
52 })
53 } else {
54 Ok(Self {
55 name: s.to_string(),
56 version_spec: VersionSpec("*".to_string()),
57 })
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct EntrypointSpec {
66 pub command: String,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub args: Vec<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct BuildSpec {
91 pub name: String,
92 pub version: String,
93 pub channels: Vec<String>,
94 pub packages: Vec<String>,
96 pub entrypoint: EntrypointSpec,
97 pub platform: Platform,
98}
99
100impl BuildSpec {
101 pub fn package_specs(&self) -> anyhow::Result<Vec<PackageSpec>> {
102 self.packages.iter().map(|s| PackageSpec::parse(s)).collect()
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ResolvedPackage {
111 pub name: String,
112 pub version: String,
113 pub build: String,
114 pub channel: String,
115 pub url: String,
117 pub sha256: String,
119 pub filename: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ResolvedSpec {
132 pub name: String,
133 pub version: String,
134 pub platform: Platform,
135 pub channels: Vec<String>,
136 pub packages: Vec<ResolvedPackage>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub repodata_snapshot: Option<String>,
140}
141
142impl ResolvedSpec {
143 pub fn sort_packages(&mut self) {
145 self.packages.sort_by(|a, b| {
146 a.name
147 .cmp(&b.name)
148 .then(a.version.cmp(&b.version))
149 .then(a.build.cmp(&b.build))
150 });
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn package_spec_parse_with_version() {
160 let ps = PackageSpec::parse("samtools ==1.19.2").unwrap();
161 assert_eq!(ps.name, "samtools");
162 assert_eq!(ps.version_spec.0, "==1.19.2");
163 }
164
165 #[test]
166 fn package_spec_parse_bare_name() {
167 let ps = PackageSpec::parse("openssl").unwrap();
168 assert_eq!(ps.name, "openssl");
169 assert_eq!(ps.version_spec.0, "*");
170 }
171
172 #[test]
173 fn resolved_spec_sort_is_deterministic() {
174 let mut spec = ResolvedSpec {
175 name: "test".into(),
176 version: "1.0".into(),
177 platform: Platform::LinuxAmd64,
178 channels: vec![],
179 packages: vec![
180 ResolvedPackage {
181 name: "zlib".into(),
182 version: "1.3.1".into(),
183 build: "h0_0".into(),
184 channel: "conda-forge".into(),
185 url: "https://example.com/zlib.conda".into(),
186 sha256: "abc".into(),
187 filename: "zlib-1.3.1-h0_0.conda".into(),
188 },
189 ResolvedPackage {
190 name: "openssl".into(),
191 version: "3.2.1".into(),
192 build: "h0_0".into(),
193 channel: "conda-forge".into(),
194 url: "https://example.com/openssl.conda".into(),
195 sha256: "def".into(),
196 filename: "openssl-3.2.1-h0_0.conda".into(),
197 },
198 ],
199 repodata_snapshot: None,
200 };
201 spec.sort_packages();
202 assert_eq!(spec.packages[0].name, "openssl");
203 assert_eq!(spec.packages[1].name, "zlib");
204 }
205}