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)]
91pub struct BuildSpec {
92 pub name: String,
93 pub version: String,
94 pub channels: Vec<String>,
95 pub packages: Vec<String>,
97 pub entrypoint: EntrypointSpec,
98 pub platform: Platform,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub base: Option<String>,
103}
104
105impl BuildSpec {
106 pub fn package_specs(&self) -> anyhow::Result<Vec<PackageSpec>> {
107 self.packages
108 .iter()
109 .map(|s| PackageSpec::parse(s))
110 .collect()
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ResolvedPackage {
119 pub name: String,
120 pub version: String,
121 pub build: String,
122 pub channel: String,
123 pub url: String,
125 pub sha256: String,
127 pub filename: String,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub depends: Vec<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ResolvedSpec {
143 pub name: String,
144 pub version: String,
145 pub platform: Platform,
146 pub channels: Vec<String>,
147 pub packages: Vec<ResolvedPackage>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub repodata_snapshot: Option<String>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub base: Option<String>,
154}
155
156impl ResolvedSpec {
157 pub fn sort_packages(&mut self) {
159 self.packages.sort_by(|a, b| {
160 a.name
161 .cmp(&b.name)
162 .then(a.version.cmp(&b.version))
163 .then(a.build.cmp(&b.build))
164 });
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn package_spec_parse_with_version() {
174 let ps = PackageSpec::parse("samtools ==1.19.2").unwrap();
175 assert_eq!(ps.name, "samtools");
176 assert_eq!(ps.version_spec.0, "==1.19.2");
177 }
178
179 #[test]
180 fn package_spec_parse_bare_name() {
181 let ps = PackageSpec::parse("openssl").unwrap();
182 assert_eq!(ps.name, "openssl");
183 assert_eq!(ps.version_spec.0, "*");
184 }
185
186 #[test]
187 fn resolved_spec_sort_is_deterministic() {
188 let mut spec = ResolvedSpec {
189 name: "test".into(),
190 version: "1.0".into(),
191 platform: Platform::LinuxAmd64,
192 channels: vec![],
193 packages: vec![
194 ResolvedPackage {
195 name: "zlib".into(),
196 version: "1.3.1".into(),
197 build: "h0_0".into(),
198 channel: "conda-forge".into(),
199 url: "https://example.com/zlib.conda".into(),
200 sha256: "abc".into(),
201 filename: "zlib-1.3.1-h0_0.conda".into(),
202 depends: vec![],
203 },
204 ResolvedPackage {
205 name: "openssl".into(),
206 version: "3.2.1".into(),
207 build: "h0_0".into(),
208 channel: "conda-forge".into(),
209 url: "https://example.com/openssl.conda".into(),
210 sha256: "def".into(),
211 filename: "openssl-3.2.1-h0_0.conda".into(),
212 depends: vec![],
213 },
214 ],
215 repodata_snapshot: None,
216 base: None,
217 };
218 spec.sort_packages();
219 assert_eq!(spec.packages[0].name, "openssl");
220 assert_eq!(spec.packages[1].name, "zlib");
221 }
222}