//! 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>; /// Which flow a target belongs to; names the terminal adapter's tee log /// (`build-*.log`, `deb-*.log`, `test-*.log`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TargetKind { /// Source-only build (produces a `.dsc`). Source, /// Binary build (produces `.debs`). Binary, /// DEP-8 test run (`pkh test`). Test, } /// 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, /// The flow driving the events; names the terminal adapter's tee log. pub kind: TargetKind, /// 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`], [`crate::deb`] and /// [`crate::pull`] 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. The pull flow only emits the /// [`BuildView::message`], [`BuildView::progress`] and /// [`BuildView::byte_progress`] events — no target, phases or outcome. 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) {} /// 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) {} /// Determinate progress of a byte transfer within the current phase /// (e.g. the staged-tree copy forked from an incremental base): /// `pos` of `total` bytes. Like [`BuildView::progress`], but adapters /// render byte units for it. fn byte_progress(&self, _label: &str, _pos: u64, _total: u64) {} /// 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> { 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> { 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> { 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> { 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() }