From 7360367ade55c72e41fb9bf17b0128632b136605 Mon Sep 17 00:00:00 2001 From: Gerald Pinder Date: Sat, 25 Jul 2026 13:43:57 -0400 Subject: [PATCH] feat(iso): Create new ISO generator flow using Titanoboa format --- Cargo.lock | 20 +-- Cargo.toml | 4 +- src/commands.rs | 6 + src/commands/generate_iso.rs | 287 +----------------------------- src/commands/generate_iso_old.rs | 290 +++++++++++++++++++++++++++++++ 5 files changed, 308 insertions(+), 299 deletions(-) create mode 100644 src/commands/generate_iso_old.rs diff --git a/Cargo.lock b/Cargo.lock index 9aa8e493..b8a6b67b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,7 +479,7 @@ dependencies = [ "colored", "comlexr", "fuzzy-matcher", - "git2 0.21.0", + "git2", "indexmap 2.14.0", "jsonschema", "log", @@ -1933,9 +1933,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git2" -version = "0.20.4" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" dependencies = [ "bitflags 2.13.0", "libc", @@ -1944,18 +1944,6 @@ dependencies = [ "url", ] -[[package]] -name = "git2" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" -dependencies = [ - "bitflags 2.13.0", - "libc", - "libgit2-sys", - "log", -] - [[package]] name = "glob" version = "0.3.3" @@ -5017,7 +5005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd39b4b2077bd36e60ca28c31d494046e747759cb9b507a7d177bb64787c39e" dependencies = [ "const_format", - "git2 0.20.4", + "git2", "is_debug", "jiff", ] diff --git a/Cargo.toml b/Cargo.toml index 520ee97d..cd6f6300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,10 +115,12 @@ default = [] v1_0_0 = [ "bootc", - "recipe-v2" + "recipe-v2", + "titanoboa-iso" ] bootc = ["blue-build-process-management/bootc"] vendored-libgit2 = ["dep:git2", "git2/vendored-libgit2"] +titanoboa-iso = ["recipe-v2"] recipe-v2 = ["blue-build-recipe/recipe-v2"] [dev-dependencies] diff --git a/src/commands.rs b/src/commands.rs index 05ee59d6..68047c0a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -12,7 +12,10 @@ pub mod bug_report; pub mod build; pub mod completions; pub mod generate; +#[cfg(feature = "titanoboa-iso")] pub mod generate_iso; +#[cfg(not(feature = "titanoboa-iso"))] +pub mod generate_iso_old; pub mod init; pub mod login; pub mod prune; @@ -21,6 +24,9 @@ pub mod recipe; pub mod switch; pub mod validate; +#[cfg(not(feature = "titanoboa-iso"))] +pub use generate_iso_old as generate_iso; + pub trait BlueBuildCommand { /// Runs the command and returns a result /// of the execution diff --git a/src/commands/generate_iso.rs b/src/commands/generate_iso.rs index 89934f74..465878df 100644 --- a/src/commands/generate_iso.rs +++ b/src/commands/generate_iso.rs @@ -1,290 +1,13 @@ -use std::{ - env, fs, - path::{self, Path, PathBuf}, -}; - -use blue_build_recipe::{Recipe, RecipeGetters}; -use blue_build_utils::{ - constants::{ - ARCHIVE_SUFFIX, BB_GENISO_ENROLLMENT_PASSWORD, BB_GENISO_ISO_NAME, - BB_GENISO_SECURE_BOOT_URL, BB_GENISO_WEB_UI, BB_SKIP_VALIDATION, BB_TEMPDIR, - JASONN3_INSTALLER_IMAGE, - }, - platform::Platform, - string_vec, tempdir, tempdir_in, -}; use bon::Builder; -use clap::{Args, Subcommand, ValueEnum}; -use miette::{Context, IntoDiagnostic, Result, bail}; -use oci_client::Reference; - -use blue_build_process_management::{ - drivers::{Driver, DriverArgs, RunDriver, opts::RunOpts}, - run_volumes, -}; +use clap::Args; -use super::{BlueBuildCommand, build::BuildCommand}; +use crate::commands::BlueBuildCommand; #[derive(Clone, Debug, Builder, Args)] -pub struct GenerateIsoCommand { - #[command(subcommand)] - command: GenIsoSubcommand, - - /// The directory to save the resulting ISO file. - #[arg(short, long)] - #[builder(into)] - output_dir: Option, - - /// The variant of the installer to use. - /// - /// The Kinoite variant will ask for a user - /// and password before installing the OS. - /// This version is the most stable and is - /// recommended. - /// - /// The Silverblue variant will ask for a user - /// and password on first boot after the OS - /// is installed. - /// - /// The Server variant is the basic installer - /// and will ask to setup a user at install time. - #[arg(short = 'V', long, default_value = "kinoite")] - variant: GenIsoVariant, - - /// The url to the secure boot public key. - /// - /// Defaults to one of UBlue's public key. - /// It's recommended to change this if your base - /// image is not from UBlue. - #[arg( - long, - default_value = "https://github.com/ublue-os/bazzite/raw/main/secure_boot.der", - env = BB_GENISO_SECURE_BOOT_URL - )] - #[builder(into)] - secure_boot_url: String, - - /// The enrollment password for the secure boot - /// key. - /// - /// Default's to UBlue's enrollment password. - /// It's recommended to change this if your base - /// image is not from UBlue. - #[arg(long, default_value = "universalblue", env = BB_GENISO_ENROLLMENT_PASSWORD)] - #[builder(into)] - enrollment_password: String, - - /// The name of your ISO image file. - #[arg(long, env = BB_GENISO_ISO_NAME)] - #[builder(into)] - iso_name: Option, - - /// Enable Anaconda WebUI. - #[arg(long, env = BB_GENISO_WEB_UI)] - #[builder(default)] - web_ui: bool, - - /// The location to temporarily store files - /// while building. If unset, it will use `/tmp`. - #[arg(long, env = BB_TEMPDIR)] - tempdir: Option, - - /// The platform of the final ISO. - #[arg(long)] - platform: Option, - - #[clap(flatten)] - #[builder(default)] - drivers: DriverArgs, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum GenIsoSubcommand { - /// Build an ISO from a remote image. - Image { - /// The image ref to create the iso from. - #[arg()] - image: String, - }, - /// Build an ISO from a recipe. - /// - /// This will build the image locally first - /// before creating the ISO. This is a long - /// process. - Recipe { - /// The path to the recipe file for your image. - #[arg()] - recipe: PathBuf, - - /// Skips validation of the recipe file. - #[arg(long, env = BB_SKIP_VALIDATION)] - skip_validation: bool, - }, -} - -#[derive(Debug, Default, Clone, Copy, ValueEnum)] -pub enum GenIsoVariant { - #[default] - Kinoite, - Silverblue, - Server, -} - -impl std::fmt::Display for GenIsoVariant { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match *self { - Self::Server => "Server", - Self::Silverblue => "Silverblue", - Self::Kinoite => "Kinoite", - } - ) - } -} +pub struct GenerateIsoCommand {} impl BlueBuildCommand for GenerateIsoCommand { - fn try_run(&mut self) -> Result<()> { - Driver::init(self.drivers); - - let image_out_dir = if let Some(ref dir) = self.tempdir { - tempdir_in(dir)? - } else { - tempdir()? - }; - - let output_dir = if let Some(output_dir) = self.output_dir.clone() { - if output_dir.exists() && !output_dir.is_dir() { - bail!("The '--output-dir' arg must be a directory"); - } - - if !output_dir.exists() { - fs::create_dir(&output_dir).into_diagnostic()?; - } - - path::absolute(output_dir).into_diagnostic()? - } else { - env::current_dir().into_diagnostic()? - }; - - let platform = self.platform.unwrap_or_default(); - - if let GenIsoSubcommand::Recipe { - recipe, - skip_validation, - } = &self.command - { - BuildCommand::builder() - .recipe(vec![recipe.clone()]) - .archive(image_out_dir.path()) - .maybe_tempdir(self.tempdir.clone()) - .skip_validation(*skip_validation) - .platform(vec![platform]) - .build() - .try_run()?; - } - - let iso_name = self.iso_name.as_ref().map_or("deploy.iso", String::as_str); - let iso_path = output_dir.join(iso_name); - - if iso_path.exists() { - fs::remove_file(iso_path).into_diagnostic()?; - } - - self.build_iso(iso_name, &output_dir, image_out_dir.path(), platform) - } -} - -impl GenerateIsoCommand { - fn build_iso( - &self, - iso_name: &str, - output_dir: &Path, - image_out_dir: &Path, - platform: Platform, - ) -> Result<()> { - let mut args = string_vec![ - format!("VARIANT={}", self.variant), - format!("ISO_NAME=build/{iso_name}"), - "DNF_CACHE=/cache/dnf", - format!("SECURE_BOOT_KEY_URL={}", self.secure_boot_url), - format!("ENROLLMENT_PASSWORD={}", self.enrollment_password), - format!("WEB_UI={}", self.web_ui), - ]; - let image_out_dir = &image_out_dir.display().to_string(); - let output_dir = &output_dir.display().to_string(); - let mut vols = run_volumes![ - output_dir => "/build-container-installer/build", - "dnf-cache" => "/cache/dnf/", - ]; - - match &self.command { - GenIsoSubcommand::Image { image } => { - let image: Reference = image - .parse() - .into_diagnostic() - .with_context(|| format!("Unable to parse image reference {image}"))?; - let (image_repo, image_name) = { - let registry = image.resolve_registry(); - let repo = image.repository(); - let image = format!("{registry}/{repo}"); - - let mut image_parts = image.split('/').collect::>(); - let image_name = image_parts.pop().unwrap(); // Should be at least 2 elements - let image_repo = image_parts.join("/"); - (image_repo, image_name.to_string()) - }; - - args.extend([ - format!("IMAGE_NAME={image_name}"), - format!("IMAGE_REPO={image_repo}"), - format!("IMAGE_TAG={}", image.tag().unwrap_or("latest")), - format!( - "VERSION={}", - Driver::get_os_version().oci_ref(&image).call()? - ), - ]); - } - GenIsoSubcommand::Recipe { - recipe, - skip_validation: _, - } => { - let recipe = Recipe::parse(recipe)?; - - args.extend([ - format!( - "IMAGE_SRC=oci-archive:/img_src/{}.{ARCHIVE_SUFFIX}", - recipe.get_name().replace('/', "_"), - ), - format!( - "VERSION={}", - Driver::get_os_version() - .oci_ref(&recipe.base_image_ref()?) - .call()?, - ), - ]); - vols.extend(&run_volumes![ - image_out_dir => "/img_src/", - ]); - } - } - - // Currently testing local tarball builds - let opts = RunOpts::builder() - .image(JASONN3_INSTALLER_IMAGE) - .privileged(true) - .platform(platform) - .remove(true) - .args(&args) - .volumes(&vols) - .build(); - - let status = Driver::run(opts)?; - - if !status.success() { - bail!("Failed to create ISO"); - } - Ok(()) + fn try_run(&mut self) -> miette::Result<()> { + todo!() } } diff --git a/src/commands/generate_iso_old.rs b/src/commands/generate_iso_old.rs new file mode 100644 index 00000000..89934f74 --- /dev/null +++ b/src/commands/generate_iso_old.rs @@ -0,0 +1,290 @@ +use std::{ + env, fs, + path::{self, Path, PathBuf}, +}; + +use blue_build_recipe::{Recipe, RecipeGetters}; +use blue_build_utils::{ + constants::{ + ARCHIVE_SUFFIX, BB_GENISO_ENROLLMENT_PASSWORD, BB_GENISO_ISO_NAME, + BB_GENISO_SECURE_BOOT_URL, BB_GENISO_WEB_UI, BB_SKIP_VALIDATION, BB_TEMPDIR, + JASONN3_INSTALLER_IMAGE, + }, + platform::Platform, + string_vec, tempdir, tempdir_in, +}; +use bon::Builder; +use clap::{Args, Subcommand, ValueEnum}; +use miette::{Context, IntoDiagnostic, Result, bail}; +use oci_client::Reference; + +use blue_build_process_management::{ + drivers::{Driver, DriverArgs, RunDriver, opts::RunOpts}, + run_volumes, +}; + +use super::{BlueBuildCommand, build::BuildCommand}; + +#[derive(Clone, Debug, Builder, Args)] +pub struct GenerateIsoCommand { + #[command(subcommand)] + command: GenIsoSubcommand, + + /// The directory to save the resulting ISO file. + #[arg(short, long)] + #[builder(into)] + output_dir: Option, + + /// The variant of the installer to use. + /// + /// The Kinoite variant will ask for a user + /// and password before installing the OS. + /// This version is the most stable and is + /// recommended. + /// + /// The Silverblue variant will ask for a user + /// and password on first boot after the OS + /// is installed. + /// + /// The Server variant is the basic installer + /// and will ask to setup a user at install time. + #[arg(short = 'V', long, default_value = "kinoite")] + variant: GenIsoVariant, + + /// The url to the secure boot public key. + /// + /// Defaults to one of UBlue's public key. + /// It's recommended to change this if your base + /// image is not from UBlue. + #[arg( + long, + default_value = "https://github.com/ublue-os/bazzite/raw/main/secure_boot.der", + env = BB_GENISO_SECURE_BOOT_URL + )] + #[builder(into)] + secure_boot_url: String, + + /// The enrollment password for the secure boot + /// key. + /// + /// Default's to UBlue's enrollment password. + /// It's recommended to change this if your base + /// image is not from UBlue. + #[arg(long, default_value = "universalblue", env = BB_GENISO_ENROLLMENT_PASSWORD)] + #[builder(into)] + enrollment_password: String, + + /// The name of your ISO image file. + #[arg(long, env = BB_GENISO_ISO_NAME)] + #[builder(into)] + iso_name: Option, + + /// Enable Anaconda WebUI. + #[arg(long, env = BB_GENISO_WEB_UI)] + #[builder(default)] + web_ui: bool, + + /// The location to temporarily store files + /// while building. If unset, it will use `/tmp`. + #[arg(long, env = BB_TEMPDIR)] + tempdir: Option, + + /// The platform of the final ISO. + #[arg(long)] + platform: Option, + + #[clap(flatten)] + #[builder(default)] + drivers: DriverArgs, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum GenIsoSubcommand { + /// Build an ISO from a remote image. + Image { + /// The image ref to create the iso from. + #[arg()] + image: String, + }, + /// Build an ISO from a recipe. + /// + /// This will build the image locally first + /// before creating the ISO. This is a long + /// process. + Recipe { + /// The path to the recipe file for your image. + #[arg()] + recipe: PathBuf, + + /// Skips validation of the recipe file. + #[arg(long, env = BB_SKIP_VALIDATION)] + skip_validation: bool, + }, +} + +#[derive(Debug, Default, Clone, Copy, ValueEnum)] +pub enum GenIsoVariant { + #[default] + Kinoite, + Silverblue, + Server, +} + +impl std::fmt::Display for GenIsoVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match *self { + Self::Server => "Server", + Self::Silverblue => "Silverblue", + Self::Kinoite => "Kinoite", + } + ) + } +} + +impl BlueBuildCommand for GenerateIsoCommand { + fn try_run(&mut self) -> Result<()> { + Driver::init(self.drivers); + + let image_out_dir = if let Some(ref dir) = self.tempdir { + tempdir_in(dir)? + } else { + tempdir()? + }; + + let output_dir = if let Some(output_dir) = self.output_dir.clone() { + if output_dir.exists() && !output_dir.is_dir() { + bail!("The '--output-dir' arg must be a directory"); + } + + if !output_dir.exists() { + fs::create_dir(&output_dir).into_diagnostic()?; + } + + path::absolute(output_dir).into_diagnostic()? + } else { + env::current_dir().into_diagnostic()? + }; + + let platform = self.platform.unwrap_or_default(); + + if let GenIsoSubcommand::Recipe { + recipe, + skip_validation, + } = &self.command + { + BuildCommand::builder() + .recipe(vec![recipe.clone()]) + .archive(image_out_dir.path()) + .maybe_tempdir(self.tempdir.clone()) + .skip_validation(*skip_validation) + .platform(vec![platform]) + .build() + .try_run()?; + } + + let iso_name = self.iso_name.as_ref().map_or("deploy.iso", String::as_str); + let iso_path = output_dir.join(iso_name); + + if iso_path.exists() { + fs::remove_file(iso_path).into_diagnostic()?; + } + + self.build_iso(iso_name, &output_dir, image_out_dir.path(), platform) + } +} + +impl GenerateIsoCommand { + fn build_iso( + &self, + iso_name: &str, + output_dir: &Path, + image_out_dir: &Path, + platform: Platform, + ) -> Result<()> { + let mut args = string_vec![ + format!("VARIANT={}", self.variant), + format!("ISO_NAME=build/{iso_name}"), + "DNF_CACHE=/cache/dnf", + format!("SECURE_BOOT_KEY_URL={}", self.secure_boot_url), + format!("ENROLLMENT_PASSWORD={}", self.enrollment_password), + format!("WEB_UI={}", self.web_ui), + ]; + let image_out_dir = &image_out_dir.display().to_string(); + let output_dir = &output_dir.display().to_string(); + let mut vols = run_volumes![ + output_dir => "/build-container-installer/build", + "dnf-cache" => "/cache/dnf/", + ]; + + match &self.command { + GenIsoSubcommand::Image { image } => { + let image: Reference = image + .parse() + .into_diagnostic() + .with_context(|| format!("Unable to parse image reference {image}"))?; + let (image_repo, image_name) = { + let registry = image.resolve_registry(); + let repo = image.repository(); + let image = format!("{registry}/{repo}"); + + let mut image_parts = image.split('/').collect::>(); + let image_name = image_parts.pop().unwrap(); // Should be at least 2 elements + let image_repo = image_parts.join("/"); + (image_repo, image_name.to_string()) + }; + + args.extend([ + format!("IMAGE_NAME={image_name}"), + format!("IMAGE_REPO={image_repo}"), + format!("IMAGE_TAG={}", image.tag().unwrap_or("latest")), + format!( + "VERSION={}", + Driver::get_os_version().oci_ref(&image).call()? + ), + ]); + } + GenIsoSubcommand::Recipe { + recipe, + skip_validation: _, + } => { + let recipe = Recipe::parse(recipe)?; + + args.extend([ + format!( + "IMAGE_SRC=oci-archive:/img_src/{}.{ARCHIVE_SUFFIX}", + recipe.get_name().replace('/', "_"), + ), + format!( + "VERSION={}", + Driver::get_os_version() + .oci_ref(&recipe.base_image_ref()?) + .call()?, + ), + ]); + vols.extend(&run_volumes![ + image_out_dir => "/img_src/", + ]); + } + } + + // Currently testing local tarball builds + let opts = RunOpts::builder() + .image(JASONN3_INSTALLER_IMAGE) + .privileged(true) + .platform(platform) + .remove(true) + .args(&args) + .volumes(&vols) + .build(); + + let status = Driver::run(opts)?; + + if !status.success() { + bail!("Failed to create ISO"); + } + Ok(()) + } +}