1use std::{
2 env, fs,
3 path::{self, Path, PathBuf},
4};
5
6use blue_build_recipe::Recipe;
7use blue_build_utils::{
8 constants::{
9 ARCHIVE_SUFFIX, BB_GENISO_ENROLLMENT_PASSWORD, BB_GENISO_ISO_NAME,
10 BB_GENISO_SECURE_BOOT_URL, BB_GENISO_WEB_UI, BB_SKIP_VALIDATION, BB_TEMPDIR,
11 JASONN3_INSTALLER_IMAGE,
12 },
13 platform::Platform,
14 string_vec,
15};
16use bon::Builder;
17use clap::{Args, Subcommand, ValueEnum};
18use miette::{Context, IntoDiagnostic, Result, bail};
19use oci_client::Reference;
20use tempfile::TempDir;
21
22use blue_build_process_management::{
23 drivers::{Driver, DriverArgs, RunDriver, opts::RunOpts},
24 run_volumes,
25};
26
27use super::{BlueBuildCommand, build::BuildCommand};
28
29#[derive(Clone, Debug, Builder, Args)]
30pub struct GenerateIsoCommand {
31 #[command(subcommand)]
32 command: GenIsoSubcommand,
33
34 #[arg(short, long)]
36 #[builder(into)]
37 output_dir: Option<PathBuf>,
38
39 #[arg(short = 'V', long, default_value = "kinoite")]
53 variant: GenIsoVariant,
54
55 #[arg(
61 long,
62 default_value = "https://github.com/ublue-os/bazzite/raw/main/secure_boot.der",
63 env = BB_GENISO_SECURE_BOOT_URL
64 )]
65 #[builder(into)]
66 secure_boot_url: String,
67
68 #[arg(long, default_value = "universalblue", env = BB_GENISO_ENROLLMENT_PASSWORD)]
75 #[builder(into)]
76 enrollment_password: String,
77
78 #[arg(long, env = BB_GENISO_ISO_NAME)]
80 #[builder(into)]
81 iso_name: Option<String>,
82
83 #[arg(long, env = BB_GENISO_WEB_UI)]
85 #[builder(default)]
86 web_ui: bool,
87
88 #[arg(long, env = BB_TEMPDIR)]
91 tempdir: Option<PathBuf>,
92
93 #[arg(long)]
95 platform: Option<Platform>,
96
97 #[clap(flatten)]
98 #[builder(default)]
99 drivers: DriverArgs,
100}
101
102#[derive(Debug, Clone, Subcommand)]
103pub enum GenIsoSubcommand {
104 Image {
106 #[arg()]
108 image: String,
109 },
110 Recipe {
116 #[arg()]
118 recipe: PathBuf,
119
120 #[arg(long, env = BB_SKIP_VALIDATION)]
122 skip_validation: bool,
123 },
124}
125
126#[derive(Debug, Default, Clone, Copy, ValueEnum)]
127pub enum GenIsoVariant {
128 #[default]
129 Kinoite,
130 Silverblue,
131 Server,
132}
133
134impl std::fmt::Display for GenIsoVariant {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 write!(
137 f,
138 "{}",
139 match *self {
140 Self::Server => "Server",
141 Self::Silverblue => "Silverblue",
142 Self::Kinoite => "Kinoite",
143 }
144 )
145 }
146}
147
148impl BlueBuildCommand for GenerateIsoCommand {
149 fn try_run(&mut self) -> Result<()> {
150 Driver::init(self.drivers);
151
152 let image_out_dir = if let Some(ref dir) = self.tempdir {
153 TempDir::new_in(dir).into_diagnostic()?
154 } else {
155 TempDir::new().into_diagnostic()?
156 };
157
158 let output_dir = if let Some(output_dir) = self.output_dir.clone() {
159 if output_dir.exists() && !output_dir.is_dir() {
160 bail!("The '--output-dir' arg must be a directory");
161 }
162
163 if !output_dir.exists() {
164 fs::create_dir(&output_dir).into_diagnostic()?;
165 }
166
167 path::absolute(output_dir).into_diagnostic()?
168 } else {
169 env::current_dir().into_diagnostic()?
170 };
171
172 let platform = self.platform.unwrap_or_default();
173
174 if let GenIsoSubcommand::Recipe {
175 recipe,
176 skip_validation,
177 } = &self.command
178 {
179 BuildCommand::builder()
180 .recipe(vec![recipe.clone()])
181 .archive(image_out_dir.path())
182 .maybe_tempdir(self.tempdir.clone())
183 .skip_validation(*skip_validation)
184 .platform(vec![platform])
185 .build()
186 .try_run()?;
187 }
188
189 let iso_name = self.iso_name.as_ref().map_or("deploy.iso", String::as_str);
190 let iso_path = output_dir.join(iso_name);
191
192 if iso_path.exists() {
193 fs::remove_file(iso_path).into_diagnostic()?;
194 }
195
196 self.build_iso(iso_name, &output_dir, image_out_dir.path(), platform)
197 }
198}
199
200impl GenerateIsoCommand {
201 fn build_iso(
202 &self,
203 iso_name: &str,
204 output_dir: &Path,
205 image_out_dir: &Path,
206 platform: Platform,
207 ) -> Result<()> {
208 let mut args = string_vec![
209 format!("VARIANT={}", self.variant),
210 format!("ISO_NAME=build/{iso_name}"),
211 "DNF_CACHE=/cache/dnf",
212 format!("SECURE_BOOT_KEY_URL={}", self.secure_boot_url),
213 format!("ENROLLMENT_PASSWORD={}", self.enrollment_password),
214 format!("WEB_UI={}", self.web_ui),
215 ];
216 let image_out_dir = &image_out_dir.display().to_string();
217 let output_dir = &output_dir.display().to_string();
218 let mut vols = run_volumes![
219 output_dir => "/build-container-installer/build",
220 "dnf-cache" => "/cache/dnf/",
221 ];
222
223 match &self.command {
224 GenIsoSubcommand::Image { image } => {
225 let image: Reference = image
226 .parse()
227 .into_diagnostic()
228 .with_context(|| format!("Unable to parse image reference {image}"))?;
229 let (image_repo, image_name) = {
230 let registry = image.resolve_registry();
231 let repo = image.repository();
232 let image = format!("{registry}/{repo}");
233
234 let mut image_parts = image.split('/').collect::<Vec<_>>();
235 let image_name = image_parts.pop().unwrap(); let image_repo = image_parts.join("/");
237 (image_repo, image_name.to_string())
238 };
239
240 args.extend([
241 format!("IMAGE_NAME={image_name}",),
242 format!("IMAGE_REPO={image_repo}"),
243 format!("IMAGE_TAG={}", image.tag().unwrap_or("latest")),
244 format!(
245 "VERSION={}",
246 Driver::get_os_version().oci_ref(&image).call()?
247 ),
248 ]);
249 }
250 GenIsoSubcommand::Recipe {
251 recipe,
252 skip_validation: _,
253 } => {
254 let recipe = Recipe::parse(recipe)?;
255
256 args.extend([
257 format!(
258 "IMAGE_SRC=oci-archive:/img_src/{}.{ARCHIVE_SUFFIX}",
259 recipe.name.replace('/', "_"),
260 ),
261 format!(
262 "VERSION={}",
263 Driver::get_os_version()
264 .oci_ref(&recipe.base_image_ref()?)
265 .call()?,
266 ),
267 ]);
268 vols.extend(&run_volumes![
269 image_out_dir => "/img_src/",
270 ]);
271 }
272 }
273
274 let opts = RunOpts::builder()
276 .image(JASONN3_INSTALLER_IMAGE)
277 .privileged(true)
278 .platform(platform)
279 .remove(true)
280 .args(&args)
281 .volumes(&vols)
282 .build();
283
284 let status = Driver::run(opts)?;
285
286 if !status.success() {
287 bail!("Failed to create ISO");
288 }
289 Ok(())
290 }
291}