#![warn(missing_docs)]
#![allow(clippy::pub_use)]
use std::clone::Clone;
use std::collections::HashMap;
use std::fs;
use std::io::{Error as IoError, ErrorKind};
use regex::RegexBuilder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use yai::YAIError;
mod data;
pub mod yai;
#[cfg(test)]
pub mod tests;
pub use data::VariantKind;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VariantError {
#[error("Unknown variant '{0}'")]
BadVariant(String),
#[error("Checking for {0}: could not read {1}")]
FileRead(String, String, #[source] IoError),
#[error("Could not parse the /etc/os-release file")]
OsRelease(#[source] YAIError),
#[error("Could not detect the current host's build variant")]
UnknownVariant,
#[error("Internal sp-variant error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VariantFormatVersion {
pub major: u32,
pub minor: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VariantFormat {
pub version: VariantFormatVersion,
}
#[derive(Debug, Serialize, Deserialize)]
struct VariantFormatTop {
format: VariantFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Detect {
pub filename: String,
pub regex: String,
pub os_id: String,
pub os_version_regex: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DebRepo {
pub codename: String,
pub vendor: String,
pub sources: String,
pub keyring: String,
pub req_packages: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct YumRepo {
pub yumdef: String,
pub keyring: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Repo {
Deb(DebRepo),
Yum(YumRepo),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Builder {
pub alias: String,
pub base_image: String,
pub branch: String,
pub kernel_package: String,
pub utf8_locale: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Variant {
#[serde(rename = "name")]
pub kind: VariantKind,
pub descr: String,
pub family: String,
pub parent: String,
pub detect: Detect,
pub commands: HashMap<String, HashMap<String, Vec<String>>>,
pub min_sys_python: String,
pub repo: Repo,
pub package: HashMap<String, String>,
pub systemd_lib: String,
pub file_ext: String,
pub initramfs_flavor: String,
pub builder: Builder,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VariantDefTop {
format: VariantFormat,
order: Vec<VariantKind>,
variants: HashMap<VariantKind, Variant>,
version: String,
}
#[inline]
#[must_use]
pub fn build_variants() -> &'static VariantDefTop {
data::get_variants()
}
#[inline]
pub fn detect() -> Result<Variant, VariantError> {
detect_from(build_variants()).map(Clone::clone)
}
#[allow(clippy::missing_inline_in_public_items)]
pub fn detect_from(variants: &VariantDefTop) -> Result<&Variant, VariantError> {
match yai::parse("/etc/os-release") {
Ok(data) => {
if let Some(os_id) = data.get("ID") {
if let Some(version_id) = data.get("VERSION_ID") {
for kind in &variants.order {
let var = &variants.variants.get(kind).ok_or_else(|| {
VariantError::Internal(format!(
"Internal error: unknown variant {} in the order",
kind.as_ref()
))
})?;
if var.detect.os_id != *os_id {
continue;
}
let re_ver = RegexBuilder::new(&var.detect.os_version_regex)
.ignore_whitespace(true)
.build()
.map_err(|err| {
VariantError::Internal(format!(
"Internal error: {}: could not parse '{}': {}",
kind.as_ref(),
var.detect.regex,
err
))
})?;
if re_ver.is_match(version_id) {
return Ok(var);
}
}
}
}
}
Err(YAIError::FileRead(io_err)) if io_err.kind() == ErrorKind::NotFound => (),
Err(err) => return Err(VariantError::OsRelease(err)),
}
for kind in &variants.order {
let var = &variants.variants.get(kind).ok_or_else(|| {
VariantError::Internal(format!(
"Internal error: unknown variant {} in the order",
kind.as_ref()
))
})?;
let re_line = RegexBuilder::new(&var.detect.regex)
.ignore_whitespace(true)
.build()
.map_err(|err| {
VariantError::Internal(format!(
"Internal error: {}: could not parse '{}': {}",
kind.as_ref(),
var.detect.regex,
err
))
})?;
match fs::read(&var.detect.filename) {
Ok(file_bytes) => {
if let Ok(contents) = String::from_utf8(file_bytes) {
{
if contents.lines().any(|line| re_line.is_match(line)) {
return Ok(var);
}
}
}
}
Err(err) => {
if err.kind() != ErrorKind::NotFound {
return Err(VariantError::FileRead(
var.kind.as_ref().to_owned(),
var.detect.filename.clone(),
err,
));
}
}
};
}
Err(VariantError::UnknownVariant)
}
#[inline]
pub fn get_from<'defs>(
variants: &'defs VariantDefTop,
name: &str,
) -> Result<&'defs Variant, VariantError> {
let kind: VariantKind = name.parse()?;
variants
.variants
.get(&kind)
.ok_or_else(|| VariantError::Internal(format!("No data for the {} variant", name)))
}
#[inline]
pub fn get_by_alias_from<'defs>(
variants: &'defs VariantDefTop,
alias: &str,
) -> Result<&'defs Variant, VariantError> {
variants
.variants
.values()
.find(|var| var.builder.alias == alias)
.ok_or_else(|| VariantError::Internal(format!("No variant with the {} alias", alias)))
}
#[inline]
#[must_use]
pub fn get_format_version() -> (u32, u32) {
get_format_version_from(build_variants())
}
#[inline]
#[must_use]
pub const fn get_format_version_from(variants: &VariantDefTop) -> (u32, u32) {
(variants.format.version.major, variants.format.version.minor)
}
#[inline]
#[must_use]
pub fn get_program_version() -> &'static str {
get_program_version_from(build_variants())
}
#[inline]
#[must_use]
pub fn get_program_version_from(variants: &VariantDefTop) -> &str {
&variants.version
}