Skip to main content

blue_build_recipe/
module.rs

1use std::path::PathBuf;
2
3use blue_build_utils::{
4    constants::BLUE_BUILD_MODULE_IMAGE_REF, secret::Secret, syntax_highlighting::highlight_ser,
5};
6use bon::Builder;
7use colored::Colorize;
8use indexmap::IndexMap;
9use log::trace;
10use miette::{Result, bail};
11use serde::{Deserialize, Serialize};
12use serde_yaml::Value;
13
14use crate::{AkmodsInfo, ModuleExt, base_recipe_path};
15
16mod type_ver;
17
18pub use type_ver::*;
19
20#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
21pub struct ModuleRequiredFields {
22    #[builder(into)]
23    #[serde(rename = "type")]
24    pub module_type: ModuleTypeVersion,
25
26    #[builder(into)]
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub source: Option<String>,
29
30    #[builder(default)]
31    #[serde(rename = "no-cache", default, skip_serializing_if = "is_false")]
32    pub no_cache: bool,
33
34    #[builder(into)]
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub env: Option<IndexMap<String, String>>,
37
38    #[builder(into, default)]
39    #[serde(skip_serializing_if = "Vec::is_empty", default)]
40    pub secrets: Vec<Secret>,
41
42    #[serde(flatten)]
43    #[builder(default, into)]
44    pub config: IndexMap<String, Value>,
45}
46
47#[expect(clippy::trivially_copy_pass_by_ref)]
48const fn is_false(b: &bool) -> bool {
49    !*b
50}
51
52impl ModuleRequiredFields {
53    #[must_use]
54    pub fn get_module_type_list(&self, typ: &str, list_key: &str) -> Option<Vec<String>> {
55        if self.module_type.typ() == typ {
56            Some(
57                self.config
58                    .get(list_key)?
59                    .as_sequence()?
60                    .iter()
61                    .filter_map(|t| Some(t.as_str()?.to_owned()))
62                    .collect(),
63            )
64        } else {
65            None
66        }
67    }
68
69    #[must_use]
70    pub fn get_containerfile_list(&self) -> Option<Vec<String>> {
71        self.get_module_type_list("containerfile", "containerfiles")
72    }
73
74    #[must_use]
75    pub fn get_containerfile_snippets(&self) -> Option<Vec<String>> {
76        self.get_module_type_list("containerfile", "snippets")
77    }
78
79    #[must_use]
80    pub fn get_copy_args(&self) -> Option<(Option<&str>, &str, &str)> {
81        Some((
82            self.config.get("from").and_then(|from| from.as_str()),
83            self.config.get("src")?.as_str()?,
84            self.config.get("dest")?.as_str()?,
85        ))
86    }
87
88    #[must_use]
89    pub fn get_env(&self) -> Vec<(&String, &String)> {
90        self.env
91            .as_ref()
92            .iter()
93            .flat_map(|args| args.iter())
94            .collect()
95    }
96
97    #[must_use]
98    pub fn get_non_local_source(&self) -> Option<&str> {
99        let source = self.source.as_deref()?;
100
101        if source == "local" {
102            None
103        } else {
104            Some(source)
105        }
106    }
107
108    #[must_use]
109    pub fn get_module_image(&self) -> String {
110        format!(
111            "{BLUE_BUILD_MODULE_IMAGE_REF}/{}:{}",
112            self.module_type.typ(),
113            self.module_type.version().unwrap_or("latest")
114        )
115    }
116
117    #[must_use]
118    pub fn is_local_source(&self) -> bool {
119        self.source
120            .as_deref()
121            .is_some_and(|source| source == "local")
122    }
123
124    #[must_use]
125    pub fn generate_akmods_info(&self, os_version: &u64) -> AkmodsInfo {
126        #[derive(Debug, Default, Copy, Clone)]
127        enum NvidiaAkmods {
128            #[default]
129            Disabled,
130            Enabled,
131            Open,
132            Proprietary,
133        }
134
135        impl From<&Value> for NvidiaAkmods {
136            fn from(value: &Value) -> Self {
137                match value.get("nvidia") {
138                    Some(enabled) if enabled.is_bool() => match enabled.as_bool() {
139                        Some(true) => Self::Enabled,
140                        _ => Self::Disabled,
141                    },
142                    Some(driver_type) if driver_type.is_string() => match driver_type.as_str() {
143                        Some("open") => Self::Open,
144                        Some("proprietary") => Self::Proprietary,
145                        _ => Self::Disabled,
146                    },
147                    _ => Self::Disabled,
148                }
149            }
150        }
151
152        trace!("generate_akmods_base({self:#?}, {os_version})");
153
154        let base = self
155            .config
156            .get("base")
157            .map(|b| b.as_str().unwrap_or_default());
158        let nvidia = self
159            .config
160            .get("nvidia")
161            .map_or_else(Default::default, NvidiaAkmods::from);
162
163        AkmodsInfo::builder()
164            .images(match (base, nvidia) {
165                (Some("bazzite"), NvidiaAkmods::Enabled | NvidiaAkmods::Proprietary) => (
166                    format!("akmods:bazzite-{os_version}"),
167                    Some(format!("akmods-extra:bazzite-{os_version}")),
168                    Some(format!("akmods-nvidia:bazzite-{os_version}")),
169                ),
170                (Some("bazzite"), NvidiaAkmods::Disabled) => (
171                    format!("akmods:bazzite-{os_version}"),
172                    Some(format!("akmods-extra:bazzite-{os_version}")),
173                    None,
174                ),
175                (Some("bazzite"), NvidiaAkmods::Open) => (
176                    format!("akmods:bazzite-{os_version}"),
177                    Some(format!("akmods-extra:bazzite-{os_version}")),
178                    Some(format!("akmods-nvidia-open:bazzite-{os_version}")),
179                ),
180                (Some(b), NvidiaAkmods::Enabled | NvidiaAkmods::Proprietary) if !b.is_empty() => (
181                    format!("akmods:{b}-{os_version}"),
182                    None,
183                    Some(format!("akmods-nvidia:{b}-{os_version}")),
184                ),
185                (Some(b), NvidiaAkmods::Disabled) if !b.is_empty() => {
186                    (format!("akmods:{b}-{os_version}"), None, None)
187                }
188                (Some(b), NvidiaAkmods::Open) if !b.is_empty() => (
189                    format!("akmods:{b}-{os_version}"),
190                    None,
191                    Some(format!("akmods-nvidia-open:{b}-{os_version}")),
192                ),
193                (_, NvidiaAkmods::Enabled | NvidiaAkmods::Proprietary) => (
194                    format!("akmods:main-{os_version}"),
195                    None,
196                    Some(format!("akmods-nvidia:main-{os_version}")),
197                ),
198                (_, NvidiaAkmods::Disabled) => (format!("akmods:main-{os_version}"), None, None),
199                (_, NvidiaAkmods::Open) => (
200                    format!("akmods:main-{os_version}"),
201                    None,
202                    Some(format!("akmods-nvidia-open:main-{os_version}")),
203                ),
204            })
205            .stage_name(format!(
206                "{}{}",
207                base.unwrap_or("main"),
208                match nvidia {
209                    NvidiaAkmods::Disabled => String::default(),
210                    _ => "-nvidia".to_string(),
211                }
212            ))
213            .build()
214    }
215}
216
217#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)]
218pub struct Module {
219    #[serde(flatten, skip_serializing_if = "Option::is_none")]
220    pub required_fields: Option<ModuleRequiredFields>,
221
222    #[builder(into)]
223    #[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
224    pub from_file: Option<String>,
225}
226
227impl Module {
228    /// Get's any child modules.
229    ///
230    /// # Errors
231    /// Will error if the module cannot be
232    /// deserialized or the user uses another
233    /// property alongside `from-file:`.
234    pub fn get_modules(
235        modules: &[Self],
236        traversed_files: Option<Vec<PathBuf>>,
237    ) -> Result<Vec<Self>> {
238        let mut found_modules = vec![];
239        let traversed_files = traversed_files.unwrap_or_default();
240
241        for module in modules {
242            found_modules.extend(
243                match &module {
244                    Self {
245                        required_fields: Some(_),
246                        from_file: None,
247                    } => vec![module.clone()],
248                    Self {
249                        required_fields: None,
250                        from_file: Some(file_name),
251                    } => {
252                        let file_name = PathBuf::from(&**file_name);
253                        if traversed_files.contains(&file_name) {
254                            bail!(
255                                "{} File {} has already been parsed:\n{traversed_files:?}",
256                                "Circular dependency detected!".bright_red(),
257                                file_name.display().to_string().bold(),
258                            );
259                        }
260
261                        let mut traversed_files = traversed_files.clone();
262                        traversed_files.push(file_name.clone());
263
264                        Self::get_modules(
265                            &ModuleExt::try_from(&file_name)?.modules,
266                            Some(traversed_files),
267                        )?
268                    }
269                    _ => {
270                        let from_example = Self::builder().from_file("test.yml").build();
271                        let module_example = Self::example();
272
273                        bail!(
274                            "Improper format for module. Must be in the format like:\n{}\n{}\n\n{}",
275                            highlight_ser(&module_example, "yaml", None)?,
276                            "or".bold(),
277                            highlight_ser(&from_example, "yaml", None)?
278                        );
279                    }
280                }
281                .into_iter(),
282            );
283        }
284        Ok(found_modules)
285    }
286
287    #[must_use]
288    pub fn get_from_file_path(&self) -> Option<PathBuf> {
289        self.from_file
290            .as_ref()
291            .map(|path| base_recipe_path().join(&**path))
292    }
293
294    #[must_use]
295    pub fn example() -> Self {
296        Self::builder()
297            .required_fields(
298                ModuleRequiredFields::builder()
299                    .module_type("script")
300                    .config(IndexMap::from_iter([
301                        (
302                            "snippets".to_string(),
303                            Value::Sequence(bon::vec!["echo 'Hello World!'"]),
304                        ),
305                        (
306                            "scripts".to_string(),
307                            Value::Sequence(bon::vec!["install-program.sh"]),
308                        ),
309                    ]))
310                    .build(),
311            )
312            .build()
313    }
314}