use clap::{Parser, Subcommand}; use fds_common::Result; use std::{path::PathBuf, process::ExitCode}; #[derive(Parser)] #[command( version, about = "Build software bundles and metadata-first cartridge images on Linux" )] struct Cli { /// Optional wrapper that accepts TOOL followed by its arguments. #[arg(long, global = true)] image_tool_runner: Option, #[command(subcommand)] command: Action, } #[derive(Subcommand)] enum Action { /// Check xz, erofs-utils and the unprivileged filesystem inspection sandbox. Doctor, /// Build or package software on the workstation, never on the Pi. Software { #[command(subcommand)] command: Software, }, /// Construct and verify a complete 1+m GPT cartridge image before burning. Create { recipe: PathBuf, output: PathBuf }, /// Verify GPT, every filesystem, catalogue and xz tarball in a prepared image. Inspect { image: PathBuf }, /// Verify a prepared image and save an image/target-bound write preview. Preview { image: PathBuf, target: PathBuf, output: PathBuf, /// Use an existing disposable regular file instead of a physical USB drive. #[arg(long)] file_target: bool, }, /// Write the entire prepared image only after exact preview confirmation. Write { preview: PathBuf, #[arg(long)] confirm: String, }, } #[derive(Subcommand)] enum Software { /// Execute an explicit trusted workstation build recipe, then package its output. Build { recipe: PathBuf, output: PathBuf }, /// Package an existing software tree without running its build command. Pack { recipe: PathBuf, output: PathBuf }, /// Check a built software descriptor, archive digest and extracted contents. Inspect { directory: PathBuf }, } fn run() -> Result<()> { let cli = Cli::parse(); let result = match cli.command { Action::Doctor => Ok(fds_workstation::doctor::cartridge( cli.image_tool_runner.as_deref(), )?), Action::Preview { image, target, output, file_target, } => serde_json::to_value(fds_workstation::writing::preview( &image, &target, &output, file_target, cli.image_tool_runner.as_deref(), )?), Action::Write { preview, confirm } => { fds_workstation::writing::write(&preview, &confirm)?; return Ok(()); } Action::Software { command } => serde_json::to_value(match command { Software::Build { recipe, output } => { fds_workstation::software::build(&recipe, &output, true)? } Software::Pack { recipe, output } => { fds_workstation::software::build(&recipe, &output, false)? } Software::Inspect { directory } => fds_workstation::software::load(&directory)?, }), Action::Create { recipe, output } => serde_json::to_value( fds_workstation::cartridge::create(&recipe, &output, cli.image_tool_runner.as_deref())?, ), Action::Inspect { image } => serde_json::to_value(fds_workstation::cartridge::inspect( &image, cli.image_tool_runner.as_deref(), )?), } .map_err(|e| fds_common::Error(e.to_string()))?; println!("{}", serde_json::to_string_pretty(&result).unwrap()); Ok(()) } fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("fds-cartridge: {e}"); ExitCode::from(2) } } } #[cfg(test)] mod tests { use super::*; use clap::CommandFactory; #[test] fn typed_grammar() { Cli::command().debug_assert(); assert!( Cli::try_parse_from([ "fds-cartridge", "software", "build", "recipe.toml", "bundle" ]) .is_ok() ); assert!( Cli::try_parse_from([ "fds-cartridge", "create", "recipe.toml", "card.img", "--force" ]) .is_err() ); assert!(Cli::try_parse_from(["fds-cartridge", "create", "recipe.toml"]).is_err()); } }