deb: drive build_binary_package through the BuildView port

The binary build joins the source build on the reporting ports:
build_binary_package takes a DebBuildOptions struct (replacing eleven
positional arguments), reports target, phases, progress and the
outcome through the environment-agnostic BuildView, and the Phase enum
with its default classifiers moves from the terminal widget into the
deb module (announced through the enter_phase helper). DebUi loses its
inherent event methods and only implements the port; tee logging and
the SIGINT behavior are unchanged.

No behavior change for the CLI; headless consumers pass report::Quiet.
This commit is contained in:
2026-09-18 20:22:45 +02:00
parent 27b1083b15
commit 052c02cdc3
8 changed files with 390 additions and 428 deletions
+48 -154
View File
@@ -21,10 +21,8 @@ use directories::ProjectDirs;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use crate::context::{LineSink, Stream};
use crate::logfmt::{
Action, AptInstallClassifier, AptUpdateClassifier, Classifier, GenericClassifier,
MakeClassifier, MmdebstrapClassifier, QuiltClassifier,
};
use crate::logfmt::{Action, Classifier, GenericClassifier};
use crate::report::BuildTarget;
/// Number of lines displayed in the rolling pane
const PANE_LINES: usize = 10;
@@ -32,69 +30,6 @@ const PANE_LINES: usize = 10;
/// Minimum interval between pane redraws
const REDRAW_INTERVAL: Duration = Duration::from_millis(50);
/// Build phases of `pkh deb`, shown in the status bar
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
/// Downloading the chroot tarball (mmdebstrap)
PreparingChroot,
/// Extracting the chroot tarball
ExtractingChroot,
/// Device nodes, /proc bind mount, etc.
FinalizingChroot,
/// apt-get update
UpdatingPackageLists,
/// Installing build-essential & co
InstallingEssentials,
/// quilt push -a
ApplyingPatches,
/// --inject packages
InjectingPackages,
/// apt-get build-dep
InstallingBuildDeps,
/// debian/rules build
Building,
/// fakeroot debian/rules binary
ProducingBinaries,
/// Retrieving produced .deb files
RetrievingArtifacts,
}
impl Phase {
/// Human-readable label displayed in the status bar
pub fn label(&self) -> &'static str {
match self {
Phase::PreparingChroot => "Preparing chroot",
Phase::ExtractingChroot => "Extracting chroot",
Phase::FinalizingChroot => "Finalizing chroot",
Phase::UpdatingPackageLists => "Updating package lists",
Phase::InstallingEssentials => "Installing essential packages",
Phase::ApplyingPatches => "Applying patches",
Phase::InjectingPackages => "Injecting packages",
Phase::InstallingBuildDeps => "Installing build dependencies",
Phase::Building => "Building package",
Phase::ProducingBinaries => "Producing binary packages",
Phase::RetrievingArtifacts => "Retrieving artifacts",
}
}
}
/// Default classifier used for a given phase
fn default_classifier(phase: Phase) -> Box<dyn Classifier> {
match phase {
Phase::PreparingChroot => Box::new(MmdebstrapClassifier::new()),
Phase::ExtractingChroot | Phase::FinalizingChroot => Box::new(GenericClassifier::new()),
Phase::UpdatingPackageLists => Box::new(AptUpdateClassifier::new()),
Phase::InstallingEssentials => Box::new(AptInstallClassifier::new("Installing essentials")),
Phase::ApplyingPatches => Box::new(QuiltClassifier::new(0)),
Phase::InjectingPackages => Box::new(AptInstallClassifier::new("Injecting packages")),
Phase::InstallingBuildDeps => {
Box::new(AptInstallClassifier::new("Installing build dependencies"))
}
Phase::Building | Phase::ProducingBinaries => Box::new(MakeClassifier::new()),
Phase::RetrievingArtifacts => Box::new(GenericClassifier::new()),
}
}
/// Visual kind of a pane line, driving its color
#[derive(Debug, Clone, Copy, PartialEq)]
enum Kind {
@@ -129,11 +64,9 @@ struct Shared {
/// Live build view for `pkh deb` / `pkh build`
///
/// Create one per build (disabled automatically when stdout is not a TTY or
/// when the user requests verbose output), then either pass it down through
/// the [`crate::report::BuildView`] port (source builds) or as
/// `Option<Arc<DebUi>>` (the `pkh deb` flow, until it migrates), and feed
/// subprocess output through [`DebUi::sink`].
/// Create one per build (it disables itself automatically when stdout is not
/// a TTY) and pass it down through the [`crate::report::BuildView`] port.
/// Subprocess output reaches it through [`crate::report::BuildView::sink`].
pub struct DebUi {
shared: Arc<Shared>,
}
@@ -252,20 +185,7 @@ impl DebUi {
}
}
/// Switch to a phase, installing its default classifier
pub fn phase(&self, phase: Phase) {
self.phase_custom(phase.label(), default_classifier(phase));
}
/// Switch to a phase with a custom classifier (e.g. quilt with a known
/// patch count)
pub fn phase_with(&self, phase: Phase, classifier: Box<dyn Classifier>) {
self.phase_custom(phase.label(), classifier);
}
/// Switch to an arbitrary status label with a custom classifier; the
/// shared implementation behind [`DebUi::phase`], [`DebUi::phase_with`]
/// and the [`BuildView`] port
/// Switch to an arbitrary status label with a custom classifier
fn phase_custom(&self, label: &str, classifier: Box<dyn Classifier>) {
{
let mut st = self.shared.state.lock().unwrap();
@@ -281,47 +201,18 @@ impl DebUi {
}
}
/// Update the status bar message directly (for in-process work such as
/// tarball extraction that has no subprocess output)
pub fn progress_message(&self, msg: &str) {
if self.active() {
self.shared.top.set_message(msg.to_string());
}
}
/// Drive the determinate progress bar directly (e.g. artifact retrieval)
pub fn count_progress(&self, label: &str, pos: usize, total: usize) {
if !self.active() || total == 0 {
return;
}
apply_progress(
&self.shared.top,
&mut self.shared.state.lock().unwrap(),
pos as u64,
total as u64,
);
self.shared.top.set_message(label.to_string());
}
/// Whether the widget is enabled and still drawn
fn active(&self) -> bool {
self.shared.enabled && !self.shared.suspended.load(Ordering::SeqCst)
}
/// Obtain a sink feeding this view; pass it to `ContextCommand::capture`
pub fn sink(self: &Arc<Self>) -> Arc<dyn LineSink> {
Arc::new(Sink {
shared: self.shared.clone(),
})
}
/// Remove the widget from the terminal (e.g. before printing passthrough
/// diagnostics or letting child cleanup commands write to the terminal);
/// idempotent
/// Release the widget from the terminal (e.g. before printing
/// passthrough diagnostics or letting child cleanup commands write to
/// the terminal); idempotent
///
/// Steady ticks are disabled first: otherwise a tick can redraw a frame
/// right after the clear, leaving stale copies of the widget on screen.
pub fn suspend(&self) {
fn suspend(&self) {
if !self.shared.enabled {
return;
}
@@ -334,20 +225,8 @@ impl DebUi {
self.shared.pane.finish_and_clear();
}
/// Clear the widget and print a success summary with the artifacts,
/// Success outcome body: clear the widget and print the artifacts,
/// rendered relative to the working directory when possible
pub fn finish_success(&self, artifacts: &[PathBuf], elapsed: Duration) {
self.success_summary(artifacts, elapsed);
}
/// Clear the widget and print a failure summary (recent captured errors
/// and the path to the full log)
pub fn finish_failure(&self) {
self.failure_summary();
}
/// Shared body of the success outcome ([`DebUi::finish_success`] and the
/// [`BuildView`] port)
fn success_summary(&self, artifacts: &[PathBuf], elapsed: Duration) {
self.suspend();
if self.shared.enabled && !artifacts.is_empty() {
@@ -358,8 +237,8 @@ impl DebUi {
}
}
/// Shared body of the failure outcome ([`DebUi::finish_failure`] and the
/// [`BuildView`] port)
/// Failure outcome body: clear the widget and print a summary (recent
/// captured errors and the path to the full log)
fn failure_summary(&self) {
self.suspend();
@@ -383,31 +262,31 @@ impl DebUi {
eprintln!("Full log: {}", log_path.display());
}
}
/// Time elapsed since the view was created
pub fn elapsed(&self) -> Duration {
self.shared.started.elapsed()
}
/// Path of the full build log file
pub fn log_path(&self) -> PathBuf {
self.shared.log_path.lock().unwrap().clone()
}
}
/// [`crate::report::BuildView`] port: forwards build events to the live
/// widget, so core flows can drive the view without knowing it is a
/// terminal widget. Where the inherent methods keep their richer signatures
/// (e.g. [`DebUi::finish_success`] takes the elapsed duration explicitly for
/// the `pkh deb` flow), the port bridges them.
/// widget, so core flows drive the view without knowing it is a terminal
/// widget.
impl crate::report::BuildView for DebUi {
fn target(&self, package: &str, version: &str, target: &str) {
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 source package {package} ({version}) for {target}"
"Building {kind}{} ({}) for {}",
target.package, target.version, target.target
));
}
self.open_log("build", package, version, &format!("for {target}"));
let log_kind = if target.source_only { "build" } else { "deb" };
self.open_log(
log_kind,
target.package,
target.version,
&format!("for {}", target.target),
);
}
fn phase(&self, name: &str, classifier: Box<dyn Classifier>) {
@@ -415,11 +294,22 @@ impl crate::report::BuildView for DebUi {
}
fn message(&self, text: &str) {
self.progress_message(text);
if self.active() {
self.shared.top.set_message(text.to_string());
}
}
fn progress(&self, label: &str, pos: usize, total: usize) {
self.count_progress(label, pos, total);
if !self.active() || total == 0 {
return;
}
apply_progress(
&self.shared.top,
&mut self.shared.state.lock().unwrap(),
pos as u64,
total as u64,
);
self.shared.top.set_message(label.to_string());
}
fn sink(&self) -> Option<Arc<dyn LineSink>> {
@@ -429,13 +319,17 @@ impl crate::report::BuildView for DebUi {
}
fn finish_success(&self, artifacts: &[PathBuf]) {
self.success_summary(artifacts, self.elapsed());
self.success_summary(artifacts, self.shared.started.elapsed());
}
fn finish_failure(&self) {
self.failure_summary();
}
fn suspend(&self) {
DebUi::suspend(self);
}
fn is_enabled(&self) -> bool {
self.shared.enabled
}