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.
This commit is contained in:
+17
-3
@@ -169,6 +169,13 @@ fn retry_after_revendor(
|
||||
opts: &SourceBuildOptions,
|
||||
original: Box<dyn Error>,
|
||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||
// The offer is only made when someone can answer it: headless prompters
|
||||
// answer with the default (false) without the error being logged here —
|
||||
// the caller logs the returned error itself, exactly once.
|
||||
if !prompter.interactive() {
|
||||
view.finish_failure();
|
||||
return Err(original);
|
||||
}
|
||||
log::error!("{original}");
|
||||
if !prompter
|
||||
.confirm(
|
||||
@@ -301,7 +308,14 @@ pub fn run_source_build(
|
||||
package: &entry.source,
|
||||
version: &entry.version.full(),
|
||||
target: &entry.distribution,
|
||||
display: format!(
|
||||
"Building source package {} ({}) for {}",
|
||||
entry.source,
|
||||
entry.version.full(),
|
||||
entry.distribution
|
||||
),
|
||||
source_only: true,
|
||||
tee_log: true,
|
||||
});
|
||||
|
||||
let source_display = entry.source.clone();
|
||||
@@ -389,9 +403,9 @@ pub fn run_source_build(
|
||||
};
|
||||
let report = crate::debian::deps::check_build_depends(&ctrl, &check_opts)?;
|
||||
if !report.is_ok() {
|
||||
// The diagnostics travel inside the error (its Display carries
|
||||
// report.message()); main renders it and maps the type to exit
|
||||
// status 3, like dpkg-buildpackage.
|
||||
// The typed error carries the diagnostics (UnmetReport is
|
||||
// public); the caller renders them and maps the type to exit
|
||||
// status 3, like dpkg-buildpackage does.
|
||||
return Err(Box::new(crate::debian::deps::UnmetBuildDependencies(
|
||||
report,
|
||||
)));
|
||||
|
||||
+33
-14
@@ -5,13 +5,30 @@ use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// Outcome of a successful [`generate_entry`] call: everything the CLI
|
||||
/// renders for the user, and everything a library consumer needs to chain
|
||||
/// further steps.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GeneratedEntry {
|
||||
/// Source package name from the previous changelog entry.
|
||||
pub package: String,
|
||||
/// Version the changelog carried before the new entry was prepended.
|
||||
pub previous_version: String,
|
||||
/// Version of the freshly added entry.
|
||||
pub new_version: String,
|
||||
/// Distribution series the new entry targets.
|
||||
pub series: String,
|
||||
/// The changelog file that was updated.
|
||||
pub path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
/// Automatically generate a changelog entry from a commit history and previous changelog
|
||||
pub fn generate_entry(
|
||||
changelog_file: &str,
|
||||
cwd: Option<&Path>,
|
||||
user_version: Option<&str>,
|
||||
target_series: Option<&str>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<GeneratedEntry, Box<dyn std::error::Error>> {
|
||||
let changelog_path = if let Some(path) = cwd {
|
||||
path.join(changelog_file)
|
||||
} else {
|
||||
@@ -19,8 +36,7 @@ pub fn generate_entry(
|
||||
};
|
||||
|
||||
// Parse existing changelog to get current (old) version
|
||||
let (package, old_version, series) = parse_changelog_header(&changelog_path)?;
|
||||
log::info!("Found package: {}, version: {}", package, old_version);
|
||||
let (package, old_version, current_series) = parse_changelog_header(&changelog_path)?;
|
||||
|
||||
// Open git repo, and find commits since last version tag
|
||||
let repo_path = if let Some(path) = cwd {
|
||||
@@ -44,7 +60,7 @@ pub fn generate_entry(
|
||||
};
|
||||
|
||||
let (maintainer_name, maintainer_email) = get_maintainer_info()?;
|
||||
let series = target_series.unwrap_or(&series).to_string();
|
||||
let series = target_series.unwrap_or(¤t_series).to_string();
|
||||
let new_entry = format_entry(
|
||||
&package,
|
||||
&new_version,
|
||||
@@ -56,9 +72,13 @@ pub fn generate_entry(
|
||||
|
||||
prepend_to_file(&changelog_path, &new_entry)?;
|
||||
|
||||
log::info!("Added new changelog entry to {}", changelog_path.display());
|
||||
|
||||
Ok(())
|
||||
Ok(GeneratedEntry {
|
||||
package,
|
||||
previous_version: old_version,
|
||||
new_version,
|
||||
series,
|
||||
path: changelog_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute the next (most probable) version number of a package, from old version and
|
||||
@@ -195,17 +215,16 @@ pub async fn series_candidates(changelog_path: &Path) -> Option<SeriesCandidates
|
||||
}
|
||||
} else {
|
||||
match crate::distro_info::get_dist_from_series(¤t).await {
|
||||
Ok(dist) => {
|
||||
match crate::distro_info::get_ordered_series_name(&dist).await {
|
||||
Ok(options) if !options.is_empty() => Some(SeriesCandidates::Choose {
|
||||
Ok(dist) => match crate::distro_info::get_ordered_series_name(&dist).await {
|
||||
// Even an empty list goes through the selector: its
|
||||
// fallback prints and takes the default, like it always has
|
||||
Ok(options) => Some(SeriesCandidates::Choose {
|
||||
options,
|
||||
default: current.clone(),
|
||||
fallback: current,
|
||||
}),
|
||||
// An empty list offers nothing to choose from
|
||||
_ => Some(SeriesCandidates::Keep(current)),
|
||||
}
|
||||
}
|
||||
Err(_) => Some(SeriesCandidates::Keep(current)),
|
||||
},
|
||||
Err(_) => Some(SeriesCandidates::Keep(current)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,9 @@ async fn build_binary_package_impl(
|
||||
package: &package,
|
||||
version: &version,
|
||||
target: &format!("{series}/{arch}"),
|
||||
display: format!("Building {package} ({version}) for {series}/{arch}"),
|
||||
source_only: false,
|
||||
tee_log: true,
|
||||
});
|
||||
|
||||
// Create an ephemeral unshare context for all Local builds. It is kept in
|
||||
|
||||
+1
-5
@@ -890,11 +890,7 @@ pub struct UnmetBuildDependencies(pub UnmetReport);
|
||||
|
||||
impl std::fmt::Display for UnmetBuildDependencies {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"build dependencies/conflicts unsatisfied; aborting\n{}",
|
||||
self.0.message()
|
||||
)
|
||||
write!(f, "build dependencies/conflicts unsatisfied; aborting")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-7
@@ -418,15 +418,23 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = generate_entry(
|
||||
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,
|
||||
@@ -461,8 +469,15 @@ fn main() {
|
||||
// Live build view, unless --verbose (DebUi additionally disables
|
||||
// itself when stdout is not a terminal)
|
||||
let quiet = pkh::report::Quiet;
|
||||
let live = pkh::ui::deb::DebUi::new(&multi);
|
||||
let view: &dyn pkh::report::BuildView = if verbose { &quiet } else { &live };
|
||||
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) {
|
||||
@@ -490,12 +505,19 @@ fn main() {
|
||||
}
|
||||
}
|
||||
if output.signed {
|
||||
info!("Package built and signed successfully!");
|
||||
println!("Package built and signed successfully!");
|
||||
} else {
|
||||
info!("Package built successfully (unsigned).");
|
||||
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.
|
||||
@@ -581,8 +603,15 @@ fn main() {
|
||||
// Live build view, unless --verbose (DebUi additionally disables
|
||||
// itself when stdout is not a terminal)
|
||||
let quiet = pkh::report::Quiet;
|
||||
let live = pkh::ui::deb::DebUi::new(&multi);
|
||||
let view: &dyn pkh::report::BuildView = if verbose { &quiet } else { &live };
|
||||
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 {
|
||||
|
||||
@@ -541,7 +541,7 @@ async fn run_wizard(
|
||||
|
||||
// Summary screen + final confirmation: Ctrl+C or 'n' abort with
|
||||
// nothing written (generation is all-or-nothing later anyway).
|
||||
log::info!("{}", summary_text(&opts, toolchain_pin.as_deref()));
|
||||
prompter.present(&summary_text(&opts, toolchain_pin.as_deref()));
|
||||
if !prompter.confirm("Generate?", true)? {
|
||||
return Err("Aborted: nothing was written to disk.".into());
|
||||
}
|
||||
@@ -575,6 +575,9 @@ pub async fn offer_verification(
|
||||
};
|
||||
|
||||
if outcome.vendoring_failed {
|
||||
// Set apart from the surrounding success output by blank lines: a
|
||||
// single warning between two success lines is easy to miss.
|
||||
prompter.present("");
|
||||
log::warn!(
|
||||
"The Cargo dependencies could NOT be vendored: this package will \
|
||||
not build until the vendoring is completed by hand:\n\
|
||||
@@ -582,6 +585,7 @@ pub async fn offer_verification(
|
||||
\x20 2. add the printed source replacement to .cargo/config.toml, \
|
||||
plus `[net] offline = true`"
|
||||
);
|
||||
prompter.present("");
|
||||
}
|
||||
|
||||
if no_verify || !prompter.interactive() {
|
||||
|
||||
@@ -976,8 +976,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Serve canned byte responses on a local port, one per connection (the
|
||||
/// last response repeats), and return the base URL
|
||||
///
|
||||
|
||||
+13
-3
@@ -26,7 +26,7 @@ use crate::debian::checksums::FileChecksums;
|
||||
use crate::debian::control::ControlInfo;
|
||||
use crate::debian::version::DebianVersion;
|
||||
use crate::launchpad;
|
||||
use crate::report::{BuildView, Prompter};
|
||||
use crate::report::{BuildTarget, BuildView, Prompter};
|
||||
|
||||
/// Everything `put` needs to run.
|
||||
pub struct PutOptions<'a> {
|
||||
@@ -89,10 +89,20 @@ async fn put_impl(opts: &PutOptions<'_>) -> Result<(), Box<dyn std::error::Error
|
||||
};
|
||||
let changes = changes::parse(&changes_path)?;
|
||||
|
||||
opts.view.message(&format!(
|
||||
// The summary line stays up for the whole flow: rendered as the view's
|
||||
// persistent status, with the per-step messages below it
|
||||
opts.view.target(BuildTarget {
|
||||
package: &changes.source,
|
||||
version: &changes.version,
|
||||
target: &target.label,
|
||||
display: format!(
|
||||
"Uploading {} {} to {}",
|
||||
changes.source, changes.version, target.label
|
||||
));
|
||||
),
|
||||
source_only: false,
|
||||
// An upload runs no subprocess: nothing to tee
|
||||
tee_log: false,
|
||||
});
|
||||
changes::validate(&changes)?;
|
||||
|
||||
// Pre-flight checks for everything the upload queue only rejects after
|
||||
|
||||
+23
-4
@@ -30,18 +30,28 @@ pub type Validator = dyn Fn(&str) -> Result<(), String>;
|
||||
|
||||
/// Identity of the build whose events follow, as announced through
|
||||
/// [`BuildView::target`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildTarget<'a> {
|
||||
/// Source package name (e.g. `hello`).
|
||||
pub package: &'a str,
|
||||
/// Full version being built (e.g. `2.10-3`).
|
||||
pub version: &'a str,
|
||||
/// What the build targets: a distribution series, optionally with an
|
||||
/// architecture (`noble`, `sid`, `noble/arm64`).
|
||||
/// architecture (`noble`, `sid`, `noble/arm64`), or the upload target
|
||||
/// label.
|
||||
pub target: &'a str,
|
||||
/// Whether this is a source-only build (producing a `.dsc`) as opposed
|
||||
/// to a binary build (producing `.deb` files).
|
||||
/// Ready-to-render status line for display adapters, composed by the
|
||||
/// flow (e.g. "Building source package hello (2.10-3) for unstable",
|
||||
/// "Uploading hello (2.10-3) to ppa:user/ppa"): the wording is the
|
||||
/// flow's, adapters render it verbatim.
|
||||
pub display: String,
|
||||
/// Whether this is a source-only build (producing a `.dsc`); names the
|
||||
/// terminal adapter's tee log (`build-*.log` vs `deb-*.log`).
|
||||
pub source_only: bool,
|
||||
/// Whether the view should tee raw subprocess output to its log file.
|
||||
/// Flows without subprocess output (uploads) pass `false` and create
|
||||
/// no log file.
|
||||
pub tee_log: bool,
|
||||
}
|
||||
|
||||
/// Observer of a running build: target identification, phases, status
|
||||
@@ -156,6 +166,15 @@ pub trait Prompter: Send + Sync {
|
||||
fn accept_host_key(&self, _host: &str, _key_type: &str, _fingerprint: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Present information to the user outside of any question: the
|
||||
/// scaffold wizard's summary screen, prominent notices around a
|
||||
/// warning. Terminal implementations print the text as-is (unstyled,
|
||||
/// on stdout); server implementations forward it as a display event.
|
||||
/// Implementations that cannot show anything drop it — flows only
|
||||
/// present context that is also available structurally (returned data,
|
||||
/// log records).
|
||||
fn present(&self, _text: &str) {}
|
||||
}
|
||||
|
||||
/// Inert view and prompter: drops every event and answers every question
|
||||
|
||||
+6
-13
@@ -83,8 +83,7 @@ impl DebUi {
|
||||
let pb = multi.add(ProgressBar::new(0));
|
||||
pb.enable_steady_tick(Duration::from_millis(80));
|
||||
pb.set_style(spinner_style());
|
||||
// Neutral identity until a `target` event names the build
|
||||
pb.set_prefix("pkh");
|
||||
pb.set_prefix("Building package");
|
||||
pb.set_message("(starting…)");
|
||||
pb
|
||||
} else {
|
||||
@@ -270,25 +269,19 @@ impl DebUi {
|
||||
/// widget.
|
||||
impl crate::report::BuildView for DebUi {
|
||||
fn target(&self, target: BuildTarget<'_>) {
|
||||
let kind = if target.source_only {
|
||||
"source package "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if self.shared.enabled {
|
||||
self.shared.top.set_prefix(format!(
|
||||
"Building {kind}{} ({}) for {}",
|
||||
target.package, target.version, target.target
|
||||
));
|
||||
self.shared.top.set_prefix(target.display.clone());
|
||||
}
|
||||
let log_kind = if target.source_only { "build" } else { "deb" };
|
||||
if target.tee_log {
|
||||
let kind = if target.source_only { "build" } else { "deb" };
|
||||
self.open_log(
|
||||
log_kind,
|
||||
kind,
|
||||
target.package,
|
||||
target.version,
|
||||
&format!("for {}", target.target),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn phase(&self, name: &str, classifier: Box<dyn Classifier>) {
|
||||
self.phase_custom(name, classifier);
|
||||
|
||||
@@ -167,6 +167,10 @@ impl crate::report::Prompter for TerminalPrompter {
|
||||
println!("{key_type} key fingerprint is {fingerprint}.");
|
||||
confirm("Accept and store this host key?", false).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn present(&self, text: &str) {
|
||||
println!("{text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `prompt` with the terminal in raw mode, always restoring it
|
||||
|
||||
Reference in New Issue
Block a user