particle-os-cli/recipe/src/module.rs

269 lines
8.9 KiB
Rust

use std::{borrow::Cow, path::PathBuf};
use blue_build_utils::syntax_highlighting::highlight_ser;
use bon::Builder;
use colored::Colorize;
use indexmap::IndexMap;
use log::trace;
use miette::{bail, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use crate::{base_recipe_path, AkmodsInfo, ModuleExt};
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)]
pub struct ModuleRequiredFields<'a> {
#[builder(into)]
#[serde(rename = "type")]
pub module_type: Cow<'a, str>,
#[builder(into)]
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Cow<'a, str>>,
#[builder(default)]
#[serde(rename = "no-cache", default, skip_serializing_if = "is_false")]
pub no_cache: bool,
#[serde(flatten)]
#[builder(default, into)]
pub config: IndexMap<String, Value>,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_false(b: &bool) -> bool {
!*b
}
impl<'a> ModuleRequiredFields<'a> {
#[must_use]
pub fn get_module_type_list(&'a self, typ: &str, list_key: &str) -> Option<Vec<String>> {
if self.module_type == typ {
Some(
self.config
.get(list_key)?
.as_sequence()?
.iter()
.filter_map(|t| Some(t.as_str()?.to_owned()))
.collect(),
)
} else {
None
}
}
#[must_use]
pub fn get_containerfile_list(&'a self) -> Option<Vec<String>> {
self.get_module_type_list("containerfile", "containerfiles")
}
#[must_use]
pub fn get_containerfile_snippets(&'a self) -> Option<Vec<String>> {
self.get_module_type_list("containerfile", "snippets")
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn get_copy_args(&'a self) -> Option<(Option<&'a str>, &'a str, &'a str)> {
#[cfg(feature = "copy")]
{
Some((
self.config.get("from").and_then(|from| from.as_str()),
self.config.get("src")?.as_str()?,
self.config.get("dest")?.as_str()?,
))
}
#[cfg(not(feature = "copy"))]
{
None
}
}
#[must_use]
pub fn get_non_local_source(&'a self) -> Option<&'a str> {
let source = self.source.as_deref()?;
if source == "local" {
None
} else {
Some(source)
}
}
#[must_use]
pub fn generate_akmods_info(&'a self, os_version: &u64) -> AkmodsInfo {
#[derive(Debug, Default, Copy, Clone)]
enum NvidiaAkmods {
#[default]
Disabled,
Enabled,
Open,
Proprietary,
}
impl From<&Value> for NvidiaAkmods {
fn from(value: &Value) -> Self {
match value.get("nvidia") {
Some(enabled) if enabled.is_bool() => match enabled.as_bool() {
Some(true) => Self::Enabled,
_ => Self::Disabled,
},
Some(driver_type) if driver_type.is_string() => match driver_type.as_str() {
Some("open") => Self::Open,
Some("proprietary") => Self::Proprietary,
_ => Self::Disabled,
},
_ => Self::Disabled,
}
}
}
trace!("generate_akmods_base({self:#?}, {os_version})");
let base = self
.config
.get("base")
.map(|b| b.as_str().unwrap_or_default());
let nvidia = self
.config
.get("nvidia")
.map_or_else(Default::default, NvidiaAkmods::from);
AkmodsInfo::builder()
.images(match (base, nvidia) {
(Some(b), NvidiaAkmods::Enabled | NvidiaAkmods::Proprietary) if !b.is_empty() => (
format!("akmods:{b}-{os_version}"),
format!("akmods-extra:{b}-{os_version}"),
Some(format!("akmods-nvidia:{b}-{os_version}")),
),
(Some(b), NvidiaAkmods::Disabled) if !b.is_empty() => (
format!("akmods:{b}-{os_version}"),
format!("akmods-extra:{b}-{os_version}"),
None,
),
(Some(b), NvidiaAkmods::Open) if !b.is_empty() => (
format!("akmods:{b}-{os_version}"),
format!("akmods-extra:{b}-{os_version}"),
Some(format!("akmods-nvidia-open:{b}-{os_version}")),
),
(_, NvidiaAkmods::Enabled | NvidiaAkmods::Proprietary) => (
format!("akmods:main-{os_version}"),
format!("akmods-extra:main-{os_version}"),
Some(format!("akmods-nvidia:main-{os_version}")),
),
(_, NvidiaAkmods::Disabled) => (
format!("akmods:main-{os_version}"),
format!("akmods-extra:main-{os_version}"),
None,
),
(_, NvidiaAkmods::Open) => (
format!("akmods:main-{os_version}"),
format!("akmods-extra:main-{os_version}"),
Some(format!("akmods-nvidia-open:main-{os_version}")),
),
})
.stage_name(format!(
"{}{}",
base.unwrap_or("main"),
match nvidia {
NvidiaAkmods::Disabled => String::default(),
_ => "-nvidia".to_string(),
}
))
.build()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)]
pub struct Module<'a> {
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub required_fields: Option<ModuleRequiredFields<'a>>,
#[builder(into)]
#[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
pub from_file: Option<Cow<'a, str>>,
}
impl Module<'_> {
/// Get's any child modules.
///
/// # Errors
/// Will error if the module cannot be
/// deserialized or the user uses another
/// property alongside `from-file:`.
pub fn get_modules(
modules: &[Self],
traversed_files: Option<Vec<PathBuf>>,
) -> Result<Vec<Self>> {
let mut found_modules = vec![];
let traversed_files = traversed_files.unwrap_or_default();
for module in modules {
found_modules.extend(
match &module {
Module {
required_fields: Some(_),
from_file: None,
} => vec![module.clone()],
Module {
required_fields: None,
from_file: Some(file_name),
} => {
let file_name = PathBuf::from(file_name.as_ref());
if traversed_files.contains(&file_name) {
bail!(
"{} File {} has already been parsed:\n{traversed_files:?}",
"Circular dependency detected!".bright_red(),
file_name.display().to_string().bold(),
);
}
let mut traversed_files = traversed_files.clone();
traversed_files.push(file_name.clone());
Self::get_modules(
&ModuleExt::try_from(&file_name)?.modules,
Some(traversed_files),
)?
}
_ => {
let from_example = Self::builder().from_file("test.yml").build();
let module_example = Self::example();
bail!(
"Improper format for module. Must be in the format like:\n{}\n{}\n\n{}",
highlight_ser(&module_example, "yaml", None)?,
"or".bold(),
highlight_ser(&from_example, "yaml", None)?
);
}
}
.into_iter(),
);
}
Ok(found_modules)
}
#[must_use]
pub fn get_from_file_path(&self) -> Option<PathBuf> {
self.from_file
.as_ref()
.map(|path| base_recipe_path().join(&**path))
}
#[must_use]
pub fn example() -> Self {
Self::builder()
.required_fields(
ModuleRequiredFields::builder()
.module_type("module-name")
.config(IndexMap::from_iter([
("module".to_string(), Value::String("config".to_string())),
("goes".to_string(), Value::String("here".to_string())),
]))
.build(),
)
.build()
}
}