Files
pkh/src/report.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

208 lines
8.9 KiB
Rust

//! Environment-agnostic reporting ports.
//!
//! The core flows report progress and ask questions exclusively through the
//! traits in this module, so the same pipeline can drive a terminal live
//! view, a headless library consumer, or a remote frontend (e.g. a builder
//! server forwarding build events to a web UI over server-sent events): every
//! event carries plain data — strings, numbers, paths — with no terminal,
//! styling or locale assumptions. Adapters decide how events reach the user:
//! the terminal live view ([`crate::ui::deb::DebUi`]) renders them in place,
//! while another embedding maps each method onto its own wire format.
//!
//! Every [`BuildView`] method defaults to doing nothing, so implementations
//! only override the events they care about; [`Quiet`] provides the inert
//! implementations used by headless runs and tests. [`Prompter`] is
//! deliberately blocking: an implementation may round-trip each question to
//! a remote user, as long as it eventually answers (or takes the default).
//! Asking can fail (the user cancels, the connection drops); flows
//! propagate the error, which aborts them.
use std::error::Error;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::context::LineSink;
use crate::logfmt::Classifier;
/// Answer validator of [`Prompter::text`]: accepts the answer, or explains
/// why it is rejected (the implementation re-asks with the explanation).
pub type Validator = dyn Fn(&str) -> Result<(), String>;
/// Identity of the build whose events follow, as announced through
/// [`BuildView::target`].
#[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`), or the upload target
/// label.
pub target: &'a str,
/// 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
/// messages, progress and the final outcome.
///
/// Implement this to observe the [`crate::build`] and [`crate::deb`] flows
/// from any frontend. All events arrive in order from the build thread;
/// long-lived views are expected to be `Send + Sync` because builds may run
/// inside async tasks.
pub trait BuildView: Send + Sync {
/// The build target was identified; the events that follow belong to it
/// (including the earliest subprocess output, e.g. a chroot download).
fn target(&self, _target: BuildTarget) {}
/// A named phase started (e.g. "Applying patches"). `classifier`
/// rewrites the phase's raw subprocess lines (see [`crate::logfmt`])
/// into display actions and countable progress; views that do not
/// rewrite lines locally can ignore it and forward raw lines from
/// [`BuildView::sink`] instead.
fn phase(&self, _name: &str, _classifier: Box<dyn Classifier>) {}
/// A status message about in-process work that produces no subprocess
/// output (e.g. "Generating .changes").
fn message(&self, _text: &str) {}
/// Determinate progress within the current phase (e.g. artifact
/// retrieval); `pos` runs from 0 to `total`.
fn progress(&self, _label: &str, _pos: usize, _total: usize) {}
/// Sink receiving every raw subprocess line while a build command runs,
/// when this view consumes the lines itself (live rewriting, tee to a
/// log file, forwarding over the network). `None` lets the caller fall
/// back to its default handling (e.g. test capture).
fn sink(&self) -> Option<Arc<dyn LineSink>> {
None
}
/// The build succeeded; `artifacts` lists the produced files in
/// distribution order (dsc → tarballs → buildinfo → changes).
fn finish_success(&self, _artifacts: &[PathBuf]) {}
/// The build failed; the view should release the display and may report
/// diagnostics it collected through [`BuildView::sink`].
fn finish_failure(&self) {}
/// Release the display before writing directly to the shared terminal
/// (passthrough diagnostics, cleanup commands that inherit it). The
/// release is final for this build; later events may be dropped. A
/// no-op for views without a display.
fn suspend(&self) {}
/// Whether this view presents build results to the user by itself;
/// callers use this to fall back to plain-line rendering when it does
/// not (headless views, verbose mode).
fn is_enabled(&self) -> bool {
false
}
}
/// Answerer of the questions a core flow may ask mid-run.
///
/// Questions are blocking on purpose: a terminal implementation waits for
/// key presses, and a builder-server implementation may forward the question
/// to a web client and await the answer on a channel. Implementations that
/// cannot ask anyone answer with the question's default.
pub trait Prompter: Send + Sync {
/// Whether this prompter can interact with a user at all. Flows with an
/// interactive and a headless path use this to pick one: a headless run
/// takes the defaults or fails with the list of missing answers instead
/// of asking question by question.
fn interactive(&self) -> bool {
false
}
/// Ask a yes/no question. `default` is the answer to take when no user
/// can be reached; `Err` means the question was cancelled (Ctrl+C,
/// dropped connection) and the flow should abort.
fn confirm(&self, _question: &str, default: bool) -> Result<bool, Box<dyn Error>> {
Ok(default)
}
/// Ask a one-line selection among `options`, with `default`
/// preselected. Selector implementations may also accept typed
/// arbitrary text; callers validate the answer and re-ask through the
/// same method when they must reject one. `Err` cancels the flow.
fn select(
&self,
_label: &str,
_options: &[String],
default: &str,
) -> Result<String, Box<dyn Error>> {
Ok(default.to_string())
}
/// Ask a free-text answer. `validate` is applied by the implementation
/// so invalid input re-asks at the source (mid-input for a terminal,
/// round-trip for a server). `Err` cancels the flow.
fn text(
&self,
_label: &str,
default: &str,
_validate: Option<&Validator>,
) -> Result<String, Box<dyn Error>> {
Ok(default.to_string())
}
/// Ask whether to accept and store an unverified SSH host key (trust on
/// first use): `host` is the display form (`host` or `[host]:port`),
/// `key_type` the key type name ("ssh-ed25519", ...) and `fingerprint`
/// the human-readable digest. Fail-closed: implementations that cannot
/// ask anyone answer `false`, refusing the connection.
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
/// with its default. The stand-in for headless library runs, verbose mode
/// and tests.
pub struct Quiet;
impl BuildView for Quiet {}
impl Prompter for Quiet {}
/// Render a path for terminal display: relative to the current working
/// directory when the target lives inside it or directly next to it
/// (`../name`, the usual layout of build artifacts), absolute otherwise.
///
/// Pure formatting shared by the flows that mention paths in their events
/// and messages; a remote frontend reproduces it (or not) on its side.
pub fn display_path(path: &Path) -> String {
let Ok(cwd) = std::env::current_dir() else {
return path.display().to_string();
};
if let Ok(rel) = path.strip_prefix(&cwd) {
return rel.display().to_string();
}
if let Some(parent) = cwd.parent()
&& let Ok(rel) = path.strip_prefix(parent)
{
return format!("../{}", rel.display());
}
path.display().to_string()
}