use std::cmp::Ordering;
use std::fmt;
const DISTRO_DATA: &str = include_str!("../data/distros.tsv");
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Release {
pub major: u32,
pub minor: u32,
}
impl Release {
pub fn parse(s: &str) -> Result<Self, String> {
let s = s.trim().trim_start_matches(['v', 'V']);
let mut parts = s.split(|c: char| !c.is_ascii_digit());
let major = parts.next().and_then(|p| p.parse().ok());
let minor = parts.next().and_then(|p| p.parse().ok());
match (major, minor) {
(Some(major), Some(minor)) => Ok(Release { major, minor }),
_ => Err(format!("not a release number: {s:?} (expected e.g. 4.12)")),
}
}
}
impl Ord for Release {
fn cmp(&self, other: &Self) -> Ordering {
(self.major, self.minor).cmp(&(other.major, other.minor))
}
}
impl PartialOrd for Release {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Release {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
pub type KernelVersion = Release;
pub type GlibcVersion = Release;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Distro {
pub id: &'static str,
pub name: &'static str,
pub kernel: KernelVersion,
pub glibc: GlibcVersion,
pub image: &'static str,
pub kernel_backports: Vec<&'static str>,
pub status: &'static str,
}
pub fn distros() -> Vec<Distro> {
DISTRO_DATA
.lines()
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
.map(|line| {
let cols: Vec<&'static str> = line.split('\t').map(str::trim).collect();
let [id, name, kernel, glibc, image, backports, status] = cols[..] else {
panic!("distro data row needs 7 columns: {cols:?}");
};
Distro {
id,
name,
kernel: Release::parse(kernel).expect("distro kernel"),
glibc: Release::parse(glibc).expect("distro glibc"),
image,
kernel_backports: if backports == "-" {
Vec::new()
} else {
backports.split(',').collect()
},
status,
}
})
.collect()
}
pub fn distro(id: &str) -> Option<Distro> {
distros().into_iter().find(|d| d.id == id)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Request<'a> {
pub distro: Option<&'a str>,
pub glibc: Option<&'a str>,
pub kernel: Option<&'a str>,
pub target: Option<&'a str>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Selection {
pub distro: Option<Distro>,
pub triple: Option<String>,
pub glibc: Option<GlibcVersion>,
pub kernel: Option<KernelVersion>,
}
impl Selection {
pub fn resolve(req: &Request<'_>) -> Result<Self, String> {
let (triple, suffix) = match req.target {
Some(t) => {
let (triple, suffix) = split_versioned_target(t)?;
(Some(triple), suffix)
}
None => (None, None),
};
if let Some(id) = req.distro {
if req.glibc.is_some() || req.kernel.is_some() || suffix.is_some() {
return Err(format!(
"--distro {id} sets the glibc and kernel floors; \
do not combine it with --glibc, --kernel, or a versioned --target"
));
}
let d = distro(id).ok_or_else(|| format!("unknown distro {id:?}"))?;
return Ok(Selection {
glibc: Some(d.glibc),
kernel: Some(d.kernel),
distro: Some(d),
triple,
});
}
let glibc = match (req.glibc.map(Release::parse).transpose()?, suffix) {
(Some(a), Some(b)) if a != b => {
return Err(format!("--glibc {a} conflicts with --target suffix .{b}"))
}
(a, b) => a.or(b),
};
Ok(Selection {
distro: None,
triple,
glibc,
kernel: req.kernel.map(Release::parse).transpose()?,
})
}
pub fn image(&self) -> Option<&'static str> {
self.distro.as_ref().map(|d| d.image)
}
pub fn describe(&self) -> String {
let glibc = self.glibc.map_or("host".to_string(), |g| g.to_string());
let kernel = self
.kernel
.map_or("not simulated (host)".to_string(), |k| k.to_string());
match &self.distro {
Some(d) => format!("glibc {glibc}, kernel {kernel} ({})", d.name),
None => format!("glibc {glibc}, kernel {kernel}"),
}
}
}
pub fn split_versioned_target(target: &str) -> Result<(String, Option<GlibcVersion>), String> {
let mut parts = target.rsplitn(3, '.');
let (minor, major, triple) = (parts.next(), parts.next(), parts.next());
match (triple, major, minor) {
(Some(triple), Some(major), Some(minor))
if !major.is_empty()
&& major.bytes().all(|b| b.is_ascii_digit())
&& !minor.is_empty()
&& minor.bytes().all(|b| b.is_ascii_digit()) =>
{
if !triple.ends_with("-gnu") {
return Err(format!(
"a glibc suffix needs a -gnu target triple: {target}"
));
}
Ok((
triple.to_string(),
Some(Release::parse(&format!("{major}.{minor}"))?),
))
}
_ => Ok((target.to_string(), None)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(s: &str) -> Release {
Release::parse(s).unwrap()
}
#[test]
fn parses_release_strings() {
assert_eq!(r("4.12.14-122.37-default"), r("4.12"));
assert_eq!(r("v5.14"), r("5.14.0"));
assert!(r("2.17") < r("2.22"));
assert!(Release::parse("linux").is_err());
}
#[test]
fn presets_parse() {
let all = distros();
assert_eq!(all.len(), 28);
let sles = distro("sles-12-sp5").unwrap();
assert_eq!((sles.glibc, sles.kernel), (r("2.22"), r("4.12")));
assert_eq!(sles.image, "registry.suse.com/suse/sles12sp5:latest");
}
#[test]
fn distro_sets_both_floors() {
let s = Selection::resolve(&Request {
distro: Some("sles-12-sp5"),
..Default::default()
})
.unwrap();
assert_eq!((s.glibc, s.kernel), (Some(r("2.22")), Some(r("4.12"))));
assert_eq!(s.image(), Some("registry.suse.com/suse/sles12sp5:latest"));
assert_eq!(
s.describe(),
"glibc 2.22, kernel 4.12 (SUSE Linux Enterprise Server 12 SP5)"
);
}
#[test]
fn distro_excludes_explicit_floors() {
for req in [
Request {
distro: Some("sles-12-sp5"),
glibc: Some("2.28"),
..Default::default()
},
Request {
distro: Some("sles-12-sp5"),
kernel: Some("5.14"),
..Default::default()
},
Request {
distro: Some("sles-12-sp5"),
target: Some("x86_64-unknown-linux-gnu.2.17"),
..Default::default()
},
] {
assert!(Selection::resolve(&req).is_err(), "{req:?}");
}
assert!(Selection::resolve(&Request {
distro: Some("nope"),
..Default::default()
})
.is_err());
}
#[test]
fn missing_kernel_means_host_kernel() {
let s = Selection::resolve(&Request {
target: Some("x86_64-unknown-linux-gnu.2.17"),
..Default::default()
})
.unwrap();
assert_eq!(s.triple.as_deref(), Some("x86_64-unknown-linux-gnu"));
assert_eq!(
(s.glibc, s.kernel, s.image()),
(Some(r("2.17")), None, None)
);
assert_eq!(s.describe(), "glibc 2.17, kernel not simulated (host)");
}
#[test]
fn explicit_floors_combine() {
let s = Selection::resolve(&Request {
glibc: Some("2.22"),
kernel: Some("4.12"),
..Default::default()
})
.unwrap();
assert_eq!((s.glibc, s.kernel), (Some(r("2.22")), Some(r("4.12"))));
let k = Selection::resolve(&Request {
kernel: Some("4.12"),
..Default::default()
})
.unwrap();
assert_eq!((k.glibc, k.kernel), (None, Some(r("4.12"))));
assert_eq!(k.describe(), "glibc host, kernel 4.12");
}
#[test]
fn versioned_target_must_agree_with_glibc() {
let agree = Request {
glibc: Some("2.17"),
target: Some("x86_64-unknown-linux-gnu.2.17"),
..Default::default()
};
assert_eq!(Selection::resolve(&agree).unwrap().glibc, Some(r("2.17")));
let conflict = Request {
glibc: Some("2.22"),
..agree
};
assert!(Selection::resolve(&conflict).is_err());
}
#[test]
fn splits_versioned_targets() {
assert_eq!(
split_versioned_target("x86_64-unknown-linux-gnu.2.17").unwrap(),
("x86_64-unknown-linux-gnu".to_string(), Some(r("2.17")))
);
assert_eq!(
split_versioned_target("x86_64-unknown-linux-gnu").unwrap(),
("x86_64-unknown-linux-gnu".to_string(), None)
);
assert_eq!(
split_versioned_target("thumbv7em-none-eabihf").unwrap().1,
None
);
assert!(split_versioned_target("x86_64-unknown-linux-musl.1.2").is_err());
}
}