report: add BuildView/Prompter ports and drive pkh build through them

Core flows no longer reach into the terminal UI: build_source_package
takes a BuildSourceOptions struct (source tree, domain options, view,
prompter) and reports phases, messages and outcomes through the
environment-agnostic ports in the new report module. The classifiers
move from ui/logfmt to the core logfmt module, DebUi becomes a
BuildView adapter, the re-vendor retry asks the prompter instead of
checking for a TTY, and artifact/success printing moves to the CLI.

Headless consumers pass report::Quiet; an embedding (e.g. a builder
server forwarding events to a web frontend) implements BuildView and
maps the plain-data events onto its own wire format.
This commit is contained in:
2026-09-18 20:02:56 +02:00
parent a7d2cfdc6e
commit 27b1083b15
11 changed files with 335 additions and 179 deletions
+95
View File
@@ -0,0 +1,95 @@
//! 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).
use std::path::PathBuf;
use std::sync::Arc;
use crate::context::LineSink;
use crate::logfmt::Classifier;
/// Observer of a running build: target identification, phases, status
/// messages, progress and the final outcome.
///
/// Implement this to observe [`crate::build`] 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: `package` at `version`, built for
/// `target` (a distribution series, optionally with an architecture).
fn target(&self, _package: &str, _version: &str, _target: &str) {}
/// 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) {}
/// 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 (confirmations
/// now, selections and free-text input as more flows migrate).
///
/// 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 {
/// Ask a yes/no question. `default` is the answer to take when no user
/// can be reached or the question is cancelled.
fn confirm(&self, _question: &str, default: bool) -> bool {
default
}
}
/// 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 {}