Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ Cargo.lock
# macOS
.DS_Store
**/.DS_Store

# Build artifacts
*.cpio.gz
vmlinuz*
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ glob = "0.3"
tempfile = "3"
futures = "0.3"
walkdir = "2"
ratatui = "0.30.0"
crossterm = "0.29.0"

[dev-dependencies]
tokio-test = "0.4"
Expand Down
25 changes: 19 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,31 @@ impl InitramfsBuilder {
}
}

let init_dest = rootfs_path.join("init");
if let Some(init_src) = &self.init_script {
let init_dest = rootfs_path.join("init");
info!("Setting init script from {:?}", init_src);
fs::copy(init_src, &init_dest)
.with_context(|| format!("Failed to copy init script from {:?}", init_src))?;

// Make executable
let mut perms = fs::metadata(&init_dest)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&init_dest, perms)?;
} else {
info!("Generating default init script");
let default_init = r#"#!/bin/sh
mount -t proc proc /proc 2>/dev/null
mount -t sysfs sysfs /sys 2>/dev/null
mount -t devtmpfs devtmpfs /dev 2>/dev/null

for cmd in /docker-entrypoint.sh /entrypoint.sh /usr/bin/entrypoint.sh; do
[ -x "$cmd" ] && exec "$cmd"
done

exec /bin/sh
"#;
fs::write(&init_dest, default_init)?;
}

let mut perms = fs::metadata(&init_dest)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&init_dest, perms)?;

info!("Creating CPIO archive from {:?}", rootfs_path);

let archive = CpioArchive::from_directory(&rootfs_path)?;
Expand Down
19 changes: 13 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct Cli {
verbose: bool,
}

mod tui;

#[derive(Subcommand)]
enum Commands {
/// Build an initramfs from a Docker/OCI image
Expand Down Expand Up @@ -89,6 +91,9 @@ enum Commands {
#[arg(long, default_value = "amd64")]
platform_arch: String,
},

/// Interactive mode (TUI)
Interactive,
}

fn setup_logging(verbose: bool) {
Expand Down Expand Up @@ -142,7 +147,6 @@ fn parse_inject(s: &str) -> Result<(PathBuf, PathBuf)> {
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
setup_logging(cli.verbose);

match cli.command {
Commands::Build {
Expand All @@ -157,6 +161,7 @@ async fn main() -> Result<()> {
username,
password_stdin,
} => {
setup_logging(cli.verbose);
let compression: Compression = compression
.parse()
.map_err(|e: String| anyhow::anyhow!(e))?;
Expand Down Expand Up @@ -235,6 +240,7 @@ async fn main() -> Result<()> {
platform_os,
platform_arch,
} => {
setup_logging(cli.verbose);
let client = RegistryClient::new(RegistryAuth::Anonymous);
let reference = RegistryClient::parse_reference(&image)?;
let options = initramfs_builder::PullOptions {
Expand All @@ -255,6 +261,7 @@ async fn main() -> Result<()> {
platform_os,
platform_arch,
} => {
setup_logging(cli.verbose);
let client = RegistryClient::new(RegistryAuth::Anonymous);
let reference = RegistryClient::parse_reference(&image)?;
let options = initramfs_builder::PullOptions {
Expand All @@ -275,11 +282,11 @@ async fn main() -> Result<()> {
);
}
println!();
println!(
"Total: {} layers, {}",
manifest.layers.len(),
format_size(manifest.total_size)
);
println!("{}", format_size(manifest.total_size));
}

Commands::Interactive => {
tui::run().await?;
}
}

Expand Down
213 changes: 213 additions & 0 deletions src/tui/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use crate::tui::screens::{ImageScreen, LanguageScreen};
use anyhow::Result;
use initramfs_builder::{BuildResult, Compression, InitramfsBuilder, RegistryAuth};
use tokio::sync::mpsc::{self, error::TryRecvError};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Screen {
Language,
Image,
Summary,
Build,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WizardMode {
#[default]
Quick,
}

#[derive(Debug, Clone)]
pub struct BuildConfig {
pub image: String,
pub arch: String,
pub compression: Compression,
pub output: String,
}

impl Default for BuildConfig {
fn default() -> Self {
Self {
image: String::new(),
arch: "amd64".to_string(),
compression: Compression::Gzip,
output: "initramfs.cpio.gz".to_string(),
}
}
}

pub struct App {
pub screen: Screen,
pub config: BuildConfig,
#[allow(dead_code)]
pub mode: WizardMode,
pub should_quit: bool,
pub language_screen: LanguageScreen,
pub image_screen: ImageScreen,
pub build_progress: Option<String>,
pub build_error: Option<String>,
pub validation_error: Option<String>,
pub loading_frame: usize,
pub build_success: bool,
pub build_receiver: Option<mpsc::Receiver<Result<BuildResult>>>,
}

impl App {
pub fn new() -> Self {
Self {
screen: Screen::Language,
config: BuildConfig::default(),
mode: WizardMode::Quick,
should_quit: false,
language_screen: LanguageScreen::new(),
image_screen: ImageScreen::new(),
build_progress: None,
build_error: None,
validation_error: None,
loading_frame: 0,
build_success: false,
build_receiver: None,
}
}

pub fn next_screen(&mut self) {
self.validation_error = None;
self.sync_screen_on_exit();

self.screen = match self.screen {
Screen::Language => {
self.update_image_from_language();
Screen::Image
}
Screen::Image => Screen::Summary,
Screen::Summary => Screen::Build,
Screen::Build => Screen::Build,
};

self.sync_screen_on_enter();
}

pub fn prev_screen(&mut self) {
self.validation_error = None;
self.screen = match self.screen {
Screen::Language => Screen::Language,
Screen::Image => Screen::Language,
Screen::Summary => Screen::Image,
Screen::Build => Screen::Summary,
};
self.sync_screen_on_enter();
}

pub fn sync_screen_on_enter(&mut self) {
if let Screen::Image = self.screen {
self.image_screen.sync_from_config(&self.config.image);
}
}

pub fn sync_screen_on_exit(&mut self) {
if let Screen::Image = self.screen {
self.config.image = self.image_screen.sync_to_config();
}
}

fn update_image_from_language(&mut self) {
let preset = &self.language_screen.presets[self.language_screen.selected];
if !preset.versions.is_empty() {
let version_idx = self
.language_screen
.version_selected
.min(preset.versions.len() - 1);
self.config.image = preset.versions[version_idx].1.to_string();
}
}

pub fn validate_current_screen(&mut self) -> bool {
self.validation_error = None;
match self.screen {
Screen::Image => {
let image = self.image_screen.input.trim();
if image.is_empty() {
self.validation_error = Some("Image cannot be empty".to_string());
return false;
}
}
Screen::Summary => {
if self.config.image.trim().is_empty() {
self.validation_error = Some("Image is required".to_string());
return false;
}
}
_ => {}
}
true
}

pub fn start_build(&mut self) {
self.screen = Screen::Build;
self.build_progress = Some("Building initramfs...".to_string());
self.loading_frame = 0;

let image = self.config.image.clone();
let arch = self.config.arch.clone();
let compression = self.config.compression;
let output = self.config.output.clone();
let (tx, rx) = mpsc::channel::<Result<BuildResult>>(1);

self.build_receiver = Some(rx);

tokio::spawn(async move {
let builder = InitramfsBuilder::new()
.image(&image)
.compression(compression)
.platform("linux", &arch)
.auth(RegistryAuth::Anonymous);

let result = builder.build(&output).await;
let _ = tx.send(result).await;
});
}

pub fn check_build_status(&mut self) {
if let Some(rx) = &mut self.build_receiver {
let rx: &mut mpsc::Receiver<Result<BuildResult>> = rx;
match rx.try_recv() {
Ok(result) => {
self.build_receiver = None;
match result {
Ok(res) => {
self.build_progress = Some(format!(
"Success! Output: {} ({} entries, {:.2} MB)",
self.config.output,
res.entries,
res.compressed_size as f64 / 1_048_576.0
));
self.build_success = true;
}
Err(e) => {
self.build_error = Some(format!("Build failed: {}", e));
self.build_success = false;
}
}
}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
self.build_receiver = None;
self.build_error =
Some("Build task panicked or disconnected unexpectedly".to_string());
self.build_success = false;
}
}
}
}

pub fn on_tick(&mut self) {
self.loading_frame = self.loading_frame.wrapping_add(1);
}

pub fn generate_cli_command(&self) -> String {
format!(
"initramfs-builder build {} \\\n --platform-arch {} \\\n -c {} \\\n -o {}",
self.config.image, self.config.arch, self.config.compression, self.config.output
)
}
}
Loading