731 lines
34 KiB
Rust
731 lines
34 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::changelog::generate_entry;
|
|
|
|
use indicatif_log_bridge::LogWrapper;
|
|
use log::{error, info};
|
|
|
|
/// Obtain the current working directory, exiting with a helpful message on failure.
|
|
fn current_dir_or_exit() -> std::path::PathBuf {
|
|
match std::env::current_dir() {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
error!("Could not determine the current working directory: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
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("new")
|
|
.about("Scaffold a new Debian source package (buildable right away)")
|
|
.arg(arg!([name] "Package name: creates ./<name>/ with a fresh project skeleton. Without it (or with --source), the given/current directory is packaged"))
|
|
// NOTE: hyphenated long names are defined via the builder API
|
|
// because clap's `arg!` macro mis-tokenizes them (see the
|
|
// prune subcommand note below).
|
|
.arg(
|
|
clap::Arg::new("lang")
|
|
.long("lang")
|
|
.value_name("LANG")
|
|
.help("Language/build system: rust, python, meson, cmake, autotools, go, shell, makefile or empty"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("source")
|
|
.long("source")
|
|
.value_name("PATH")
|
|
.conflicts_with("name")
|
|
.help("Package the sources in PATH instead of creating ./<name>/"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("upstream_version")
|
|
.long("upstream-version")
|
|
.value_name("VERSION")
|
|
.help("Upstream version (default: 0.1.0)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("revision")
|
|
.long("revision")
|
|
.value_name("N")
|
|
.value_parser(clap::value_parser!(u32))
|
|
.help("Debian revision (default: 1)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("description")
|
|
.long("description")
|
|
.value_name("DESC")
|
|
.help("One-line package description"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("homepage")
|
|
.long("homepage")
|
|
.value_name("URL")
|
|
.help("Upstream homepage (http:// or https://)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("license")
|
|
.long("license")
|
|
.value_name("SPDX")
|
|
.help("Upstream license (SPDX identifier, e.g. MIT, GPL-3.0+)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("command")
|
|
.long("command")
|
|
.value_name("CMD")
|
|
.help("Installed command name (default: the package name)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("maintainer")
|
|
.long("maintainer")
|
|
.value_name("NAME <EMAIL>")
|
|
.help("Maintainer (default: DEBFULLNAME/DEBEMAIL, then git config)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("depends")
|
|
.long("depends")
|
|
.value_name("LIST")
|
|
.action(clap::ArgAction::Append)
|
|
.long_help("Runtime Depends of the metapackage flavor ('empty' template), as a comma-separated list (e.g. \"hello, hello-data (>= 1.0)\"). Can be specified multiple times.")
|
|
.help("Metapackage Depends list, comma-separated ('empty' template only)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("dist")
|
|
.long("dist")
|
|
.value_name("DIST")
|
|
.help("Target distribution: debian or ubuntu (default: current vendor)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("series")
|
|
.long("series")
|
|
.value_name("SERIES")
|
|
.help("Target series (default: the development series of --dist)"),
|
|
)
|
|
.arg(arg!(--release "Write the --series into debian/changelog instead of UNRELEASED").required(false))
|
|
.arg(arg!(--native "Use the 3.0 (native) source format (no orig tarball)").required(false))
|
|
.arg(
|
|
clap::Arg::new("no_git")
|
|
.long("no-git")
|
|
.action(clap::ArgAction::SetTrue)
|
|
.help("Do not initialize a git repository (.gitignore files are written anyway)"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("no_verify")
|
|
.long("no-verify")
|
|
.action(clap::ArgAction::SetTrue)
|
|
.help("Skip the post-scaffold build verification (the structural self-checks always run)"),
|
|
)
|
|
.arg(arg!(--defaults "Take the default answer for every question left unanswered (the package name is still required)").required(false)),
|
|
)
|
|
.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!(--archive "Only use the archive to download package source, not git").required(false))
|
|
.arg(arg!(--ppa <ppa> "Download the package from a specific PPA (format: user/ppa_name)").required(false))
|
|
.arg(arg!(--repository <url> "Download the package from an external flat repository, given as its full suite URL (e.g. https://pkg.noctalia.dev/deb/resolute/)").required(false)
|
|
.conflicts_with("ppa"))
|
|
.arg(arg!(-p --pocket <pocket> "Target package distribution pocket (updates, security, proposed)").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 (into a .dsc)")
|
|
.arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false)),
|
|
)
|
|
.subcommand(
|
|
Command::new("put")
|
|
.about("Upload the built source package to a PPA")
|
|
.arg(arg!(--ppa <ppa> "Upload to a PPA (format: user/ppa_name)"))
|
|
.arg(arg!([changes] "Explicit .changes file to upload (default: the one built from this package, next to the source tree)").required(false))
|
|
.arg(arg!(--force "Upload even if this exact .changes file was already uploaded to the target")),
|
|
)
|
|
.subcommand(
|
|
Command::new("deb")
|
|
.about("Build the source package into binary package (.deb)")
|
|
.arg(arg!(-s --series <series> "Target distribution series").required(false))
|
|
.arg(arg!(-a --arch <arch> "Target architecture").required(false))
|
|
.arg(arg!(-p --pocket <pocket> "Build against dependencies from a specific distribution pocket (updates, security, proposed)").required(false)
|
|
.long_help("Build against dependencies from a specific distribution pocket (e.g. updates, security, proposed).\nThe '<series>-<pocket>' suite will be enabled on archive sources when resolving build-dependencies."))
|
|
.arg(arg!(--ppa <ppa> "Build the package adding a specific PPA for dependencies (can be specified multiple times)")
|
|
.long_help("Build the package adding a specific PPA for dependencies. Can be specified multiple times.").required(false).action(clap::ArgAction::Append))
|
|
.arg(arg!(--inject <package> "Inject a package into the build environment (can be specified multiple times)")
|
|
.long_help("Inject a package into the build environment before build-dep. Can be a .deb file path, a package name from the archive, or a package from a previously added PPA. Can be specified multiple times.").required(false).action(clap::ArgAction::Append))
|
|
.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 [local]").required(false)
|
|
.long_help("Change build mode [local]\nDefault will chose depending on other parameters, don't provide if unsure"))
|
|
.arg(arg!(-j --jobs <jobs> "Number of parallel build jobs (default: number of CPUs available in the build context)").required(false))
|
|
.arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false)
|
|
.long_help("Show raw tool output instead of the live build view.\nAlso implied by RUST_LOG=debug for pkh's own logs.")),
|
|
)
|
|
.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"))
|
|
)
|
|
)
|
|
.subcommand(
|
|
Command::new("prune")
|
|
.about("Prune residual pkh build artifacts and caches")
|
|
// NOTE: --dry-run is defined via the builder API because clap's
|
|
// `arg!` macro mis-tokenizes hyphenated long names (it would
|
|
// parse `--dry-run` as long="dry" plus a spurious short flag,
|
|
// tripping the "Short flags should precede long flags" assert).
|
|
.arg(
|
|
clap::Arg::new("dry_run")
|
|
.long("dry-run")
|
|
.action(clap::ArgAction::SetTrue)
|
|
.help("List what would be removed without removing anything"),
|
|
)
|
|
.arg(
|
|
clap::Arg::new("all")
|
|
.long("all")
|
|
.action(clap::ArgAction::SetTrue)
|
|
.help("Also remove cached chroot tarballs (expensive to re-download)"),
|
|
)
|
|
)
|
|
.get_matches();
|
|
|
|
match matches.subcommand() {
|
|
Some(("new", sub_matches)) => {
|
|
let depends: Vec<String> = sub_matches
|
|
.get_many::<String>("depends")
|
|
.map(|values| values.cloned().collect())
|
|
.unwrap_or_default();
|
|
let no_verify = sub_matches
|
|
.get_one::<bool>("no_verify")
|
|
.copied()
|
|
.unwrap_or(false);
|
|
let cli = pkh::new::options::NewCli {
|
|
name: sub_matches.get_one::<String>("name").cloned(),
|
|
lang: sub_matches.get_one::<String>("lang").cloned(),
|
|
source: sub_matches
|
|
.get_one::<String>("source")
|
|
.map(std::path::PathBuf::from),
|
|
upstream_version: sub_matches.get_one::<String>("upstream_version").cloned(),
|
|
revision: sub_matches.get_one::<u32>("revision").copied(),
|
|
description: sub_matches.get_one::<String>("description").cloned(),
|
|
homepage: sub_matches.get_one::<String>("homepage").cloned(),
|
|
license: sub_matches.get_one::<String>("license").cloned(),
|
|
command: sub_matches.get_one::<String>("command").cloned(),
|
|
maintainer: sub_matches.get_one::<String>("maintainer").cloned(),
|
|
depends,
|
|
dist: sub_matches.get_one::<String>("dist").cloned(),
|
|
series: sub_matches.get_one::<String>("series").cloned(),
|
|
release: sub_matches
|
|
.get_one::<bool>("release")
|
|
.copied()
|
|
.unwrap_or(false),
|
|
native: sub_matches
|
|
.get_one::<bool>("native")
|
|
.copied()
|
|
.unwrap_or(false),
|
|
git: !sub_matches
|
|
.get_one::<bool>("no_git")
|
|
.copied()
|
|
.unwrap_or(false),
|
|
defaults: sub_matches
|
|
.get_one::<bool>("defaults")
|
|
.copied()
|
|
.unwrap_or(false),
|
|
};
|
|
|
|
// The wizard (interactive terminal) fills the same NewCli and
|
|
// resolves through the same pipeline; without a TTY the resolve
|
|
// error lists every missing answer. Afterwards the two
|
|
// verification builds are offered (`--no-verify` skips them;
|
|
// the structural self-checks inside `scaffold` always run), with
|
|
// the scaffold outcome (e.g. a failed vendoring) shaping the
|
|
// offer.
|
|
if let Err(e) = rt.block_on(async {
|
|
let opts = pkh::new::questions::run(cli).await?;
|
|
let outcome = pkh::new::scaffold(opts.clone(), &multi)?;
|
|
pkh::new::questions::offer_verification(&opts, &outcome, &multi, no_verify).await;
|
|
Ok::<(), Box<dyn std::error::Error>>(())
|
|
}) {
|
|
error!("{}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
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());
|
|
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
|
|
let repository = sub_matches
|
|
.get_one::<String>("repository")
|
|
.map(|s| s.as_str());
|
|
let pocket = sub_matches
|
|
.get_one::<String>("pocket")
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("");
|
|
let archive = sub_matches.get_one::<bool>("archive").unwrap_or(&false);
|
|
|
|
let (pb, progress_callback) = pkh::ui::create_progress_bar(&multi);
|
|
|
|
// Convert PPA to base URL if provided
|
|
let base_url = ppa.map(|ppa_str| {
|
|
// PPA format: user/ppa_name
|
|
let parts: Vec<&str> = ppa_str.split('/').collect();
|
|
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
|
|
error!("Invalid PPA format: '{}'. Expected: user/ppa_name", ppa_str);
|
|
std::process::exit(1);
|
|
}
|
|
pkh::package_info::ppa_to_base_url(parts[0], parts[1])
|
|
});
|
|
|
|
// 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,
|
|
pocket,
|
|
dist,
|
|
base_url.as_deref(),
|
|
repository,
|
|
Some(&progress_callback),
|
|
)
|
|
.await?;
|
|
pkh::pull::pull(&package_info, None, Some(&progress_callback), *archive).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 = current_dir_or_exit();
|
|
let version = sub_matches.get_one::<String>("version").map(|s| s.as_str());
|
|
let cli_series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
|
|
|
|
// Determine target series: CLI flag > interactive selector > current changelog series
|
|
let target_series = if let Some(s) = cli_series {
|
|
Some(s.to_string())
|
|
} else {
|
|
// Parse current changelog to determine the default series
|
|
let changelog_path = cwd.join("debian/changelog");
|
|
match pkh::changelog::parse_changelog_header(&changelog_path) {
|
|
Ok((_pkg, _ver, current_series)) => {
|
|
// UNRELEASED is not a real series: offer it as a
|
|
// pinned first entry (selecting it keeps the changelog
|
|
// unreleased) on top of the current vendor's series
|
|
// list, defaulting to the development series. Any
|
|
// other series resolves through the series list of
|
|
// its own distribution.
|
|
match rt.block_on(async {
|
|
if pkh::distro_info::is_unreleased(¤t_series) {
|
|
// Vendors keep original casing ("Ubuntu"),
|
|
// while the series data keys are lowercase
|
|
let dist = pkh::build::env::current_vendor().to_lowercase();
|
|
let mut series_list =
|
|
vec![pkh::distro_info::UNRELEASED.to_string()];
|
|
series_list.extend(
|
|
pkh::distro_info::get_ordered_series_name(&dist).await?,
|
|
);
|
|
Ok(series_list)
|
|
} else {
|
|
let dist =
|
|
pkh::distro_info::get_dist_from_series(¤t_series).await?;
|
|
pkh::distro_info::get_ordered_series_name(&dist).await
|
|
}
|
|
}) {
|
|
Ok(series_list) => {
|
|
// Default to the development series (the
|
|
// first real entry) when the changelog is
|
|
// UNRELEASED, not to the pinned entry itself
|
|
let default = if pkh::distro_info::is_unreleased(¤t_series)
|
|
&& series_list.len() > 1
|
|
{
|
|
series_list[1].clone()
|
|
} else {
|
|
current_series.clone()
|
|
};
|
|
match pkh::ui::select_series(&series_list, &default) {
|
|
Ok(selected) => Some(selected),
|
|
Err(e) => {
|
|
error!(
|
|
"Series selection failed: {}. Using current series '{}' instead.",
|
|
e, current_series
|
|
);
|
|
Some(current_series)
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
// Could not fetch series list, use current series as default
|
|
Some(current_series)
|
|
}
|
|
}
|
|
}
|
|
Err(_) => None,
|
|
}
|
|
};
|
|
|
|
if let Err(e) = generate_entry(
|
|
"debian/changelog",
|
|
Some(&cwd),
|
|
version,
|
|
target_series.as_deref(),
|
|
) {
|
|
error!("{}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let editor = match std::env::var("EDITOR") {
|
|
Ok(e) => e,
|
|
Err(_) => {
|
|
error!(
|
|
"No editor configured. Set the EDITOR environment variable \
|
|
(e.g. `EDITOR=nano` or `export EDITOR=vim`) and retry."
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
let _status = std::process::Command::new(&editor)
|
|
.current_dir(&cwd)
|
|
.args(["debian/changelog"])
|
|
.status()
|
|
.map_err(|e| {
|
|
error!(
|
|
"Could not launch editor '{}': {}. \
|
|
Make sure it is installed and available on PATH.",
|
|
editor, e
|
|
);
|
|
std::process::exit(1);
|
|
});
|
|
}
|
|
Some(("build", sub_matches)) => {
|
|
let cwd = current_dir_or_exit();
|
|
let verbose = sub_matches
|
|
.get_one::<bool>("verbose")
|
|
.copied()
|
|
.unwrap_or(false);
|
|
|
|
// Live build view: disabled by --verbose or when stdout is not a
|
|
// terminal (DebUi handles the non-TTY case itself)
|
|
let ui = if verbose {
|
|
None
|
|
} else {
|
|
Some(std::sync::Arc::new(pkh::ui::deb::DebUi::new(&multi)))
|
|
};
|
|
|
|
if let Err(e) = pkh::build::build_source_package(Some(&cwd), ui) {
|
|
error!("{}", e);
|
|
// Unmet build dependencies/conflicts exit with status 3,
|
|
// like dpkg-buildpackage does.
|
|
if e.downcast_ref::<pkh::debian::deps::UnmetBuildDependencies>()
|
|
.is_some()
|
|
{
|
|
std::process::exit(3);
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
Some(("put", sub_matches)) => {
|
|
let cwd = current_dir_or_exit();
|
|
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
|
|
let changes = sub_matches
|
|
.get_one::<String>("changes")
|
|
.map(std::path::PathBuf::from);
|
|
let force = sub_matches
|
|
.get_one::<bool>("force")
|
|
.copied()
|
|
.unwrap_or(false);
|
|
|
|
// Only PPA targets are implemented for now
|
|
let Some(ppa) = ppa else {
|
|
error!(
|
|
"pkh put needs a target: pass --ppa user/ppa_name \
|
|
(archive uploads are not supported yet)"
|
|
);
|
|
std::process::exit(1);
|
|
};
|
|
|
|
let options = pkh::put::PutOptions {
|
|
ppa: ppa.to_string(),
|
|
changes,
|
|
force,
|
|
cwd,
|
|
};
|
|
if let Err(e) = rt.block_on(async { pkh::put::put(&options, &multi).await }) {
|
|
error!("{}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
Some(("deb", sub_matches)) => {
|
|
let cwd = current_dir_or_exit();
|
|
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
|
|
let pocket = sub_matches.get_one::<String>("pocket").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 ppa: Vec<&str> = sub_matches
|
|
.get_many::<String>("ppa")
|
|
.map(|v| v.map(|s| s.as_str()).collect())
|
|
.unwrap_or_default();
|
|
let ppa = if ppa.is_empty() {
|
|
None
|
|
} else {
|
|
Some(ppa.as_slice())
|
|
};
|
|
let inject_packages: Vec<&str> = sub_matches
|
|
.get_many::<String>("inject")
|
|
.map(|v| v.map(|s| s.as_str()).collect())
|
|
.unwrap_or_default();
|
|
let inject_packages = if inject_packages.is_empty() {
|
|
None
|
|
} else {
|
|
Some(inject_packages.as_slice())
|
|
};
|
|
let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str());
|
|
let mode: Option<pkh::deb::BuildMode> = match mode {
|
|
Some("local") => Some(pkh::deb::BuildMode::Local),
|
|
_ => None,
|
|
};
|
|
let verbose = sub_matches
|
|
.get_one::<bool>("verbose")
|
|
.copied()
|
|
.unwrap_or(false);
|
|
|
|
let jobs = sub_matches.get_one::<String>("jobs").map(|s| s.as_str());
|
|
let jobs = jobs.map(|j| {
|
|
j.parse::<usize>().unwrap_or_else(|_| {
|
|
error!("Invalid --jobs value '{}': expected a positive integer", j);
|
|
std::process::exit(1);
|
|
})
|
|
});
|
|
|
|
// Live build view: disabled by --verbose or when stdout is not a
|
|
// terminal (DebUi handles the non-TTY case itself)
|
|
let ui = if verbose {
|
|
None
|
|
} else {
|
|
Some(std::sync::Arc::new(pkh::ui::deb::DebUi::new(&multi)))
|
|
};
|
|
|
|
let result = rt.block_on(async {
|
|
pkh::deb::build_binary_package(
|
|
arch,
|
|
series,
|
|
pocket,
|
|
Some(cwd.as_path()),
|
|
*cross,
|
|
mode,
|
|
ppa,
|
|
inject_packages,
|
|
None,
|
|
ui.clone(),
|
|
jobs,
|
|
)
|
|
.await
|
|
});
|
|
|
|
match result {
|
|
Ok(artifacts) => {
|
|
let _ = artifacts;
|
|
info!("Done.");
|
|
}
|
|
Err(e) => {
|
|
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").unwrap_or_else(|| {
|
|
error!(
|
|
"An --endpoint is required to create an ssh context. \
|
|
Expected format: [ssh://][user@]host[:port]"
|
|
);
|
|
std::process::exit(1);
|
|
});
|
|
|
|
// 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!(),
|
|
}
|
|
}
|
|
Some(("prune", sub_matches)) => {
|
|
let dry_run = sub_matches
|
|
.get_one::<bool>("dry_run")
|
|
.copied()
|
|
.unwrap_or(false);
|
|
let all = sub_matches.get_one::<bool>("all").copied().unwrap_or(false);
|
|
|
|
let options = pkh::prune::PruneOptions { dry_run, all };
|
|
match pkh::prune::prune(options) {
|
|
Ok(report) => {
|
|
if report.is_empty() {
|
|
info!("Nothing to prune.");
|
|
} else {
|
|
let action = if report.dry_run {
|
|
"Would remove"
|
|
} else {
|
|
"Removed"
|
|
};
|
|
for path in &report.removed {
|
|
info!("{} {}", action, path.display());
|
|
}
|
|
for (path, err) in &report.failed {
|
|
error!("Failed to remove {}: {}", path.display(), err);
|
|
}
|
|
if report.dry_run {
|
|
info!(
|
|
"(dry run) {} item(s) would be removed.",
|
|
report.removed.len()
|
|
);
|
|
} else {
|
|
info!(
|
|
"Pruned {} item(s){} ({} failure(s)).",
|
|
report.removed.len(),
|
|
if all {
|
|
" including cached chroot tarballs"
|
|
} else {
|
|
""
|
|
},
|
|
report.failed.len()
|
|
);
|
|
}
|
|
}
|
|
if !report.failed.is_empty() {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("{}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
|
|
}
|
|
}
|