1#![warn(missing_docs)]
8#![allow(clippy::pub_use)]
10
11use std::clone::Clone;
12use std::collections::HashMap;
13use std::fs;
14use std::io::{Error as IoError, ErrorKind};
15
16use regex::RegexBuilder;
17use serde_derive::{Deserialize, Serialize};
18use thiserror::Error;
19
20use yai::YAIError;
21
22mod data;
23
24pub mod yai;
25
26#[cfg(test)]
27pub mod tests;
28
29pub use data::VariantKind;
30
31#[derive(Debug, Error)]
33#[non_exhaustive]
34pub enum VariantError {
35 #[error("Unknown variant '{0}'")]
37 BadVariant(String),
38
39 #[error("Checking for {0}: could not read {1}")]
41 FileRead(String, String, #[source] IoError),
42
43 #[error("Could not parse the /etc/os-release file")]
45 OsRelease(#[source] YAIError),
46
47 #[error("Could not detect the current host's build variant")]
49 UnknownVariant,
50
51 #[error("Internal sp-variant error: {0}")]
53 Internal(String),
54}
55
56#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
58#[non_exhaustive]
59pub struct VariantFormatVersion {
60 pub major: u32,
62 pub minor: u32,
64}
65
66#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct VariantFormat {
70 pub version: VariantFormatVersion,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
75struct VariantFormatTop {
76 format: VariantFormat,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct Detect {
83 pub filename: String,
85 pub regex: String,
87 pub os_id: String,
89 pub os_version_regex: String,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[non_exhaustive]
96pub struct Supported {
97 pub repo: bool,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[non_exhaustive]
104pub struct DebRepo {
105 pub codename: String,
107 pub vendor: String,
109 pub sources: String,
111 pub keyring: String,
113 pub req_packages: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[non_exhaustive]
120pub struct YumRepo {
121 pub yumdef: String,
123 pub keyring: String,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(untagged)]
130#[non_exhaustive]
131pub enum Repo {
132 Deb(DebRepo),
134 Yum(YumRepo),
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[non_exhaustive]
141pub struct Builder {
142 pub alias: String,
144 pub base_image: String,
146 pub branch: String,
148 pub kernel_package: String,
150 pub utf8_locale: String,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[non_exhaustive]
157pub struct Variant {
158 #[serde(rename = "name")]
160 pub kind: VariantKind,
161 pub descr: String,
163 pub family: String,
165 pub parent: String,
167 pub detect: Detect,
169 pub supported: Supported,
171 pub commands: HashMap<String, HashMap<String, Vec<String>>>,
173 pub min_sys_python: String,
175 pub repo: Repo,
177 pub package: HashMap<String, String>,
179 pub systemd_lib: String,
181 pub file_ext: String,
183 pub initramfs_flavor: String,
185 pub builder: Builder,
187}
188
189#[derive(Debug, Serialize, Deserialize)]
191pub struct VariantDefTop {
192 format: VariantFormat,
193 order: Vec<VariantKind>,
194 variants: HashMap<VariantKind, Variant>,
195 version: String,
196}
197
198#[inline]
200#[must_use]
201pub fn build_variants() -> &'static VariantDefTop {
202 data::get_variants()
203}
204
205#[inline]
210pub fn detect() -> Result<Variant, VariantError> {
211 detect_from(build_variants()).cloned()
212}
213
214#[allow(clippy::missing_inline_in_public_items)]
222pub fn detect_from(variants: &VariantDefTop) -> Result<&Variant, VariantError> {
223 match yai::parse("/etc/os-release") {
224 Ok(data) => {
225 if let Some(os_id) = data.get("ID") {
226 if let Some(version_id) = data.get("VERSION_ID") {
227 for kind in &variants.order {
228 let var = &variants.variants.get(kind).ok_or_else(|| {
229 VariantError::Internal(format!(
230 "Internal error: unknown variant {kind} in the order",
231 kind = kind.as_ref()
232 ))
233 })?;
234 if var.detect.os_id != *os_id {
235 continue;
236 }
237 let re_ver = RegexBuilder::new(&var.detect.os_version_regex)
238 .ignore_whitespace(true)
239 .build()
240 .map_err(|err| {
241 VariantError::Internal(format!(
242 "Internal error: {kind}: could not parse '{regex}': {err}",
243 kind = kind.as_ref(),
244 regex = var.detect.regex
245 ))
246 })?;
247 if re_ver.is_match(version_id) {
248 return Ok(var);
249 }
250 }
251 }
252 }
253 }
255 Err(YAIError::FileRead(io_err)) if io_err.kind() == ErrorKind::NotFound => (),
256 Err(err) => return Err(VariantError::OsRelease(err)),
257 }
258
259 for kind in &variants.order {
260 let var = &variants.variants.get(kind).ok_or_else(|| {
261 VariantError::Internal(format!(
262 "Internal error: unknown variant {kind} in the order",
263 kind = kind.as_ref()
264 ))
265 })?;
266 let re_line = RegexBuilder::new(&var.detect.regex)
267 .ignore_whitespace(true)
268 .build()
269 .map_err(|err| {
270 VariantError::Internal(format!(
271 "Internal error: {kind}: could not parse '{regex}': {err}",
272 kind = kind.as_ref(),
273 regex = var.detect.regex
274 ))
275 })?;
276 match fs::read(&var.detect.filename) {
277 Ok(file_bytes) => {
278 if let Ok(contents) = String::from_utf8(file_bytes) {
279 {
280 if contents.lines().any(|line| re_line.is_match(line)) {
281 return Ok(var);
282 }
283 }
284 }
285 }
286 Err(err) => {
287 if err.kind() != ErrorKind::NotFound {
288 return Err(VariantError::FileRead(
289 var.kind.as_ref().to_owned(),
290 var.detect.filename.clone(),
291 err,
292 ));
293 }
294 }
295 };
296 }
297 Err(VariantError::UnknownVariant)
298}
299
300#[inline]
306pub fn get_from<'defs>(
307 variants: &'defs VariantDefTop,
308 name: &str,
309) -> Result<&'defs Variant, VariantError> {
310 let kind: VariantKind = name.parse()?;
311 variants
312 .variants
313 .get(&kind)
314 .ok_or_else(|| VariantError::Internal(format!("No data for the {name} variant")))
315}
316
317#[inline]
322pub fn get_by_alias_from<'defs>(
323 variants: &'defs VariantDefTop,
324 alias: &str,
325) -> Result<&'defs Variant, VariantError> {
326 variants
327 .variants
328 .values()
329 .find(|var| var.builder.alias == alias)
330 .ok_or_else(|| VariantError::Internal(format!("No variant with the {alias} alias")))
331}
332
333#[inline]
335#[must_use]
336pub fn get_all_variants() -> &'static HashMap<VariantKind, Variant> {
337 get_all_variants_from(build_variants())
338}
339
340#[inline]
342#[must_use]
343pub const fn get_all_variants_from(variants: &VariantDefTop) -> &HashMap<VariantKind, Variant> {
344 &variants.variants
345}
346
347#[inline]
349pub fn get_all_variants_in_order() -> impl Iterator<Item = &'static Variant> {
350 get_all_variants_in_order_from(build_variants())
351}
352
353#[inline]
360#[allow(clippy::indexing_slicing)]
361pub fn get_all_variants_in_order_from(variants: &VariantDefTop) -> impl Iterator<Item = &Variant> {
362 variants.order.iter().map(|kind| &variants.variants[kind])
363}
364
365#[inline]
367#[must_use]
368pub fn get_format_version() -> (u32, u32) {
369 get_format_version_from(build_variants())
370}
371
372#[inline]
374#[must_use]
375pub const fn get_format_version_from(variants: &VariantDefTop) -> (u32, u32) {
376 (variants.format.version.major, variants.format.version.minor)
377}
378
379#[inline]
381#[must_use]
382pub fn get_program_version() -> &'static str {
383 get_program_version_from(build_variants())
384}
385
386#[inline]
388#[must_use]
389pub fn get_program_version_from(variants: &VariantDefTop) -> &str {
390 &variants.version
391}