1use std::cmp::Ordering;
16use std::fmt;
17
18const DISTRO_DATA: &str = include_str!("../data/distros.tsv");
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct Release {
24 pub major: u32,
25 pub minor: u32,
26}
27
28impl Release {
29 pub fn parse(s: &str) -> Result<Self, String> {
33 let s = s.trim().trim_start_matches(['v', 'V']);
34 let mut parts = s.split(|c: char| !c.is_ascii_digit());
35 let major = parts.next().and_then(|p| p.parse().ok());
36 let minor = parts.next().and_then(|p| p.parse().ok());
37 match (major, minor) {
38 (Some(major), Some(minor)) => Ok(Release { major, minor }),
39 _ => Err(format!("not a release number: {s:?} (expected e.g. 4.12)")),
40 }
41 }
42}
43
44impl Ord for Release {
45 fn cmp(&self, other: &Self) -> Ordering {
46 (self.major, self.minor).cmp(&(other.major, other.minor))
47 }
48}
49
50impl PartialOrd for Release {
51 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52 Some(self.cmp(other))
53 }
54}
55
56impl fmt::Display for Release {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "{}.{}", self.major, self.minor)
59 }
60}
61
62pub type KernelVersion = Release;
64pub type GlibcVersion = Release;
66
67#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct Distro {
70 pub id: &'static str,
71 pub name: &'static str,
72 pub kernel: KernelVersion,
74 pub glibc: GlibcVersion,
75 pub image: &'static str,
77 pub kernel_backports: Vec<&'static str>,
79 pub status: &'static str,
81}
82
83pub fn distros() -> Vec<Distro> {
85 DISTRO_DATA
86 .lines()
87 .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
88 .map(|line| {
89 let cols: Vec<&'static str> = line.split('\t').map(str::trim).collect();
90 let [id, name, kernel, glibc, image, backports, status] = cols[..] else {
91 panic!("distro data row needs 7 columns: {cols:?}");
92 };
93 Distro {
94 id,
95 name,
96 kernel: Release::parse(kernel).expect("distro kernel"),
97 glibc: Release::parse(glibc).expect("distro glibc"),
98 image,
99 kernel_backports: if backports == "-" {
100 Vec::new()
101 } else {
102 backports.split(',').collect()
103 },
104 status,
105 }
106 })
107 .collect()
108}
109
110pub fn distro(id: &str) -> Option<Distro> {
112 distros().into_iter().find(|d| d.id == id)
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
117pub struct Request<'a> {
118 pub distro: Option<&'a str>,
120 pub glibc: Option<&'a str>,
122 pub kernel: Option<&'a str>,
124 pub target: Option<&'a str>,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct Selection {
131 pub distro: Option<Distro>,
133 pub triple: Option<String>,
135 pub glibc: Option<GlibcVersion>,
137 pub kernel: Option<KernelVersion>,
139}
140
141impl Selection {
142 pub fn resolve(req: &Request<'_>) -> Result<Self, String> {
149 let (triple, suffix) = match req.target {
150 Some(t) => {
151 let (triple, suffix) = split_versioned_target(t)?;
152 (Some(triple), suffix)
153 }
154 None => (None, None),
155 };
156 if let Some(id) = req.distro {
157 if req.glibc.is_some() || req.kernel.is_some() || suffix.is_some() {
158 return Err(format!(
159 "--distro {id} sets the glibc and kernel floors; \
160 do not combine it with --glibc, --kernel, or a versioned --target"
161 ));
162 }
163 let d = distro(id).ok_or_else(|| format!("unknown distro {id:?}"))?;
164 return Ok(Selection {
165 glibc: Some(d.glibc),
166 kernel: Some(d.kernel),
167 distro: Some(d),
168 triple,
169 });
170 }
171 let glibc = match (req.glibc.map(Release::parse).transpose()?, suffix) {
172 (Some(a), Some(b)) if a != b => {
173 return Err(format!("--glibc {a} conflicts with --target suffix .{b}"))
174 }
175 (a, b) => a.or(b),
176 };
177 Ok(Selection {
178 distro: None,
179 triple,
180 glibc,
181 kernel: req.kernel.map(Release::parse).transpose()?,
182 })
183 }
184
185 pub fn image(&self) -> Option<&'static str> {
187 self.distro.as_ref().map(|d| d.image)
188 }
189
190 pub fn describe(&self) -> String {
194 let glibc = self.glibc.map_or("host".to_string(), |g| g.to_string());
195 let kernel = self
196 .kernel
197 .map_or("not simulated (host)".to_string(), |k| k.to_string());
198 match &self.distro {
199 Some(d) => format!("glibc {glibc}, kernel {kernel} ({})", d.name),
200 None => format!("glibc {glibc}, kernel {kernel}"),
201 }
202 }
203}
204
205pub fn split_versioned_target(target: &str) -> Result<(String, Option<GlibcVersion>), String> {
208 let mut parts = target.rsplitn(3, '.');
209 let (minor, major, triple) = (parts.next(), parts.next(), parts.next());
210 match (triple, major, minor) {
211 (Some(triple), Some(major), Some(minor))
212 if !major.is_empty()
213 && major.bytes().all(|b| b.is_ascii_digit())
214 && !minor.is_empty()
215 && minor.bytes().all(|b| b.is_ascii_digit()) =>
216 {
217 if !triple.ends_with("-gnu") {
218 return Err(format!(
219 "a glibc suffix needs a -gnu target triple: {target}"
220 ));
221 }
222 Ok((
223 triple.to_string(),
224 Some(Release::parse(&format!("{major}.{minor}"))?),
225 ))
226 }
227 _ => Ok((target.to_string(), None)),
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn r(s: &str) -> Release {
236 Release::parse(s).unwrap()
237 }
238
239 #[test]
240 fn parses_release_strings() {
241 assert_eq!(r("4.12.14-122.37-default"), r("4.12"));
242 assert_eq!(r("v5.14"), r("5.14.0"));
243 assert!(r("2.17") < r("2.22"));
244 assert!(Release::parse("linux").is_err());
245 }
246
247 #[test]
248 fn presets_parse() {
249 let all = distros();
250 assert_eq!(all.len(), 28);
251 let sles = distro("sles-12-sp5").unwrap();
252 assert_eq!((sles.glibc, sles.kernel), (r("2.22"), r("4.12")));
253 assert_eq!(sles.image, "registry.suse.com/suse/sles12sp5:latest");
254 }
255
256 #[test]
257 fn distro_sets_both_floors() {
258 let s = Selection::resolve(&Request {
259 distro: Some("sles-12-sp5"),
260 ..Default::default()
261 })
262 .unwrap();
263 assert_eq!((s.glibc, s.kernel), (Some(r("2.22")), Some(r("4.12"))));
264 assert_eq!(s.image(), Some("registry.suse.com/suse/sles12sp5:latest"));
265 assert_eq!(
266 s.describe(),
267 "glibc 2.22, kernel 4.12 (SUSE Linux Enterprise Server 12 SP5)"
268 );
269 }
270
271 #[test]
272 fn distro_excludes_explicit_floors() {
273 for req in [
274 Request {
275 distro: Some("sles-12-sp5"),
276 glibc: Some("2.28"),
277 ..Default::default()
278 },
279 Request {
280 distro: Some("sles-12-sp5"),
281 kernel: Some("5.14"),
282 ..Default::default()
283 },
284 Request {
285 distro: Some("sles-12-sp5"),
286 target: Some("x86_64-unknown-linux-gnu.2.17"),
287 ..Default::default()
288 },
289 ] {
290 assert!(Selection::resolve(&req).is_err(), "{req:?}");
291 }
292 assert!(Selection::resolve(&Request {
293 distro: Some("nope"),
294 ..Default::default()
295 })
296 .is_err());
297 }
298
299 #[test]
300 fn missing_kernel_means_host_kernel() {
301 let s = Selection::resolve(&Request {
302 target: Some("x86_64-unknown-linux-gnu.2.17"),
303 ..Default::default()
304 })
305 .unwrap();
306 assert_eq!(s.triple.as_deref(), Some("x86_64-unknown-linux-gnu"));
307 assert_eq!(
308 (s.glibc, s.kernel, s.image()),
309 (Some(r("2.17")), None, None)
310 );
311 assert_eq!(s.describe(), "glibc 2.17, kernel not simulated (host)");
312 }
313
314 #[test]
315 fn explicit_floors_combine() {
316 let s = Selection::resolve(&Request {
317 glibc: Some("2.22"),
318 kernel: Some("4.12"),
319 ..Default::default()
320 })
321 .unwrap();
322 assert_eq!((s.glibc, s.kernel), (Some(r("2.22")), Some(r("4.12"))));
323 let k = Selection::resolve(&Request {
324 kernel: Some("4.12"),
325 ..Default::default()
326 })
327 .unwrap();
328 assert_eq!((k.glibc, k.kernel), (None, Some(r("4.12"))));
329 assert_eq!(k.describe(), "glibc host, kernel 4.12");
330 }
331
332 #[test]
333 fn versioned_target_must_agree_with_glibc() {
334 let agree = Request {
335 glibc: Some("2.17"),
336 target: Some("x86_64-unknown-linux-gnu.2.17"),
337 ..Default::default()
338 };
339 assert_eq!(Selection::resolve(&agree).unwrap().glibc, Some(r("2.17")));
340 let conflict = Request {
341 glibc: Some("2.22"),
342 ..agree
343 };
344 assert!(Selection::resolve(&conflict).is_err());
345 }
346
347 #[test]
348 fn splits_versioned_targets() {
349 assert_eq!(
350 split_versioned_target("x86_64-unknown-linux-gnu.2.17").unwrap(),
351 ("x86_64-unknown-linux-gnu".to_string(), Some(r("2.17")))
352 );
353 assert_eq!(
354 split_versioned_target("x86_64-unknown-linux-gnu").unwrap(),
355 ("x86_64-unknown-linux-gnu".to_string(), None)
356 );
357 assert_eq!(
358 split_versioned_target("thumbv7em-none-eabihf").unwrap().1,
359 None
360 );
361 assert!(split_versioned_target("x86_64-unknown-linux-musl.1.2").is_err());
362 }
363}