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