use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Platform {
#[serde(rename = "linux/amd64")]
LinuxAmd64,
#[serde(rename = "linux/arm64")]
LinuxArm64,
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Platform::LinuxAmd64 => write!(f, "linux/amd64"),
Platform::LinuxArm64 => write!(f, "linux/arm64"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct VersionSpec(pub String);
impl std::fmt::Display for VersionSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageSpec {
pub name: String,
pub version_spec: VersionSpec,
}
impl PackageSpec {
pub fn parse(s: &str) -> anyhow::Result<Self> {
let s = s.trim();
if let Some((name, spec)) = s.split_once(' ') {
Ok(Self {
name: name.trim().to_string(),
version_spec: VersionSpec(spec.trim().to_string()),
})
} else {
Ok(Self {
name: s.to_string(),
version_spec: VersionSpec("*".to_string()),
})
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntrypointSpec {
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildSpec {
pub name: String,
pub version: String,
pub channels: Vec<String>,
pub packages: Vec<String>,
pub entrypoint: EntrypointSpec,
pub platform: Platform,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base: Option<String>,
}
impl BuildSpec {
pub fn package_specs(&self) -> anyhow::Result<Vec<PackageSpec>> {
self.packages
.iter()
.map(|s| PackageSpec::parse(s))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedPackage {
pub name: String,
pub version: String,
pub build: String,
pub channel: String,
pub url: String,
pub sha256: String,
pub filename: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub depends: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedSpec {
pub name: String,
pub version: String,
pub platform: Platform,
pub channels: Vec<String>,
pub packages: Vec<ResolvedPackage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repodata_snapshot: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base: Option<String>,
}
impl ResolvedSpec {
pub fn sort_packages(&mut self) {
self.packages.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then(a.version.cmp(&b.version))
.then(a.build.cmp(&b.build))
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn package_spec_parse_with_version() {
let ps = PackageSpec::parse("samtools ==1.19.2").unwrap();
assert_eq!(ps.name, "samtools");
assert_eq!(ps.version_spec.0, "==1.19.2");
}
#[test]
fn package_spec_parse_bare_name() {
let ps = PackageSpec::parse("openssl").unwrap();
assert_eq!(ps.name, "openssl");
assert_eq!(ps.version_spec.0, "*");
}
#[test]
fn resolved_spec_sort_is_deterministic() {
let mut spec = ResolvedSpec {
name: "test".into(),
version: "1.0".into(),
platform: Platform::LinuxAmd64,
channels: vec![],
packages: vec![
ResolvedPackage {
name: "zlib".into(),
version: "1.3.1".into(),
build: "h0_0".into(),
channel: "conda-forge".into(),
url: "https://example.com/zlib.conda".into(),
sha256: "abc".into(),
filename: "zlib-1.3.1-h0_0.conda".into(),
depends: vec![],
},
ResolvedPackage {
name: "openssl".into(),
version: "3.2.1".into(),
build: "h0_0".into(),
channel: "conda-forge".into(),
url: "https://example.com/openssl.conda".into(),
sha256: "def".into(),
filename: "openssl-3.2.1-h0_0.conda".into(),
depends: vec![],
},
],
repodata_snapshot: None,
base: None,
};
spec.sort_packages();
assert_eq!(spec.packages[0].name, "openssl");
assert_eq!(spec.packages[1].name, "zlib");
}
}