use anyhow::Result;
use clap::{Parser, Subcommand};
use indicatif::{ProgressBar, ProgressStyle};
use initramfs_builder::{Compression, InitramfsBuilder, RegistryAuth, RegistryClient};
use std::io::{self, BufRead};
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "initramfs-builder")]
#[command(author, version, about = "Convert Docker/OCI images to bootable initramfs", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true)]
verbose: bool,
}
#[derive(Subcommand)]
enum Commands {
Build {
image: String,
#[arg(short, long, default_value = "initramfs.cpio.gz")]
output: String,
#[arg(short, long, default_value = "gzip")]
compression: String,
#[arg(long)]
exclude: Vec<String>,
#[arg(long, value_name = "SRC:DEST")]
inject: Vec<String>,
#[arg(long, value_name = "PATH")]
init: Option<PathBuf>,
#[arg(long, default_value = "linux")]
platform_os: String,
#[arg(long, default_value = "amd64")]
platform_arch: String,
#[arg(long)]
username: Option<String>,
#[arg(long)]
password_stdin: bool,
},
Inspect {
image: String,
#[arg(long, default_value = "linux")]
platform_os: String,
#[arg(long, default_value = "amd64")]
platform_arch: String,
},
ListLayers {
image: String,
#[arg(long, default_value = "linux")]
platform_os: String,
#[arg(long, default_value = "amd64")]
platform_arch: String,
},
}
fn setup_logging(verbose: bool) {
let filter = if verbose {
EnvFilter::new("debug")
} else {
EnvFilter::new("info")
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.init();
}
fn read_password_stdin() -> Result<String> {
let stdin = io::stdin();
let mut line = String::new();
stdin.lock().read_line(&mut line)?;
Ok(line.trim().to_string())
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}
fn parse_inject(s: &str) -> Result<(PathBuf, PathBuf)> {
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() != 2 {
anyhow::bail!(
"Invalid inject format '{}'. Expected format: /path/on/host:/path/in/initramfs",
s
);
}
Ok((PathBuf::from(parts[0]), PathBuf::from(parts[1])))
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
setup_logging(cli.verbose);
match cli.command {
Commands::Build {
image,
output,
compression,
exclude,
inject,
init,
platform_os,
platform_arch,
username,
password_stdin,
} => {
let compression: Compression = compression
.parse()
.map_err(|e: String| anyhow::anyhow!(e))?;
let auth = match (username, password_stdin) {
(Some(user), true) => {
let password = read_password_stdin()?;
RegistryAuth::Basic {
username: user,
password,
}
}
(Some(user), false) => {
eprintln!("Warning: username provided without password");
RegistryAuth::Basic {
username: user,
password: String::new(),
}
}
_ => RegistryAuth::Anonymous,
};
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
pb.set_message(format!("Building initramfs from {}...", image));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
let exclude_refs: Vec<&str> = exclude.iter().map(|s| s.as_str()).collect();
let mut builder = InitramfsBuilder::new()
.image(&image)
.compression(compression)
.platform(&platform_os, &platform_arch)
.auth(auth);
for pattern in &exclude_refs {
builder = builder.exclude(&[*pattern]);
}
for inject_arg in &inject {
let (src, dest) = parse_inject(inject_arg)?;
builder = builder.inject(src, dest);
}
if let Some(init_path) = init {
builder = builder.init_script(init_path);
}
let result = builder.build(&output).await?;
pb.finish_and_clear();
println!("Successfully built initramfs:");
println!(" Output: {}", output);
println!(" Entries: {}", result.entries);
println!(" Uncompressed: {}", format_size(result.uncompressed_size));
println!(" Compressed: {}", format_size(result.compressed_size));
println!(
" Ratio: {:.1}%",
(result.compressed_size as f64 / result.uncompressed_size as f64) * 100.0
);
if result.injected_files > 0 {
println!(" Injected files: {}", result.injected_files);
}
if result.has_custom_init {
println!(" Custom init: yes");
}
}
Commands::Inspect {
image,
platform_os,
platform_arch,
} => {
let client = RegistryClient::new(RegistryAuth::Anonymous);
let reference = RegistryClient::parse_reference(&image)?;
let options = initramfs_builder::PullOptions {
platform_os,
platform_arch,
};
let manifest = client.fetch_manifest(&reference, &options).await?;
println!("Image: {}", image);
println!("Config digest: {}", manifest.config_digest);
println!("Layers: {}", manifest.layers.len());
println!("Total size: {}", format_size(manifest.total_size));
}
Commands::ListLayers {
image,
platform_os,
platform_arch,
} => {
let client = RegistryClient::new(RegistryAuth::Anonymous);
let reference = RegistryClient::parse_reference(&image)?;
let options = initramfs_builder::PullOptions {
platform_os,
platform_arch,
};
let manifest = client.fetch_manifest(&reference, &options).await?;
println!("Layers for {}:", image);
println!();
for (idx, layer) in manifest.layers.iter().enumerate() {
println!(
" {}. {} ({})",
idx + 1,
&layer.digest[7..19],
format_size(layer.size)
);
}
println!();
println!(
"Total: {} layers, {}",
manifest.layers.len(),
format_size(manifest.total_size)
);
}
}
Ok(())
}