1use std::{
2 collections::BTreeMap,
3 ops::Not,
4 path::{Path, PathBuf},
5};
6
7use crate::{BuildScripts, DriverTemplate, commands::validate::ValidateCommand};
8use blue_build_process_management::drivers::{
9 CiDriver, Driver, DriverArgs, InspectDriver, opts::GetMetadataOpts,
10};
11use blue_build_recipe::Recipe;
12use blue_build_template::{ContainerFileTemplate, Template};
13use blue_build_utils::{
14 constants::{BB_SKIP_VALIDATION, CONFIG_PATH, RECIPE_FILE, RECIPE_PATH},
15 current_timestamp,
16 platform::Platform,
17 syntax_highlighting::{self, DefaultThemes},
18};
19use bon::Builder;
20use cached::proc_macro::cached;
21use clap::Args;
22use colored::Colorize;
23use log::{debug, info, trace, warn};
24use miette::{Context, IntoDiagnostic, Result};
25use oci_client::Reference;
26
27use super::BlueBuildCommand;
28
29#[derive(Debug, Clone, Args, Builder)]
30pub struct GenerateCommand {
31 #[arg()]
33 #[builder(into)]
34 recipe: Option<PathBuf>,
35
36 #[arg(short, long)]
38 #[builder(into)]
39 output: Option<PathBuf>,
40
41 #[arg(long)]
46 #[builder(into)]
47 registry: Option<String>,
48
49 #[arg(long)]
54 #[builder(into)]
55 registry_namespace: Option<String>,
56
57 #[arg(short, long)]
63 #[builder(default)]
64 display_full_recipe: bool,
65
66 #[arg(short = 't', long)]
71 syntax_theme: Option<DefaultThemes>,
72
73 #[arg(long)]
76 platform: Option<Platform>,
77
78 #[arg(long, env = BB_SKIP_VALIDATION)]
80 #[builder(default)]
81 skip_validation: bool,
82
83 #[clap(flatten)]
84 #[builder(default)]
85 drivers: DriverArgs,
86}
87
88impl BlueBuildCommand for GenerateCommand {
89 fn try_run(&mut self) -> Result<()> {
90 Driver::init(self.drivers);
91
92 self.template_file()
93 }
94}
95
96impl GenerateCommand {
97 fn template_file(&self) -> Result<()> {
98 trace!("TemplateCommand::template_file()");
99
100 let recipe_path = self.recipe.clone().unwrap_or_else(|| {
101 let legacy_path = Path::new(CONFIG_PATH);
102 let recipe_path = Path::new(RECIPE_PATH);
103 if recipe_path.exists() && recipe_path.is_dir() {
104 recipe_path.join(RECIPE_FILE)
105 } else {
106 warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}");
107 legacy_path.join(RECIPE_FILE)
108 }
109 });
110
111 if self.skip_validation.not() {
112 ValidateCommand::builder()
113 .recipe(recipe_path.clone())
114 .build()
115 .try_run()?;
116 }
117
118 let registry = if let (Some(registry), Some(registry_namespace)) =
119 (&self.registry, &self.registry_namespace)
120 {
121 format!("{registry}/{registry_namespace}")
122 } else {
123 Driver::get_registry()?
124 };
125
126 debug!("Deserializing recipe");
127 let recipe = Recipe::parse(&recipe_path)?;
128 trace!("recipe_de: {recipe:#?}");
129
130 if self.display_full_recipe {
131 if let Some(output) = self.output.as_ref() {
132 std::fs::write(output, serde_yaml::to_string(&recipe).into_diagnostic()?)
133 .into_diagnostic()?;
134 } else {
135 syntax_highlighting::print_ser(&recipe, "yml", self.syntax_theme)?;
136 }
137 return Ok(());
138 }
139
140 info!("Templating for recipe at {}", recipe_path.display());
141
142 let base_image: Reference = format!("{}:{}", &recipe.base_image, &recipe.image_version)
143 .parse()
144 .into_diagnostic()
145 .wrap_err_with(|| {
146 format!(
147 "Failed to parse image with base {} and version {}",
148 recipe.base_image.bright_blue(),
149 recipe.image_version.to_string().bright_yellow()
150 )
151 })?;
152 let base_digest =
153 &Driver::get_metadata(GetMetadataOpts::builder().image(&base_image).build())?;
154 let base_digest = base_digest.digest();
155 let build_features = &[
156 #[cfg(feature = "bootc")]
157 "bootc".into(),
158 ];
159 let build_scripts_dir = BuildScripts::extract_mount_dir()?;
160
161 let default_labels = generate_default_labels(&recipe)?;
162 let labels = recipe.generate_labels(&default_labels);
163
164 let template = ContainerFileTemplate::builder()
165 .os_version(
166 Driver::get_os_version()
167 .oci_ref(&recipe.base_image_ref()?)
168 .call()?,
169 )
170 .build_id(Driver::get_build_id())
171 .recipe(&recipe)
172 .recipe_path(recipe_path.as_path())
173 .registry(®istry)
174 .build_scripts_dir(&build_scripts_dir)
175 .base_digest(base_digest)
176 .maybe_nushell_version(recipe.nushell_version.as_ref())
177 .build_features(build_features)
178 .build_engine(Driver::get_build_driver().build_engine())
179 .labels(&labels)
180 .build();
181
182 let output_str = template.render().into_diagnostic().wrap_err_with(|| {
183 format!(
184 "Failed to render Containerfile for {}",
185 recipe_path.display().to_string().cyan()
186 )
187 })?;
188 if let Some(output) = self.output.as_ref() {
189 debug!("Templating to file {}", output.display());
190 trace!("Containerfile:\n{output_str}");
191
192 std::fs::write(output, output_str).into_diagnostic()?;
193 } else {
194 debug!("Templating to stdout");
195 syntax_highlighting::print(&output_str, "Dockerfile", self.syntax_theme)?;
196 }
197
198 Ok(())
199 }
200}
201
202pub fn generate_default_labels(recipe: &Recipe) -> Result<BTreeMap<String, String>> {
223 #[cached(
225 result = true,
226 key = "String",
227 convert = r"{ recipe.name.to_string() }"
228 )]
229 fn inner(recipe: &Recipe) -> Result<BTreeMap<String, String>> {
230 trace!("Generate LABELS for recipe: ({})", recipe.name);
231
232 let build_id = Driver::get_build_id().to_string();
233 let source = Driver::get_repo_url()?;
234 let image_metada = Driver::get_metadata(
235 GetMetadataOpts::builder()
236 .image(&recipe.base_image_ref()?)
237 .build(),
238 )?;
239 let base_digest = image_metada.digest().to_string();
240 let base_name = format!("{}:{}", recipe.base_image, recipe.image_version);
241 let current_timestamp = current_timestamp();
242
243 Ok(BTreeMap::from([
246 (
247 blue_build_utils::constants::BUILD_ID_LABEL.to_string(),
248 build_id,
249 ),
250 (
251 "org.opencontainers.image.title".to_string(),
252 recipe.name.clone(),
253 ),
254 (
255 "org.opencontainers.image.description".to_string(),
256 recipe.description.clone(),
257 ),
258 ("org.opencontainers.image.source".to_string(), source),
259 (
260 "org.opencontainers.image.base.digest".to_string(),
261 base_digest,
262 ),
263 ("org.opencontainers.image.base.name".to_string(), base_name),
264 (
265 "org.opencontainers.image.created".to_string(),
266 current_timestamp,
267 ),
268 ]))
269 }
270 inner(recipe)
271}