Files
pkh/src/main.rs
T
vhaudiquet 6caedce61a report: reproduce the pre-refactor CLI output through the ports
Instead of carrying raw UI in core, the ports now represent everything
the CLI used to do inline:

- Prompter::present shows context outside of a question (the wizard
  summary screen, the vendoring notice spacing); TerminalPrompter
  prints it on stdout exactly like the println!s it replaces, server
  embeds forward it as a display event.
- generate_entry returns the generated entry (package, versions,
  series, path) instead of printing; the CLI renders the same lines.
- BuildTarget carries a flow-composed display line and a tee_log flag:
  the terminal adapter renders it verbatim ("Building source package
  ...", "Building ... for series/arch", "Uploading ... to ...") and
  uploads open no build log.
- The unmet build-dependency diagnostics are rendered by the CLI from
  the typed error, in the original order (details, then summary).
- --verbose constructs no live view at all (an idle widget used to
  linger), and the re-vendor offer only logs when it is actually
  asked, so headless runs print the error exactly once.
2026-09-19 00:52:31 +02:00

770 lines
36 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 (default for a new project skeleton: no orig tarball)").required(false))
.arg(
clap::Arg::new("quilt")
.long("quilt")
.action(clap::ArgAction::SetTrue)
.conflicts_with("native")
.help("Use the 3.0 (quilt) source format with an orig tarball (default when packaging an existing project)"),
)
.arg(
clap::Arg::new("orig_from")
.long("orig-from")
.value_name("MODE")
.value_parser(["release", "git", "path", "snapshot"])
.conflicts_with("native")
.help("How to produce the orig tarball (quilt only): release (download the forge tarball of the tag; network), git (git archive of the tag), path (repack --orig-path), snapshot (tar the working tree). Default: git on a tag, snapshot otherwise"),
)
.arg(
clap::Arg::new("orig_path")
.long("orig-path")
.value_name("FILE|URL")
.conflicts_with("native")
.help("Tarball used by --orig-from path: a local file or an http(s) URL (.tar, .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz), repacked to the orig"),
)
.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))
.arg(arg!(--orig <when> "Original source tarball in the upload [auto, always, never] (default: auto)").required(false)
.long_help("Whether the upload distributes the original source tarball(s), like dpkg-genchanges' -sa/-si/-sd source styles.\nauto: include them only when the upstream version changed since the previous changelog entry (the default);\nalways: force inclusion, even for a revision bump of the same upstream version;\nnever: never include them, even for a new upstream version.\nAn explicit value is ignored with a warning for native packages (they have no separate orig tarball).")
.value_parser(["auto", "always", "never"])),
)
.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),
quilt: sub_matches
.get_one::<bool>("quilt")
.copied()
.unwrap_or(false),
orig_from: sub_matches.get_one::<String>("orig_from").cloned(),
orig_path: sub_matches.get_one::<String>("orig_path").cloned(),
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.
let prompter = pkh::ui::prompt::TerminalPrompter;
if let Err(e) = rt.block_on(async {
let opts = pkh::new::questions::run(cli, &prompter).await?;
let outcome = pkh::new::scaffold(opts.clone(), &multi)?;
pkh::new::questions::offer_verification(
&opts, &outcome, &multi, no_verify, &prompter,
)
.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 = match ppa.map(pkh::package_info::split_ppa) {
Some(Ok((user, name))) => Some(pkh::package_info::ppa_to_base_url(user, name)),
Some(Err(e)) => {
error!("{e}");
std::process::exit(1);
}
None => None,
};
// 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 {
let changelog_path = cwd.join("debian/changelog");
match rt.block_on(pkh::changelog::series_candidates(&changelog_path)) {
Some(pkh::changelog::SeriesCandidates::Choose {
options,
default,
fallback,
}) => match pkh::ui::select_series(&options, &default) {
Ok(selected) => Some(selected),
Err(e) => {
error!(
"Series selection failed: {}. Using current series '{}' instead.",
e, fallback
);
Some(fallback)
}
},
// Could not fetch the series list: use the current series
Some(pkh::changelog::SeriesCandidates::Keep(current)) => Some(current),
// No parsable changelog: leave the series decision to
// generate_entry
None => None,
}
};
let entry = match generate_entry(
"debian/changelog",
Some(&cwd),
version,
target_series.as_deref(),
) {
Ok(entry) => entry,
Err(e) => {
error!("{}", e);
std::process::exit(1);
}
};
println!(
"Found package: {}, version: {}",
entry.package, entry.previous_version
);
println!("Added new changelog entry to {}", entry.path.display());
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, unless --verbose (DebUi additionally disables
// itself when stdout is not a terminal)
let quiet = pkh::report::Quiet;
let live = if verbose {
None
} else {
Some(pkh::ui::deb::DebUi::new(&multi))
};
let view: &dyn pkh::report::BuildView = live
.as_ref()
.map(|v| v as &dyn pkh::report::BuildView)
.unwrap_or(&quiet);
let prompter = pkh::ui::prompt::TerminalPrompter;
let orig_source = match sub_matches.get_one::<String>("orig").map(String::as_str) {
Some("always") => pkh::build::OrigSourceMode::Always,
Some("never") => pkh::build::OrigSourceMode::Never,
_ => pkh::build::OrigSourceMode::Auto,
};
match pkh::build::build_source_package(pkh::build::BuildSourceOptions {
source: Some(cwd),
options: pkh::build::SourceBuildOptions {
orig_source,
..Default::default()
},
view,
prompter: &prompter,
}) {
Ok(output) => {
// The live view lists the artifacts itself when it
// renders; otherwise (verbose mode or non-TTY stdout)
// print them as plain lines.
if !view.is_enabled() {
for artifact in output.artifacts() {
println!(" {}", pkh::report::display_path(&artifact));
}
}
if output.signed {
println!("Package built and signed successfully!");
} else {
println!("Package built successfully (unsigned).");
}
}
Err(e) => {
// The unmet-dependency diagnostics first, then the
// summary: the exact rendering the flow used to do.
if let Some(unmet) =
e.downcast_ref::<pkh::debian::deps::UnmetBuildDependencies>()
{
eprintln!("{}", unmet.0.message());
}
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 view = pkh::ui::deb::DebUi::new(&multi);
let prompter = pkh::ui::prompt::TerminalPrompter;
let options = pkh::put::PutOptions {
ppa: ppa.to_string(),
changes,
force,
cwd,
view: &view,
prompter: &prompter,
};
if let Err(e) = rt.block_on(async { pkh::put::put(&options).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").cloned();
let pocket = sub_matches.get_one::<String>("pocket").cloned();
let arch = sub_matches.get_one::<String>("arch").cloned();
let cross = sub_matches
.get_one::<bool>("cross")
.copied()
.unwrap_or(false);
let ppa: Vec<String> = sub_matches
.get_many::<String>("ppa")
.map(|v| v.cloned().collect())
.unwrap_or_default();
let inject: Vec<String> = sub_matches
.get_many::<String>("inject")
.map(|v| v.cloned().collect())
.unwrap_or_default();
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, unless --verbose (DebUi additionally disables
// itself when stdout is not a terminal)
let quiet = pkh::report::Quiet;
let live = if verbose {
None
} else {
Some(pkh::ui::deb::DebUi::new(&multi))
};
let view: &dyn pkh::report::BuildView = live
.as_ref()
.map(|v| v as &dyn pkh::report::BuildView)
.unwrap_or(&quiet);
let result = rt.block_on(async {
pkh::deb::build_binary_package(pkh::deb::DebBuildOptions {
arch,
series,
pocket,
cwd: Some(cwd.clone()),
cross,
mode,
ppa,
inject,
jobs,
view,
..Default::default()
})
.await
});
match result {
Ok(_) => 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);
});
match pkh::context::ContextConfig::from_endpoint(endpoint) {
Ok(config) => config,
Err(e) => {
error!("{e}");
std::process::exit(1);
}
}
}
_ => {
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`"),
}
}