Files
pkh/src/main.rs
Valentin Haudiquet b3365afe5b
All checks were successful
CI / build (push) Successful in 7m21s
docs: added documentation, enforced documentation
2026-01-01 18:37:40 +01:00

251 lines
11 KiB
Rust

use std::env;
use std::io::Write;
extern crate clap;
use clap::{Command, arg, command};
use pkh::context::ContextConfig;
extern crate flate2;
use pkh::pull::pull;
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 <series> "Target package distribution series").required(false),
)
.arg(
arg!(-d --dist <dist> "Target package distribution (debian, ubuntu)")
.required(false),
)
.arg(arg!(-v --version <version> "Target package version").required(false))
.arg(arg!(--ppa <ppa> "Download the package from a specific PPA").required(false))
.arg(arg!(<package> "Target package")),
)
.subcommand(
Command::new("chlog")
.about("Auto-generate changelog entry, editing it, committing it afterwards")
.arg(arg!(-s --series <series> "Target distribution series").required(false))
.arg(arg!(--backport "This changelog is for a backport entry").required(false))
.arg(arg!(-v --version <version> "Target version").required(false)),
)
.subcommand(Command::new("build").about("Build the source package"))
.subcommand(
Command::new("deb")
.about("Build the binary package")
.arg(arg!(-s --series <series> "Target distribution series").required(false))
.arg(arg!(-a --arch <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 <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!(<name> "Context name"))
.arg(arg!(--type <type> "Context type: ssh (only type supported for now)"))
.arg(arg!(--endpoint <endpoint> "Context endpoint (for example: ssh://user@host:port)"))
)
.subcommand(
Command::new("rm")
.about("Remove a context")
.arg(arg!(<name> "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!(<name> "Context name"))
)
)
.get_matches();
match matches.subcommand() {
Some(("pull", sub_matches)) => {
let package = sub_matches.get_one::<String>("package").expect("required");
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
let dist = sub_matches.get_one::<String>("dist").map(|s| s.as_str());
let version = sub_matches
.get_one::<String>("version")
.map(|s| s.as_str())
.unwrap_or("");
let ppa = sub_matches
.get_one::<String>("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(pull(
package,
version,
series,
"",
ppa,
dist,
None,
Some(&progress_callback),
)) {
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::<String>("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::<String>("series").map(|s| s.as_str());
let arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str());
let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false);
let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str());
let mode: Option<pkh::deb::BuildMode> = 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::<String>("name").unwrap();
let type_str = args
.get_one::<String>("type")
.map(|s| s.as_str())
.unwrap_or("local");
let context = match type_str {
"local" => ContextConfig::Local,
"ssh" => {
let endpoint = args
.get_one::<String>("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<user>[^@]+)@)?(?P<host>[^:/]+)(?::(?P<port>\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::<u16>().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::<String>("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::<String>("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`"),
}
}