use std::env; use std::io::Write; extern crate clap; use clap::{Command, arg, command}; use pkh::context::ContextConfig; extern crate flate2; use pkh::changelog::generate_entry; use indicatif_log_bridge::LogWrapper; use log::{error, info}; mod ui; fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); let logger = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .format_timestamp(None) .format(|buf, record| writeln!(buf, "{}", record.args())) .build(); let multi = indicatif::MultiProgress::new(); LogWrapper::new(multi.clone(), logger).try_init().unwrap(); let matches = command!() .subcommand_required(true) .disable_version_flag(true) .subcommand( Command::new("pull") .about("Pull a source package from the archive or git") .arg( arg!(-s --series "Target package distribution series").required(false), ) .arg( arg!(-d --dist "Target package distribution (debian, ubuntu)") .required(false), ) .arg(arg!(-v --version "Target package version").required(false)) .arg(arg!(--ppa "Download the package from a specific PPA").required(false)) .arg(arg!( "Target package")), ) .subcommand( Command::new("chlog") .about("Auto-generate changelog entry, editing it, committing it afterwards") .arg(arg!(-s --series "Target distribution series").required(false)) .arg(arg!(--backport "This changelog is for a backport entry").required(false)) .arg(arg!(-v --version "Target version").required(false)), ) .subcommand(Command::new("build").about("Build the source package (into a .dsc)")) .subcommand( Command::new("deb") .about("Build the source package into binary package (.deb)") .arg(arg!(-s --series "Target distribution series").required(false)) .arg(arg!(-a --arch "Target architecture").required(false)) .arg(arg!(--cross "Cross-compile for target architecture (instead of qemu-binfmt)") .long_help("Cross-compile for target architecture (instead of using qemu-binfmt)\nNote that most packages cannot be cross-compiled").required(false)) .arg(arg!(--mode "Change build mode [sbuild, local]").required(false) .long_help("Change build mode [sbuild, local]\nDefault will chose depending on other parameters, don't provide if unsure")), ) .subcommand( Command::new("context") .about("Manage contexts") .subcommand_required(true) .subcommand( Command::new("create") .about("Create a new context") .arg(arg!( "Context name")) .arg(arg!(--type "Context type: ssh (only type supported for now)")) .arg(arg!(--endpoint "Context endpoint (for example: ssh://user@host:port)")) ) .subcommand( Command::new("rm") .about("Remove a context") .arg(arg!( "Context name")) ) .subcommand( Command::new("ls") .about("List contexts") ) .subcommand(Command::new("show").about("Show current context")) .subcommand( Command::new("use") .about("Set current context") .arg(arg!( "Context name")) ) ) .get_matches(); match matches.subcommand() { Some(("pull", sub_matches)) => { let package = sub_matches.get_one::("package").expect("required"); let series = sub_matches.get_one::("series").map(|s| s.as_str()); let dist = sub_matches.get_one::("dist").map(|s| s.as_str()); let version = sub_matches.get_one::("version").map(|s| s.as_str()); let _ppa = sub_matches .get_one::("ppa") .map(|s| s.as_str()) .unwrap_or(""); let (pb, progress_callback) = ui::create_progress_bar(&multi); // Since pull is async, we need to block on it if let Err(e) = rt.block_on(async { let package_info = pkh::package_info::lookup( package, version, series, "", dist, Some(&progress_callback), ) .await?; pkh::pull::pull(&package_info, None, Some(&progress_callback), false).await }) { pb.finish_and_clear(); error!("{}", e); std::process::exit(1); } pb.finish_and_clear(); multi.remove(&pb); info!("Done."); } Some(("chlog", sub_matches)) => { let cwd = std::env::current_dir().unwrap(); let version = sub_matches.get_one::("version").map(|s| s.as_str()); if let Err(e) = generate_entry("debian/changelog", Some(&cwd), version) { error!("{}", e); std::process::exit(1); } let editor = std::env::var("EDITOR").unwrap(); let _status = std::process::Command::new(editor) .current_dir(&cwd) .args(["debian/changelog"]) .status(); } Some(("build", _sub_matches)) => { let cwd = std::env::current_dir().unwrap(); if let Err(e) = pkh::build::build_source_package(Some(&cwd)) { error!("{}", e); std::process::exit(1); } } Some(("deb", sub_matches)) => { let cwd = std::env::current_dir().unwrap(); let series = sub_matches.get_one::("series").map(|s| s.as_str()); let arch = sub_matches.get_one::("arch").map(|s| s.as_str()); let cross = sub_matches.get_one::("cross").unwrap_or(&false); let mode: Option<&str> = sub_matches.get_one::("mode").map(|s| s.as_str()); let mode: Option = match mode { Some("sbuild") => Some(pkh::deb::BuildMode::Sbuild), Some("local") => Some(pkh::deb::BuildMode::Local), _ => None, }; if let Err(e) = pkh::deb::build_binary_package(arch, series, Some(cwd.as_path()), *cross, mode) { error!("{}", e); std::process::exit(1); } } Some(("context", sub_matches)) => { let mgr = pkh::context::manager(); match sub_matches.subcommand() { Some(("create", args)) => { let name = args.get_one::("name").unwrap(); let type_str = args .get_one::("type") .map(|s| s.as_str()) .unwrap_or("local"); let context = match type_str { "local" => ContextConfig::Local, "ssh" => { let endpoint = args .get_one::("endpoint") .expect("Endpoint is required for ssh context"); // Parse host, user, port from endpoint // Formats: [ssh://][user@]host[:port] let endpoint_re = regex::Regex::new(r"^(?:ssh://)?(?:(?P[^@]+)@)?(?P[^:/]+)(?::(?P\d+))?$").unwrap(); let endpoint_cap = endpoint_re.captures(endpoint).unwrap_or_else(|| { error!("Invalid endpoint format: '{}'. Expected [ssh://][user@]host[:port]", endpoint); std::process::exit(1); }); let host = endpoint_cap.name("host").unwrap().as_str().to_string(); let user = endpoint_cap.name("user").map(|m| m.as_str().to_string()); let port = endpoint_cap.name("port").map(|m| { m.as_str().parse::().unwrap_or_else(|_| { error!("Invalid port number"); std::process::exit(1); }) }); ContextConfig::Ssh { host, user, port } } _ => { error!("Unknown context type: {}", type_str); std::process::exit(1); } }; if let Err(e) = mgr.add_context(name, context) { error!("Failed to create context: {}", e); std::process::exit(1); } info!("Context '{}' created.", name); } Some(("rm", args)) => { let name = args.get_one::("name").unwrap(); if let Err(e) = mgr.remove_context(name) { error!("Failed to remove context: {}", e); std::process::exit(1); } info!("Context '{}' removed.", name); } Some(("ls", _)) => { let contexts = mgr.list_contexts(); let current = mgr.current_name(); for ctx in contexts { if ctx == current { println!("* {}", ctx); } else { println!(" {}", ctx); } } } Some(("show", _)) => {} Some(("use", args)) => { let name = args.get_one::("name").unwrap(); if let Err(e) = mgr.set_current(name) { error!("Failed to set context: {}", e); std::process::exit(1); } info!("Switched to context '{}'.", name); } _ => unreachable!(), } } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), } }