//! The `sausage` binary: subcommands only, each delegates to its module. use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] #[command(version, about)] struct Cli { /// Shared host-user compile cache (otherwise SAUSAGE_CACHE or the XDG cache folder). #[arg(long, global = true)] cache: Option, #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { /// Create a world with default tools and /etc/sausage/config.yaml. Init { world: PathBuf, #[arg(long)] userland: Option, }, /// Start a world: the harness, its services and a console. Run { world: PathBuf, /// Run the native Rust builder without namespace isolation. #[arg(long)] no_builder_sandbox: bool, }, /// Run a shell in a world. With `-c`, run one command and exit with its status. Sh { world: PathBuf, #[arg(short = 'c')] command: Option, #[arg(long)] userland: Option, }, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); if let Some(cache) = cli.cache { sausage::process::configure_cache(cache)?; } match cli.command { Command::Init { world, userland } => { let bundle = userland .or_else(|| std::env::var_os("SAUSAGE_USERLAND").map(PathBuf::from)) .ok_or_else(|| { anyhow::anyhow!( "Copy tools failed: default tools unavailable. Set SAUSAGE_USERLAND or pass --userland." ) })?; use std::io::{IsTerminal, Write}; let terminal = std::io::stderr().is_terminal(); let mut progress_line = false; let initialized = sausage::world::World::init_with_progress(&world, &bundle, |line| { if terminal && line.starts_with("Precompile tools ") { eprint!("\r\x1b[2K{line}"); let _ = std::io::stderr().flush(); progress_line = true; } else { if progress_line { eprintln!(); progress_line = false; } eprintln!("{line}"); } }) .await; if progress_line { eprintln!(); } initialized?; eprintln!("Next steps:"); eprintln!( " Edit {} to configure providers and API keys.", world.join("world/etc/sausage/config.yaml").display() ); eprintln!( " sausage run '{}'", world.to_string_lossy().replace('\'', "'\\''") ); Ok(()) } Command::Run { world, no_builder_sandbox, } => { anyhow::ensure!( world.join("world").is_dir(), "No world at {}. Run sausage init {} first.", world.display(), world.display() ); sausage::api::run_with_userland( &world, None, sausage::process::RuntimeOptions { no_builder_sandbox }, ) .await } Command::Sh { world, command, userland, } => { use sausage::exec::{Command, Exec, Options, Stream}; let bundle = userland.or_else(|| std::env::var_os("SAUSAGE_USERLAND").map(PathBuf::from)); let world = sausage::world::World::open_with_userland(&world, bundle.as_deref())?; let runtime = sausage::process::Runtime::new(world)?; let script = command.ok_or_else(|| anyhow::anyhow!("interactive shell is not implemented"))?; let status = runtime .exec( Command::Shell(script), Options { cwd: "/".into(), ..Default::default() }, &mut |stream, bytes| { use std::io::Write; match stream { Stream::Stdout => { let _ = std::io::stdout().write_all(bytes); } Stream::Stderr => { let _ = std::io::stderr().write_all(bytes); } } }, ) .await?; std::process::exit(status); } } }