1#![warn(missing_docs)]
45
46use std::cmp::Ordering;
47use std::fmt;
48
49const DISTRO_DATA: &str = include_str!("../data/distros.tsv");
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub struct Release {
55 pub major: u32,
57 pub minor: u32,
59}
60
61impl Release {
62 pub fn parse(s: &str) -> Result<Self, String> {
71 let s = s.trim().trim_start_matches(['v', 'V']);
72 let mut parts = s.split(|c: char| !c.is_ascii_digit());
73 let major = parts.next().and_then(|p| p.parse().ok());
74 let minor = parts.next().and_then(|p| p.parse().ok());
75 match (major, minor) {
76 (Some(major), Some(minor)) => Ok(Release { major, minor }),
77 _ => Err(format!("not a release number: {s:?} (expected e.g. 4.12)")),
78 }
79 }
80}
81
82impl Ord for Release {
83 fn cmp(&self, other: &Self) -> Ordering {
84 (self.major, self.minor).cmp(&(other.major, other.minor))
85 }
86}
87
88impl PartialOrd for Release {
89 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
90 Some(self.cmp(other))
91 }
92}
93
94impl fmt::Display for Release {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(f, "{}.{}", self.major, self.minor)
97 }
98}
99
100pub type KernelVersion = Release;
102pub type GlibcVersion = Release;
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct Distro {
108 pub id: &'static str,
110 pub name: &'static str,
112 pub kernel: KernelVersion,
114 pub glibc: GlibcVersion,
116 pub image: &'static str,
118 pub kernel_backports: Vec<&'static str>,
120 pub status: &'static str,
122}
123
124pub fn distros() -> Vec<Distro> {
126 DISTRO_DATA
127 .lines()
128 .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
129 .map(|line| {
130 let cols: Vec<&'static str> = line.split('\t').map(str::trim).collect();
131 let [id, name, kernel, glibc, image, backports, status] = cols[..] else {
132 panic!("distro data row needs 7 columns: {cols:?}");
133 };
134 Distro {
135 id,
136 name,
137 kernel: Release::parse(kernel).expect("distro kernel"),
138 glibc: Release::parse(glibc).expect("distro glibc"),
139 image,
140 kernel_backports: if backports == "-" {
141 Vec::new()
142 } else {
143 backports.split(',').collect()
144 },
145 status,
146 }
147 })
148 .collect()
149}
150
151pub fn distro(id: &str) -> Option<Distro> {
153 distros().into_iter().find(|d| d.id == id)
154}
155
156#[derive(Clone, Debug, Default, PartialEq, Eq)]
158pub struct Request<'a> {
159 pub distro: Option<&'a str>,
161 pub glibc: Option<&'a str>,
163 pub kernel: Option<&'a str>,
165 pub target: Option<&'a str>,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct Selection {
172 pub distro: Option<Distro>,
174 pub triple: Option<String>,
176 pub glibc: Option<GlibcVersion>,
178 pub kernel: Option<KernelVersion>,
180}
181
182impl Selection {
183 pub fn resolve(req: &Request<'_>) -> Result<Self, String> {
196 let (triple, suffix) = match req.target {
197 Some(t) => {
198 let (triple, suffix) = split_versioned_target(t)?;
199 (Some(triple), suffix)
200 }
201 None => (None, None),
202 };
203 if let Some(id) = req.distro {
204 if req.glibc.is_some() || req.kernel.is_some() || suffix.is_some() {
205 return Err(format!(
206 "--distro {id} sets the glibc and kernel floors; \
207 do not combine it with --glibc, --kernel, or a versioned --target"
208 ));
209 }
210 let d = distro(id).ok_or_else(|| format!("unknown distro {id:?}"))?;
211 return Ok(Selection {
212 glibc: Some(d.glibc),
213 kernel: Some(d.kernel),
214 distro: Some(d),
215 triple,
216 });
217 }
218 let glibc = match (req.glibc.map(Release::parse).transpose()?, suffix) {
219 (Some(a), Some(b)) if a != b => {
220 return Err(format!("--glibc {a} conflicts with --target suffix .{b}"))
221 }
222 (a, b) => a.or(b),
223 };
224 Ok(Selection {
225 distro: None,
226 triple,
227 glibc,
228 kernel: req.kernel.map(Release::parse).transpose()?,
229 })
230 }
231
232 pub fn image(&self) -> Option<&'static str> {
234 self.distro.as_ref().map(|d| d.image)
235 }
236
237 pub fn describe(&self) -> String {
241 let glibc = self.glibc.map_or("host".to_string(), |g| g.to_string());
242 let kernel = self
243 .kernel
244 .map_or("not simulated (host)".to_string(), |k| k.to_string());
245 match &self.distro {
246 Some(d) => format!("glibc {glibc}, kernel {kernel} ({})", d.name),
247 None => format!("glibc {glibc}, kernel {kernel}"),
248 }
249 }
250}
251
252pub fn split_versioned_target(target: &str) -> Result<(String, Option<GlibcVersion>), String> {
260 let mut parts = target.rsplitn(3, '.');
261 let (minor, major, triple) = (parts.next(), parts.next(), parts.next());
262 match (triple, major, minor) {
263 (Some(triple), Some(major), Some(minor))
264 if !major.is_empty()
265 && major.bytes().all(|b| b.is_ascii_digit())
266 && !minor.is_empty()
267 && minor.bytes().all(|b| b.is_ascii_digit()) =>
268 {
269 if !triple.ends_with("-gnu") {
270 return Err(format!(
271 "a glibc suffix needs a -gnu target triple: {target}"
272 ));
273 }
274 Ok((
275 triple.to_string(),
276 Some(Release::parse(&format!("{major}.{minor}"))?),
277 ))
278 }
279 _ => Ok((target.to_string(), None)),
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn r(s: &str) -> Release {
288 Release::parse(s).unwrap()
289 }
290
291 #[test]
292 fn parses_release_strings() {
293 assert_eq!(r("4.12.14-122.37-default"), r("4.12"));
294 assert_eq!(r("v5.14"), r("5.14.0"));
295 assert!(r("2.17") < r("2.22"));
296 assert!(Release::parse("linux").is_err());
297 }
298
299 #[test]
300 fn presets_parse() {
301 let all = distros();
302 assert_eq!(all.len(), 28);
303 let sles = distro("sles-12-sp5").unwrap();
304 assert_eq!((sles.glibc, sles.kernel), (r("2.22"), r("4.12")));
305 assert_eq!(sles.image, "registry.suse.com/suse/sles12sp5:latest");
306 }
307
308 #[test]
309 fn distro_sets_both_floors() {
310 let s = Selection::resolve(&Request {
311 distro: Some("sles-12-sp5"),
312 ..Default::default()
313 })
314 .unwrap();
315 assert_eq!((s.glibc, s.kernel), (Some(r("2.22")), Some(r("4.12"))));
316 assert_eq!(s.image(), Some("registry.suse.com/suse/sles12sp5:latest"));
317 assert_eq!(
318 s.describe(),
319 "glibc 2.22, kernel 4.12 (SUSE Linux Enterprise Server 12 SP5)"
320 );
321 }
322
323 #[test]
324 fn distro_excludes_explicit_floors() {
325 for req in [
326 Request {
327 distro: Some("sles-12-sp5"),
328 glibc: Some("2.28"),
329 ..Default::default()
330 },
331 Request {
332 distro: Some("sles-12-sp5"),
333 kernel: Some("5.14"),
334 ..Default::default()
335 },
336 Request {
337 distro: Some("sles-12-sp5"),
338 target: Some("x86_64-unknown-linux-gnu.2.17"),
339 ..Default::default()
340 },
341 ] {
342 assert!(Selection::resolve(&req).is_err(), "{req:?}");
343 }
344 assert!(Selection::resolve(&Request {
345 distro: Some("nope"),
346 ..Default::default()
347 })
348 .is_err());
349 }
350
351 #[test]
352 fn missing_kernel_means_host_kernel() {
353 let s = Selection::resolve(&Request {
354 target: Some("x86_64-unknown-linux-gnu.2.17"),
355 ..Default::default()
356 })
357 .unwrap();
358 assert_eq!(s.triple.as_deref(), Some("x86_64-unknown-linux-gnu"));
359 assert_eq!(
360 (s.glibc, s.kernel, s.image()),
361 (Some(r("2.17")), None, None)
362 );
363 assert_eq!(s.describe(), "glibc 2.17, kernel not simulated (host)");
364 }
365
366 #[test]
367 fn explicit_floors_combine() {
368 let s = Selection::resolve(&Request {
369 glibc: Some("2.22"),
370 kernel: Some("4.12"),
371 ..Default::default()
372 })
373 .unwrap();
374 assert_eq!((s.glibc, s.kernel), (Some(r("2.22")), Some(r("4.12"))));
375 let k = Selection::resolve(&Request {
376 kernel: Some("4.12"),
377 ..Default::default()
378 })
379 .unwrap();
380 assert_eq!((k.glibc, k.kernel), (None, Some(r("4.12"))));
381 assert_eq!(k.describe(), "glibc host, kernel 4.12");
382 }
383
384 #[test]
385 fn versioned_target_must_agree_with_glibc() {
386 let agree = Request {
387 glibc: Some("2.17"),
388 target: Some("x86_64-unknown-linux-gnu.2.17"),
389 ..Default::default()
390 };
391 assert_eq!(Selection::resolve(&agree).unwrap().glibc, Some(r("2.17")));
392 let conflict = Request {
393 glibc: Some("2.22"),
394 ..agree
395 };
396 assert!(Selection::resolve(&conflict).is_err());
397 }
398
399 #[test]
400 fn splits_versioned_targets() {
401 assert_eq!(
402 split_versioned_target("x86_64-unknown-linux-gnu.2.17").unwrap(),
403 ("x86_64-unknown-linux-gnu".to_string(), Some(r("2.17")))
404 );
405 assert_eq!(
406 split_versioned_target("x86_64-unknown-linux-gnu").unwrap(),
407 ("x86_64-unknown-linux-gnu".to_string(), None)
408 );
409 assert_eq!(
410 split_versioned_target("thumbv7em-none-eabihf").unwrap().1,
411 None
412 );
413 assert!(split_versioned_target("x86_64-unknown-linux-musl.1.2").is_err());
414 }
415}