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 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}