blue_build/commands/
generate.rs1use std::{
2 env,
3 ops::Not,
4 path::{Path, PathBuf},
5};
6
7use blue_build_process_management::drivers::{
8 CiDriver, Driver, DriverArgs, InspectDriver, opts::GetMetadataOpts, types::Platform,
9};
10use blue_build_recipe::Recipe;
11use blue_build_template::{ContainerFileTemplate, Template};
12use blue_build_utils::{
13 constants::{
14 BB_SKIP_VALIDATION, BUILD_SCRIPTS_IMAGE_REF, CONFIG_PATH, RECIPE_FILE, RECIPE_PATH,
15 },
16 syntax_highlighting::{self, DefaultThemes},
17};
18use bon::Builder;
19use cached::proc_macro::cached;
20use clap::{Args, crate_version};
21use log::{debug, info, trace, warn};
22use miette::{IntoDiagnostic, Result};
23use oci_distribution::Reference;
24
25use crate::{commands::validate::ValidateCommand, shadow};
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, default_value = "native")]
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
146 let template = ContainerFileTemplate::builder()
147 .os_version(
148 Driver::get_os_version()
149 .oci_ref(&recipe.base_image_ref()?)
150 .maybe_platform(self.platform)
151 .call()?,
152 )
153 .build_id(Driver::get_build_id())
154 .recipe(&recipe)
155 .recipe_path(recipe_path.as_path())
156 .registry(registry)
157 .repo(Driver::get_repo_url()?)
158 .build_scripts_image(determine_scripts_tag(self.platform)?.to_string())
159 .base_digest(
160 Driver::get_metadata(
161 &GetMetadataOpts::builder()
162 .image(&base_image)
163 .maybe_platform(self.platform)
164 .build(),
165 )?
166 .digest,
167 )
168 .maybe_nushell_version(recipe.nushell_version.as_ref())
169 .build();
170
171 let output_str = template.render().into_diagnostic()?;
172 if let Some(output) = self.output.as_ref() {
173 debug!("Templating to file {}", output.display());
174 trace!("Containerfile:\n{output_str}");
175
176 std::fs::write(output, output_str).into_diagnostic()?;
177 } else {
178 debug!("Templating to stdout");
179 syntax_highlighting::print(&output_str, "Dockerfile", self.syntax_theme)?;
180 }
181
182 Ok(())
183 }
184}
185
186#[cached(
187 result = true,
188 key = "Option<Platform>",
189 convert = r#"{ platform }"#,
190 sync_writes = "by_key"
191)]
192fn determine_scripts_tag(platform: Option<Platform>) -> Result<Reference> {
193 trace!("determine_scripts_tag({platform:?})");
194
195 let opts = GetMetadataOpts::builder().maybe_platform(platform);
196 format!("{BUILD_SCRIPTS_IMAGE_REF}:{}", shadow::COMMIT_HASH)
197 .parse()
198 .into_diagnostic()
199 .and_then(|image| {
200 Driver::get_metadata(&opts.clone().image(&image).build())
201 .inspect_err(|e| trace!("{e:?}"))
202 .map(|_| image)
203 })
204 .or_else(|_| {
205 let image: Reference = format!("{BUILD_SCRIPTS_IMAGE_REF}:{}", shadow::BRANCH)
206 .parse()
207 .into_diagnostic()?;
208 Driver::get_metadata(&opts.clone().image(&image).build())
209 .inspect_err(|e| trace!("{e:?}"))
210 .map(|_| image)
211 })
212 .or_else(|_| {
213 let image: Reference = format!("{BUILD_SCRIPTS_IMAGE_REF}:v{}", crate_version!())
214 .parse()
215 .into_diagnostic()?;
216 Driver::get_metadata(&opts.image(&image).build())
217 .inspect_err(|e| trace!("{e:?}"))
218 .map(|_| image)
219 })
220 .inspect(|image| debug!("Using build scripts image: {image}"))
221}