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.
208 lines
8.9 KiB
Rust
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()
|
|
}
|