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
+7 -2
View File
@@ -24,7 +24,7 @@ use crate::debian::{
ChecksumEntry, ControlInfo, FileChecksums, FilesEntry, FilesList, parse_paragraphs, ChecksumEntry, ControlInfo, FileChecksums, FilesEntry, FilesList, parse_paragraphs,
}; };
use crate::logfmt::{DpkgSourceClassifier, GenericClassifier}; use crate::logfmt::{DpkgSourceClassifier, GenericClassifier};
use crate::report::{BuildView, Prompter}; use crate::report::{BuildTarget, BuildView, Prompter};
/// Whether the upload distributes the upstream orig tarballs (`--orig`), /// Whether the upload distributes the upstream orig tarballs (`--orig`),
/// mirroring the `dpkg-genchanges` source styles. /// mirroring the `dpkg-genchanges` source styles.
@@ -294,7 +294,12 @@ pub fn run_source_build(
let ctrl = ControlInfo::parse(&control_path)?; let ctrl = ControlInfo::parse(&control_path)?;
view.target(&entry.source, &entry.version.full(), &entry.distribution); view.target(BuildTarget {
package: &entry.source,
version: &entry.version.full(),
target: &entry.distribution,
source_only: true,
});
let source_display = entry.source.clone(); let source_display = entry.source.clone();
+17 -26
View File
@@ -1,5 +1,6 @@
use crate::context::{self, Context, ContextConfig}; use crate::context::{self, Context, ContextConfig};
use crate::ui::deb::{DebUi, Phase}; use crate::deb::{Phase, enter_phase};
use crate::report::BuildView;
use directories::ProjectDirs; use directories::ProjectDirs;
use std::any::Any; use std::any::Any;
use std::error::Error; use std::error::Error;
@@ -312,7 +313,7 @@ impl EphemeralContextGuard {
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
base_ctx: Arc<Context>, base_ctx: Arc<Context>,
ui: Option<Arc<DebUi>>, view: &dyn BuildView,
) -> Result<Self, Box<dyn Error>> { ) -> Result<Self, Box<dyn Error>> {
// Save the globally-installed context so Drop can restore exactly // Save the globally-installed context so Drop can restore exactly
// this handle: concurrent builds install their own ephemeral // this handle: concurrent builds install their own ephemeral
@@ -355,7 +356,7 @@ impl EphemeralContextGuard {
// Download and extract the chroot tarball // Download and extract the chroot tarball
if let Err(e) = if let Err(e) =
Self::download_and_extract_chroot(series, arch, &chroot_path, base_ctx.clone(), &ui) Self::download_and_extract_chroot(series, arch, &chroot_path, base_ctx.clone(), view)
.await .await
{ {
// The guard (and its Drop) never materializes on this path, so // The guard (and its Drop) never materializes on this path, so
@@ -408,7 +409,7 @@ impl EphemeralContextGuard {
arch: Option<&str>, arch: Option<&str>,
chroot_path: &PathBuf, chroot_path: &PathBuf,
ctx: Arc<context::Context>, ctx: Arc<context::Context>,
ui: &Option<Arc<DebUi>>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Clone ctx for use in create_device_nodes after download_chroot_tarball consumes it // Clone ctx for use in create_device_nodes after download_chroot_tarball consumes it
let ctx_for_devices = ctx.clone(); let ctx_for_devices = ctx.clone();
@@ -464,10 +465,8 @@ impl EphemeralContextGuard {
series, series,
arch arch
); );
if let Some(u) = ui { enter_phase(view, Phase::PreparingChroot);
u.phase(Phase::PreparingChroot); Self::download_chroot_tarball(series, arch, &tarball_path, ctx, view).await?;
}
Self::download_chroot_tarball(series, arch, &tarball_path, ctx, ui).await?;
} else { } else {
log::debug!( log::debug!(
"Using cached chroot tarball for {} (arch: {:?})", "Using cached chroot tarball for {} (arch: {:?})",
@@ -478,16 +477,12 @@ impl EphemeralContextGuard {
// Extract tarball to chroot directory // Extract tarball to chroot directory
log::debug!("Extracting chroot tarball to {}...", chroot_path.display()); log::debug!("Extracting chroot tarball to {}...", chroot_path.display());
if let Some(u) = ui { enter_phase(view, Phase::ExtractingChroot);
u.phase(Phase::ExtractingChroot); Self::extract_tarball(&tarball_path, chroot_path, view)?;
}
Self::extract_tarball(&tarball_path, chroot_path, ui.as_deref())?;
// Create device nodes in the chroot // Create device nodes in the chroot
log::debug!("Creating device nodes in chroot..."); log::debug!("Creating device nodes in chroot...");
if let Some(u) = ui { enter_phase(view, Phase::FinalizingChroot);
u.phase(Phase::FinalizingChroot);
}
Self::create_device_nodes(chroot_path, ctx_for_devices.clone())?; Self::create_device_nodes(chroot_path, ctx_for_devices.clone())?;
// Bind mount /proc from host into chroot (before entering unshare namespace) // Bind mount /proc from host into chroot (before entering unshare namespace)
@@ -503,7 +498,7 @@ impl EphemeralContextGuard {
arch: Option<&str>, arch: Option<&str>,
tarball_path: &Path, tarball_path: &Path,
ctx: Arc<context::Context>, ctx: Arc<context::Context>,
ui: &Option<Arc<DebUi>>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Create a lock file to make sure that noone tries to use the file while it's not fully downloaded // Create a lock file to make sure that noone tries to use the file while it's not fully downloaded
let lockfile_path = tarball_path.with_extension("lock"); let lockfile_path = tarball_path.with_extension("lock");
@@ -537,8 +532,8 @@ impl EphemeralContextGuard {
cmd.arg(series) cmd.arg(series)
.arg(tarball_path.to_string_lossy().to_string()); .arg(tarball_path.to_string_lossy().to_string());
if let Some(u) = ui { if let Some(s) = view.sink() {
cmd.capture(u.sink()); cmd.capture(s);
} }
let status = cmd.status()?; let status = cmd.status()?;
@@ -575,7 +570,7 @@ impl EphemeralContextGuard {
fn extract_tarball( fn extract_tarball(
tarball_path: &PathBuf, tarball_path: &PathBuf,
chroot_path: &PathBuf, chroot_path: &PathBuf,
ui: Option<&DebUi>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Create the chroot directory // Create the chroot directory
fs::create_dir_all(chroot_path)?; fs::create_dir_all(chroot_path)?;
@@ -593,15 +588,11 @@ impl EphemeralContextGuard {
let mut entry = entry?; let mut entry = entry?;
entry.unpack_in(chroot_path)?; entry.unpack_in(chroot_path)?;
count += 1; count += 1;
if count.is_multiple_of(100) if count.is_multiple_of(100) {
&& let Some(u) = ui view.message(&format!("Extracting chroot… ({count} files)"));
{
u.progress_message(&format!("Extracting chroot… ({count} files)"));
} }
} }
if let Some(u) = ui { view.message(&format!("Extracting chroot… ({count} files)"));
u.progress_message(&format!("Extracting chroot… ({count} files)"));
}
Ok(()) Ok(())
} }
+32 -45
View File
@@ -1,9 +1,9 @@
/// Local binary package building /// Local binary package building
/// Directly calling 'debian/rules' in current context /// Directly calling 'debian/rules' in current context
use crate::context::{Context, ContextCommand, LineSink}; use crate::context::{Context, ContextCommand, LineSink};
use crate::deb::find_dsc_file; use crate::deb::{Phase, enter_phase, find_dsc_file};
use crate::logfmt::QuiltClassifier; use crate::logfmt::QuiltClassifier;
use crate::ui::deb::{DebUi, Phase}; use crate::report::BuildView;
use log::warn; use log::warn;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::error::Error; use std::error::Error;
@@ -33,13 +33,13 @@ pub async fn build(
pocket: Option<&str>, pocket: Option<&str>,
build_root: &str, build_root: &str,
cross: bool, cross: bool,
ppa: Option<&[&str]>, ppa: &[String],
inject_packages: Option<&[&str]>, inject_packages: &[String],
ctx: Arc<Context>, ctx: Arc<Context>,
ui: Option<Arc<DebUi>>, view: &dyn BuildView,
jobs: Option<usize>, jobs: Option<usize>,
) -> Result<Vec<PathBuf>, Box<dyn Error>> { ) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let sink: Option<Arc<dyn LineSink>> = ui.as_ref().map(|u| u.sink()); let sink: Option<Arc<dyn LineSink>> = view.sink();
// Environment // Environment
let mut env = HashMap::<String, String>::new(); let mut env = HashMap::<String, String>::new();
@@ -83,8 +83,7 @@ pub async fn build(
let mut added_ppas: Vec<(&str, &str)> = Vec::new(); let mut added_ppas: Vec<(&str, &str)> = Vec::new();
// Add PPA repositories if specified // Add PPA repositories if specified
if let Some(ppas) = ppa { for ppa_str in ppa {
for ppa_str in ppas {
// PPA format: user/ppa_name // PPA format: user/ppa_name
let parts: Vec<&str> = ppa_str.split('/').collect(); let parts: Vec<&str> = ppa_str.split('/').collect();
if parts.len() == 2 { if parts.len() == 2 {
@@ -133,7 +132,6 @@ pub async fn build(
); );
} }
} }
}
// UBUNTU: Ensure the 'universe' component is enabled on official // UBUNTU: Ensure the 'universe' component is enabled on official
// Ubuntu sources (many build dependencies live there). The old // Ubuntu sources (many build dependencies live there). The old
@@ -193,9 +191,7 @@ pub async fn build(
// Update package lists // Update package lists
log::debug!("Updating package lists for local build..."); log::debug!("Updating package lists for local build...");
if let Some(u) = &ui { enter_phase(view, Phase::UpdatingPackageLists);
u.phase(Phase::UpdatingPackageLists);
}
let status = cap( let status = cap(
ctx.command("apt-get").envs(env.clone()).arg("update"), ctx.command("apt-get").envs(env.clone()).arg("update"),
&sink, &sink,
@@ -234,9 +230,7 @@ pub async fn build(
cmd.arg(format!("libc6:{arch}")); cmd.arg(format!("libc6:{arch}"));
cmd.arg(format!("libc6-dev:{arch}")); cmd.arg(format!("libc6-dev:{arch}"));
} }
if let Some(u) = &ui { enter_phase(view, Phase::InstallingEssentials);
u.phase(Phase::InstallingEssentials);
}
let status = cap(&mut cmd, &sink).status()?; let status = cap(&mut cmd, &sink).status()?;
if !status.success() { if !status.success() {
return Err("Could not install essential packages for the build".into()); return Err("Could not install essential packages for the build".into());
@@ -261,18 +255,16 @@ pub async fn build(
} }
// Apply quilt patches if the package provides a patch series // Apply quilt patches if the package provides a patch series
apply_quilt_patches(package_dir_str, &env, ctx.clone(), &ui, &sink)?; apply_quilt_patches(package_dir_str, &env, ctx.clone(), view, &sink)?;
// Install injected packages if specified // Install injected packages if specified
if let Some(packages) = inject_packages { if !inject_packages.is_empty() {
install_injected_packages(packages, &env, ctx.clone(), &ui, &sink)?; install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
} }
// Install arch-specific build dependencies // Install arch-specific build dependencies
log::debug!("Installing arch-specific build dependencies..."); log::debug!("Installing arch-specific build dependencies...");
if let Some(u) = &ui { enter_phase(view, Phase::InstallingBuildDeps);
u.phase(Phase::InstallingBuildDeps);
}
let mut cmd = ctx.command("apt-get"); let mut cmd = ctx.command("apt-get");
cmd.current_dir(package_dir_str) cmd.current_dir(package_dir_str)
.envs(env.clone()) .envs(env.clone())
@@ -286,9 +278,7 @@ pub async fn build(
// If build-dep fails, we try to explain the failure using dose-debcheck // If build-dep fails, we try to explain the failure using dose-debcheck
if !status.success() { if !status.success() {
if let Some(u) = &ui { view.suspend();
u.suspend();
}
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
return Err("Could not install build-dependencies for the build".into()); return Err("Could not install build-dependencies for the build".into());
} }
@@ -320,9 +310,7 @@ pub async fn build(
// If build-dep fails, we try to explain the failure using dose-debcheck // If build-dep fails, we try to explain the failure using dose-debcheck
if !status.success() { if !status.success() {
if let Some(u) = &ui { view.suspend();
u.suspend();
}
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
return Err("Could not install build-dependencies for the build".into()); return Err("Could not install build-dependencies for the build".into());
} }
@@ -330,9 +318,7 @@ pub async fn build(
// Run the build step // Run the build step
log::debug!("Building (debian/rules build) package..."); log::debug!("Building (debian/rules build) package...");
if let Some(u) = &ui { enter_phase(view, Phase::Building);
u.phase(Phase::Building);
}
let status = cap( let status = cap(
ctx.command("debian/rules") ctx.command("debian/rules")
.current_dir(package_dir_str) .current_dir(package_dir_str)
@@ -346,9 +332,7 @@ pub async fn build(
} }
// Run the 'binary' step to produce deb // Run the 'binary' step to produce deb
if let Some(u) = &ui { enter_phase(view, Phase::ProducingBinaries);
u.phase(Phase::ProducingBinaries);
}
let status = cap( let status = cap(
ctx.command("fakeroot") ctx.command("fakeroot")
.current_dir(package_dir_str) .current_dir(package_dir_str)
@@ -495,7 +479,7 @@ fn apply_quilt_patches(
package_dir: &str, package_dir: &str,
env: &HashMap<String, String>, env: &HashMap<String, String>,
ctx: Arc<Context>, ctx: Arc<Context>,
ui: &Option<Arc<DebUi>>, view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>, sink: &Option<Arc<dyn LineSink>>,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let series_path = Path::new(package_dir).join("debian/patches/series"); let series_path = Path::new(package_dir).join("debian/patches/series");
@@ -555,12 +539,10 @@ fn apply_quilt_patches(
} }
// Apply all patches listed in the series // Apply all patches listed in the series
if let Some(u) = ui { view.phase(
u.phase_with( Phase::ApplyingPatches.label(),
Phase::ApplyingPatches,
Box::new(QuiltClassifier::new(total_patches)), Box::new(QuiltClassifier::new(total_patches)),
); );
}
let mut patch_env = env.clone(); let mut patch_env = env.clone();
patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string()); patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string());
let status = cap( let status = cap(
@@ -631,17 +613,15 @@ fn pin_pocket(pocket_suite: &str, ctx: &Arc<Context>) -> Result<(), Box<dyn Erro
} }
fn install_injected_packages( fn install_injected_packages(
packages: &[&str], packages: &[String],
env: &HashMap<String, String>, env: &HashMap<String, String>,
ctx: Arc<Context>, ctx: Arc<Context>,
ui: &Option<Arc<DebUi>>, view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>, sink: &Option<Arc<dyn LineSink>>,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
log::info!("Installing injected packages: {:?}", packages); log::info!("Installing injected packages: {:?}", packages);
if let Some(u) = ui { enter_phase(view, Phase::InjectingPackages);
u.phase(Phase::InjectingPackages);
}
// Separate .deb files from package names // Separate .deb files from package names
let mut deb_files: Vec<String> = Vec::new(); let mut deb_files: Vec<String> = Vec::new();
@@ -661,7 +641,7 @@ fn install_injected_packages(
); );
deb_files.push(chroot_path.to_string_lossy().to_string()); deb_files.push(chroot_path.to_string_lossy().to_string());
} else { } else {
package_names.push(pkg); package_names.push(pkg.as_str());
} }
} }
@@ -855,6 +835,13 @@ mod tests {
) )
.unwrap(); .unwrap();
apply_quilt_patches(tree.to_str().unwrap(), &HashMap::new(), ctx, &None, &None).unwrap(); apply_quilt_patches(
tree.to_str().unwrap(),
&HashMap::new(),
ctx,
&crate::report::Quiet,
&None,
)
.unwrap();
} }
} }
+183 -102
View File
@@ -5,7 +5,11 @@ pub(crate) mod ephemeral;
mod local; mod local;
use crate::context::{self, Context}; use crate::context::{self, Context};
use crate::ui::deb::{DebUi, Phase}; use crate::logfmt::{
AptInstallClassifier, AptUpdateClassifier, Classifier, GenericClassifier, MakeClassifier,
MmdebstrapClassifier, QuiltClassifier,
};
use crate::report::{BuildTarget, BuildView};
use std::error::Error; use std::error::Error;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@@ -17,67 +21,163 @@ pub enum BuildMode {
Local, Local,
} }
/// Phases of a binary build, announced to the [`BuildView`]
#[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 line classifier rewriting a phase's subprocess output
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()),
}
}
/// Enter `phase` on the view with its default line classifier
pub(crate) fn enter_phase(view: &dyn BuildView, phase: Phase) {
view.phase(phase.label(), default_classifier(phase));
}
/// Parameters of one [`build_binary_package`] call.
pub struct DebBuildOptions<'a> {
/// Target architecture; defaults to the host architecture.
pub arch: Option<String>,
/// Target distribution series; defaults to the changelog series
/// (UNRELEASED resolves to the vendor's development series).
pub series: Option<String>,
/// Distribution pocket to resolve build-dependencies from.
pub pocket: Option<String>,
/// Source tree to build; defaults to the process working directory.
pub cwd: Option<PathBuf>,
/// Cross-compile for the target architecture instead of using
/// qemu-binfmt.
pub cross: bool,
/// Build mode; defaults to [`BuildMode::Local`].
pub mode: Option<BuildMode>,
/// PPAs to add for build-dependencies (`user/ppa_name`).
pub ppa: Vec<String>,
/// Packages to inject into the build environment before build-dep
/// (.deb paths, archive names or PPA packages).
pub inject: Vec<String>,
/// Parallel build jobs; defaults to the core count available in the
/// build context.
pub jobs: Option<usize>,
/// Explicit build context; defaults to the current context.
pub ctx: Option<Arc<Context>>,
/// Where build events (phases, progress, outcome) are reported.
pub view: &'a dyn BuildView,
}
impl Default for DebBuildOptions<'_> {
fn default() -> Self {
static QUIET: crate::report::Quiet = crate::report::Quiet;
DebBuildOptions {
arch: None,
series: None,
pocket: None,
cwd: None,
cross: false,
mode: None,
ppa: Vec::new(),
inject: Vec::new(),
jobs: None,
ctx: None,
view: &QUIET,
}
}
}
/// Build package in 'cwd' to a .deb /// Build package in 'cwd' to a .deb
/// ///
/// Returns the list of produced artifacts (.deb files plus the upload /// Returns the list of produced artifacts (.deb files plus the upload
/// metadata `.buildinfo`/`.changes`) retrieved locally, identified from /// metadata `.buildinfo`/`.changes`) retrieved locally, identified from
/// `debian/files` and the native metadata generation rather than by /// `debian/files` and the native metadata generation rather than by
/// globbing the build root (which would surface stale files). When `ui` is /// globbing the build root (which would surface stale files). Subprocess
/// set, a live view (status bar + rolling log pane) is displayed and all /// output is captured through the view's sink (live view + tee log for the
/// subprocess output is captured through it; on failure the widget is /// terminal adapter); on failure the view is cleared and prints a summary
/// cleared and a summary of captured errors is printed. /// of captured errors.
#[allow(clippy::too_many_arguments)]
pub async fn build_binary_package( pub async fn build_binary_package(
arch: Option<&str>, opts: DebBuildOptions<'_>,
series: Option<&str>,
pocket: Option<&str>,
cwd: Option<&Path>,
cross: bool,
mode: Option<BuildMode>,
ppa: Option<&[&str]>,
inject_packages: Option<&[&str]>,
ctx: Option<Arc<Context>>,
ui: Option<Arc<DebUi>>,
jobs: Option<usize>,
) -> Result<Vec<PathBuf>, Box<dyn Error>> { ) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let result = build_binary_package_impl( let view = opts.view;
arch, let result = build_binary_package_impl(opts).await;
series,
pocket,
cwd,
cross,
mode,
ppa,
inject_packages,
ctx,
&ui,
jobs,
)
.await;
if let (Some(u), Err(_)) = (&ui, &result) { if result.is_err() {
u.finish_failure(); view.finish_failure();
} }
result result
} }
/// Implementation of [`build_binary_package`], without failure handling /// Implementation of [`build_binary_package`], without failure handling
#[allow(clippy::too_many_arguments)]
async fn build_binary_package_impl( async fn build_binary_package_impl(
arch: Option<&str>, opts: DebBuildOptions<'_>,
series: Option<&str>,
pocket: Option<&str>,
cwd: Option<&Path>,
cross: bool,
mode: Option<BuildMode>,
ppa: Option<&[&str]>,
inject_packages: Option<&[&str]>,
ctx: Option<Arc<Context>>,
ui: &Option<Arc<DebUi>>,
jobs: Option<usize>,
) -> Result<Vec<PathBuf>, Box<dyn Error>> { ) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let cwd = cwd.unwrap_or_else(|| Path::new(".")); let DebBuildOptions {
ref arch,
ref series,
ref pocket,
ref cwd,
cross,
ref mode,
ref ppa,
ref inject,
ref jobs,
ref ctx,
view,
} = opts;
let cwd = cwd.as_deref().unwrap_or_else(|| Path::new("."));
// Parse changelog to get package name, version and series // Parse changelog to get package name, version and series
let changelog_path = cwd.join("debian/changelog"); let changelog_path = cwd.join("debian/changelog");
@@ -101,44 +201,43 @@ async fn build_binary_package_impl(
&package_series &package_series
}; };
let current_arch = crate::get_current_arch(); let current_arch = crate::get_current_arch();
let arch = arch.unwrap_or(&current_arch); let arch = arch.as_deref().unwrap_or(&current_arch);
// Make sure we select a specific mode, either using user-requested // Make sure we select a specific mode, either using user-requested
// or by using default for user-supplied parameters // or by using default for user-supplied parameters
let mode = if let Some(m) = mode { let default_mode = BuildMode::Local;
m let mode = mode.as_ref().unwrap_or(&default_mode);
} else {
// By default, we use local build
BuildMode::Local
};
// Create an ephemeral unshare context for all Local builds // Create an ephemeral unshare context for all Local builds
// Use qemu_binfmt when target architecture differs from host and cross is not requested // Use qemu_binfmt when target architecture differs from host and cross is not requested
let chroot_arch = if mode == BuildMode::Local && arch != current_arch && !cross { let chroot_arch = if mode == &BuildMode::Local && arch != current_arch && !cross {
Some(arch) Some(arch)
} else { } else {
None None
}; };
// Use provided context or get current // Use provided context or get current
let base_ctx = ctx.unwrap_or_else(context::current); let base_ctx = ctx.clone().unwrap_or_else(context::current);
// Identify the target in the live UI once the changelog is parsed, so // Identify the target in the live view once the changelog is parsed, so
// even the chroot download output is attributed and tee'd // even the chroot download output is attributed and tee'd
if let Some(u) = ui { view.target(BuildTarget {
u.set_target(&package, &version, series, arch); package: &package,
} version: &version,
target: &format!("{series}/{arch}"),
source_only: false,
});
// Create an ephemeral unshare context for all Local builds. It is kept in // Create an ephemeral unshare context for all Local builds. It is kept in
// this scope so it outlives the guarded section below and is only dropped // this scope so it outlives the guarded section below and is only dropped
// once the live view has been cleared. // once the live view has been cleared.
let mut guard = if mode == BuildMode::Local { let mut guard = if *mode == BuildMode::Local {
Some( Some(
ephemeral::EphemeralContextGuard::new_with_context( ephemeral::EphemeralContextGuard::new_with_context(
series, series,
chroot_arch, chroot_arch,
base_ctx.clone(), base_ctx.clone(),
ui.clone(), view,
) )
.await?, .await?,
) )
@@ -179,14 +278,14 @@ async fn build_binary_package_impl(
&version, &version,
arch, arch,
series, series,
pocket, pocket.as_deref(),
&build_root, &build_root,
cross, cross,
ppa, ppa,
inject_packages, inject,
build_ctx.clone(), build_ctx.clone(),
ui.clone(), view,
jobs, *jobs,
) )
.await? .await?
} }
@@ -194,9 +293,7 @@ async fn build_binary_package_impl(
// Retrieve the produced artifacts (binary packages plus the upload // Retrieve the produced artifacts (binary packages plus the upload
// metadata) to the parent directory. // metadata) to the parent directory.
if let Some(u) = ui { enter_phase(view, Phase::RetrievingArtifacts);
u.phase(Phase::RetrievingArtifacts);
}
let total_debs = remote_files.len(); let total_debs = remote_files.len();
let mut artifacts = Vec::with_capacity(total_debs); let mut artifacts = Vec::with_capacity(total_debs);
@@ -206,14 +303,10 @@ async fn build_binary_package_impl(
build_ctx.retrieve_path(remote_file, &local_dest)?; build_ctx.retrieve_path(remote_file, &local_dest)?;
artifacts.push(local_dest); artifacts.push(local_dest);
if let Some(u) = ui { view.progress("Retrieving artifacts", idx + 1, total_debs);
u.count_progress("Retrieving artifacts", idx + 1, total_debs);
}
} }
if let Some(u) = ui { view.finish_success(&artifacts);
u.finish_success(&artifacts, u.elapsed());
}
Ok(artifacts) Ok(artifacts)
} }
@@ -222,9 +315,7 @@ async fn build_binary_package_impl(
// Clear the live view before returning: the ephemeral guard is dropped at // Clear the live view before returning: the ephemeral guard is dropped at
// the end of this function and its cleanup commands (umount, rm -rf of // the end of this function and its cleanup commands (umount, rm -rf of
// the chroot) inherit the terminal, so they must not fight the widget. // the chroot) inherit the terminal, so they must not fight the widget.
if let Some(u) = ui { view.suspend();
u.suspend();
}
// Mark build as successful to trigger chroot cleanup // Mark build as successful to trigger chroot cleanup
if result.is_ok() if result.is_ok()
@@ -446,19 +537,14 @@ mod tests {
log::debug!("Package directory: {}", cwd.display()); log::debug!("Package directory: {}", cwd.display());
log::info!("Starting binary package build..."); log::info!("Starting binary package build...");
crate::deb::build_binary_package( crate::deb::build_binary_package(DebBuildOptions {
arch, arch: arch.map(str::to_string),
Some(series), series: Some(series.to_string()),
None,
Some(&cwd),
cross, cross,
None, cwd: Some(cwd.to_path_buf()),
None, ctx: Some(ctx),
None, ..Default::default()
Some(ctx), })
None,
None,
)
.await .await
.expect("Cannot build binary package (deb)"); .expect("Cannot build binary package (deb)");
log::info!("Successfully built binary package"); log::info!("Successfully built binary package");
@@ -620,19 +706,14 @@ mod tests {
let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap()); let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap());
crate::deb::build_binary_package( crate::deb::build_binary_package(DebBuildOptions {
Some("arm64"), arch: Some("arm64".to_string()),
Some("noble"), series: Some("noble".to_string()),
None, cwd: Some(pkg_dir),
Some(&pkg_dir), cross: true,
true, ctx: Some(ctx),
None, ..Default::default()
None, })
None,
Some(ctx),
None,
None,
)
.await .await
.expect("Cannot cross-build package declaring Build-Depends-Indep"); .expect("Cannot cross-build package declaring Build-Depends-Indep");
+24 -36
View File
@@ -573,28 +573,21 @@ fn main() {
} }
Some(("deb", sub_matches)) => { Some(("deb", sub_matches)) => {
let cwd = current_dir_or_exit(); let cwd = current_dir_or_exit();
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str()); let series = sub_matches.get_one::<String>("series").cloned();
let pocket = sub_matches.get_one::<String>("pocket").map(|s| s.as_str()); let pocket = sub_matches.get_one::<String>("pocket").cloned();
let arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str()); let arch = sub_matches.get_one::<String>("arch").cloned();
let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false); let cross = sub_matches
let ppa: Vec<&str> = sub_matches .get_one::<bool>("cross")
.copied()
.unwrap_or(false);
let ppa: Vec<String> = sub_matches
.get_many::<String>("ppa") .get_many::<String>("ppa")
.map(|v| v.map(|s| s.as_str()).collect()) .map(|v| v.cloned().collect())
.unwrap_or_default(); .unwrap_or_default();
let ppa = if ppa.is_empty() { let inject: Vec<String> = sub_matches
None
} else {
Some(ppa.as_slice())
};
let inject_packages: Vec<&str> = sub_matches
.get_many::<String>("inject") .get_many::<String>("inject")
.map(|v| v.map(|s| s.as_str()).collect()) .map(|v| v.cloned().collect())
.unwrap_or_default(); .unwrap_or_default();
let inject_packages = if inject_packages.is_empty() {
None
} else {
Some(inject_packages.as_slice())
};
let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str()); let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str());
let mode: Option<pkh::deb::BuildMode> = match mode { let mode: Option<pkh::deb::BuildMode> = match mode {
Some("local") => Some(pkh::deb::BuildMode::Local), Some("local") => Some(pkh::deb::BuildMode::Local),
@@ -613,36 +606,31 @@ fn main() {
}) })
}); });
// Live build view: disabled by --verbose or when stdout is not a // Live build view, unless --verbose (DebUi additionally disables
// terminal (DebUi handles the non-TTY case itself) // itself when stdout is not a terminal)
let ui = if verbose { let quiet = pkh::report::Quiet;
None let live = pkh::ui::deb::DebUi::new(&multi);
} else { let view: &dyn pkh::report::BuildView = if verbose { &quiet } else { &live };
Some(std::sync::Arc::new(pkh::ui::deb::DebUi::new(&multi)))
};
let result = rt.block_on(async { let result = rt.block_on(async {
pkh::deb::build_binary_package( pkh::deb::build_binary_package(pkh::deb::DebBuildOptions {
arch, arch,
series, series,
pocket, pocket,
Some(cwd.as_path()), cwd: Some(cwd.clone()),
*cross, cross,
mode, mode,
ppa, ppa,
inject_packages, inject,
None,
ui.clone(),
jobs, jobs,
) view,
..Default::default()
})
.await .await
}); });
match result { match result {
Ok(artifacts) => { Ok(_) => info!("Done."),
let _ = artifacts;
info!("Done.");
}
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);
std::process::exit(1); std::process::exit(1);
+7 -14
View File
@@ -624,20 +624,13 @@ pub async fn offer_verification(
return; return;
} }
let ui = Some(std::sync::Arc::new(crate::ui::deb::DebUi::new(multi))); let view = crate::ui::deb::DebUi::new(multi);
if let Err(e) = crate::deb::build_binary_package( if let Err(e) = crate::deb::build_binary_package(crate::deb::DebBuildOptions {
None, series: Some(opts.series.clone()),
Some(&opts.series), cwd: Some(tree.clone()),
None, view: &view,
Some(&tree), ..Default::default()
false, })
None,
None,
None,
None,
ui,
None,
)
.await .await
{ {
log::error!("Verification binary build failed: {e}"); log::error!("Verification binary build failed: {e}");
+29 -6
View File
@@ -21,16 +21,33 @@ use std::sync::Arc;
use crate::context::LineSink; use crate::context::LineSink;
use crate::logfmt::Classifier; use crate::logfmt::Classifier;
/// Identity of the build whose events follow, as announced through
/// [`BuildView::target`].
#[derive(Debug, Clone, Copy)]
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`).
pub target: &'a str,
/// Whether this is a source-only build (producing a `.dsc`) as opposed
/// to a binary build (producing `.deb` files).
pub source_only: bool,
}
/// Observer of a running build: target identification, phases, status /// Observer of a running build: target identification, phases, status
/// messages, progress and the final outcome. /// messages, progress and the final outcome.
/// ///
/// Implement this to observe [`crate::build`] flows from any frontend. All /// Implement this to observe the [`crate::build`] and [`crate::deb`] flows
/// events arrive in order from the build thread; long-lived views are /// from any frontend. All events arrive in order from the build thread;
/// expected to be `Send + Sync` because builds may run inside async tasks. /// long-lived views are expected to be `Send + Sync` because builds may run
/// inside async tasks.
pub trait BuildView: Send + Sync { pub trait BuildView: Send + Sync {
/// The build target was identified: `package` at `version`, built for /// The build target was identified; the events that follow belong to it
/// `target` (a distribution series, optionally with an architecture). /// (including the earliest subprocess output, e.g. a chroot download).
fn target(&self, _package: &str, _version: &str, _target: &str) {} fn target(&self, _target: BuildTarget) {}
/// A named phase started (e.g. "Applying patches"). `classifier` /// A named phase started (e.g. "Applying patches"). `classifier`
/// rewrites the phase's raw subprocess lines (see [`crate::logfmt`]) /// rewrites the phase's raw subprocess lines (see [`crate::logfmt`])
@@ -63,6 +80,12 @@ pub trait BuildView: Send + Sync {
/// diagnostics it collected through [`BuildView::sink`]. /// diagnostics it collected through [`BuildView::sink`].
fn finish_failure(&self) {} 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; /// Whether this view presents build results to the user by itself;
/// callers use this to fall back to plain-line rendering when it does /// callers use this to fall back to plain-line rendering when it does
/// not (headless views, verbose mode). /// not (headless views, verbose mode).
+48 -154
View File
@@ -21,10 +21,8 @@ use directories::ProjectDirs;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use crate::context::{LineSink, Stream}; use crate::context::{LineSink, Stream};
use crate::logfmt::{ use crate::logfmt::{Action, Classifier, GenericClassifier};
Action, AptInstallClassifier, AptUpdateClassifier, Classifier, GenericClassifier, use crate::report::BuildTarget;
MakeClassifier, MmdebstrapClassifier, QuiltClassifier,
};
/// Number of lines displayed in the rolling pane /// Number of lines displayed in the rolling pane
const PANE_LINES: usize = 10; const PANE_LINES: usize = 10;
@@ -32,69 +30,6 @@ const PANE_LINES: usize = 10;
/// Minimum interval between pane redraws /// Minimum interval between pane redraws
const REDRAW_INTERVAL: Duration = Duration::from_millis(50); 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 /// Visual kind of a pane line, driving its color
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
enum Kind { enum Kind {
@@ -129,11 +64,9 @@ struct Shared {
/// Live build view for `pkh deb` / `pkh build` /// Live build view for `pkh deb` / `pkh build`
/// ///
/// Create one per build (disabled automatically when stdout is not a TTY or /// Create one per build (it disables itself automatically when stdout is not
/// when the user requests verbose output), then either pass it down through /// a TTY) and pass it down through the [`crate::report::BuildView`] port.
/// the [`crate::report::BuildView`] port (source builds) or as /// Subprocess output reaches it through [`crate::report::BuildView::sink`].
/// `Option<Arc<DebUi>>` (the `pkh deb` flow, until it migrates), and feed
/// subprocess output through [`DebUi::sink`].
pub struct DebUi { pub struct DebUi {
shared: Arc<Shared>, shared: Arc<Shared>,
} }
@@ -252,20 +185,7 @@ impl DebUi {
} }
} }
/// Switch to a phase, installing its default classifier /// Switch to an arbitrary status label with a custom 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
fn phase_custom(&self, label: &str, classifier: Box<dyn Classifier>) { fn phase_custom(&self, label: &str, classifier: Box<dyn Classifier>) {
{ {
let mut st = self.shared.state.lock().unwrap(); 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 /// Whether the widget is enabled and still drawn
fn active(&self) -> bool { fn active(&self) -> bool {
self.shared.enabled && !self.shared.suspended.load(Ordering::SeqCst) self.shared.enabled && !self.shared.suspended.load(Ordering::SeqCst)
} }
/// Obtain a sink feeding this view; pass it to `ContextCommand::capture` /// Release the widget from the terminal (e.g. before printing
pub fn sink(self: &Arc<Self>) -> Arc<dyn LineSink> { /// passthrough diagnostics or letting child cleanup commands write to
Arc::new(Sink { /// the terminal); idempotent
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
/// ///
/// Steady ticks are disabled first: otherwise a tick can redraw a frame /// Steady ticks are disabled first: otherwise a tick can redraw a frame
/// right after the clear, leaving stale copies of the widget on screen. /// right after the clear, leaving stale copies of the widget on screen.
pub fn suspend(&self) { fn suspend(&self) {
if !self.shared.enabled { if !self.shared.enabled {
return; return;
} }
@@ -334,20 +225,8 @@ impl DebUi {
self.shared.pane.finish_and_clear(); 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 /// 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) { fn success_summary(&self, artifacts: &[PathBuf], elapsed: Duration) {
self.suspend(); self.suspend();
if self.shared.enabled && !artifacts.is_empty() { if self.shared.enabled && !artifacts.is_empty() {
@@ -358,8 +237,8 @@ impl DebUi {
} }
} }
/// Shared body of the failure outcome ([`DebUi::finish_failure`] and the /// Failure outcome body: clear the widget and print a summary (recent
/// [`BuildView`] port) /// captured errors and the path to the full log)
fn failure_summary(&self) { fn failure_summary(&self) {
self.suspend(); self.suspend();
@@ -383,31 +262,31 @@ impl DebUi {
eprintln!("Full log: {}", log_path.display()); 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 /// [`crate::report::BuildView`] port: forwards build events to the live
/// widget, so core flows can drive the view without knowing it is a /// widget, so core flows drive the view without knowing it is a terminal
/// terminal widget. Where the inherent methods keep their richer signatures /// widget.
/// (e.g. [`DebUi::finish_success`] takes the elapsed duration explicitly for
/// the `pkh deb` flow), the port bridges them.
impl crate::report::BuildView for DebUi { 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 { if self.shared.enabled {
self.shared.top.set_prefix(format!( 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>) { fn phase(&self, name: &str, classifier: Box<dyn Classifier>) {
@@ -415,11 +294,22 @@ impl crate::report::BuildView for DebUi {
} }
fn message(&self, text: &str) { 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) { 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>> { fn sink(&self) -> Option<Arc<dyn LineSink>> {
@@ -429,13 +319,17 @@ impl crate::report::BuildView for DebUi {
} }
fn finish_success(&self, artifacts: &[PathBuf]) { fn finish_success(&self, artifacts: &[PathBuf]) {
self.success_summary(artifacts, self.elapsed()); self.success_summary(artifacts, self.shared.started.elapsed());
} }
fn finish_failure(&self) { fn finish_failure(&self) {
self.failure_summary(); self.failure_summary();
} }
fn suspend(&self) {
DebUi::suspend(self);
}
fn is_enabled(&self) -> bool { fn is_enabled(&self) -> bool {
self.shared.enabled self.shared.enabled
} }