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,
};
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`),
/// mirroring the `dpkg-genchanges` source styles.
@@ -294,7 +294,12 @@ pub fn run_source_build(
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();
+17 -26
View File
@@ -1,5 +1,6 @@
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 std::any::Any;
use std::error::Error;
@@ -312,7 +313,7 @@ impl EphemeralContextGuard {
series: &str,
arch: Option<&str>,
base_ctx: Arc<Context>,
ui: Option<Arc<DebUi>>,
view: &dyn BuildView,
) -> Result<Self, Box<dyn Error>> {
// Save the globally-installed context so Drop can restore exactly
// this handle: concurrent builds install their own ephemeral
@@ -355,7 +356,7 @@ impl EphemeralContextGuard {
// Download and extract the chroot tarball
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
{
// The guard (and its Drop) never materializes on this path, so
@@ -408,7 +409,7 @@ impl EphemeralContextGuard {
arch: Option<&str>,
chroot_path: &PathBuf,
ctx: Arc<context::Context>,
ui: &Option<Arc<DebUi>>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
// Clone ctx for use in create_device_nodes after download_chroot_tarball consumes it
let ctx_for_devices = ctx.clone();
@@ -464,10 +465,8 @@ impl EphemeralContextGuard {
series,
arch
);
if let Some(u) = ui {
u.phase(Phase::PreparingChroot);
}
Self::download_chroot_tarball(series, arch, &tarball_path, ctx, ui).await?;
enter_phase(view, Phase::PreparingChroot);
Self::download_chroot_tarball(series, arch, &tarball_path, ctx, view).await?;
} else {
log::debug!(
"Using cached chroot tarball for {} (arch: {:?})",
@@ -478,16 +477,12 @@ impl EphemeralContextGuard {
// Extract tarball to chroot directory
log::debug!("Extracting chroot tarball to {}...", chroot_path.display());
if let Some(u) = ui {
u.phase(Phase::ExtractingChroot);
}
Self::extract_tarball(&tarball_path, chroot_path, ui.as_deref())?;
enter_phase(view, Phase::ExtractingChroot);
Self::extract_tarball(&tarball_path, chroot_path, view)?;
// Create device nodes in the chroot
log::debug!("Creating device nodes in chroot...");
if let Some(u) = ui {
u.phase(Phase::FinalizingChroot);
}
enter_phase(view, Phase::FinalizingChroot);
Self::create_device_nodes(chroot_path, ctx_for_devices.clone())?;
// Bind mount /proc from host into chroot (before entering unshare namespace)
@@ -503,7 +498,7 @@ impl EphemeralContextGuard {
arch: Option<&str>,
tarball_path: &Path,
ctx: Arc<context::Context>,
ui: &Option<Arc<DebUi>>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
// 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");
@@ -537,8 +532,8 @@ impl EphemeralContextGuard {
cmd.arg(series)
.arg(tarball_path.to_string_lossy().to_string());
if let Some(u) = ui {
cmd.capture(u.sink());
if let Some(s) = view.sink() {
cmd.capture(s);
}
let status = cmd.status()?;
@@ -575,7 +570,7 @@ impl EphemeralContextGuard {
fn extract_tarball(
tarball_path: &PathBuf,
chroot_path: &PathBuf,
ui: Option<&DebUi>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
// Create the chroot directory
fs::create_dir_all(chroot_path)?;
@@ -593,15 +588,11 @@ impl EphemeralContextGuard {
let mut entry = entry?;
entry.unpack_in(chroot_path)?;
count += 1;
if count.is_multiple_of(100)
&& let Some(u) = ui
{
u.progress_message(&format!("Extracting chroot… ({count} files)"));
if count.is_multiple_of(100) {
view.message(&format!("Extracting chroot… ({count} files)"));
}
}
if let Some(u) = ui {
u.progress_message(&format!("Extracting chroot… ({count} files)"));
}
view.message(&format!("Extracting chroot… ({count} files)"));
Ok(())
}
+32 -45
View File
@@ -1,9 +1,9 @@
/// Local binary package building
/// Directly calling 'debian/rules' in current context
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::ui::deb::{DebUi, Phase};
use crate::report::BuildView;
use log::warn;
use std::collections::{BTreeMap, HashMap};
use std::error::Error;
@@ -33,13 +33,13 @@ pub async fn build(
pocket: Option<&str>,
build_root: &str,
cross: bool,
ppa: Option<&[&str]>,
inject_packages: Option<&[&str]>,
ppa: &[String],
inject_packages: &[String],
ctx: Arc<Context>,
ui: Option<Arc<DebUi>>,
view: &dyn BuildView,
jobs: Option<usize>,
) -> 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
let mut env = HashMap::<String, String>::new();
@@ -83,8 +83,7 @@ pub async fn build(
let mut added_ppas: Vec<(&str, &str)> = Vec::new();
// Add PPA repositories if specified
if let Some(ppas) = ppa {
for ppa_str in ppas {
for ppa_str in ppa {
// PPA format: user/ppa_name
let parts: Vec<&str> = ppa_str.split('/').collect();
if parts.len() == 2 {
@@ -133,7 +132,6 @@ pub async fn build(
);
}
}
}
// UBUNTU: Ensure the 'universe' component is enabled on official
// Ubuntu sources (many build dependencies live there). The old
@@ -193,9 +191,7 @@ pub async fn build(
// Update package lists
log::debug!("Updating package lists for local build...");
if let Some(u) = &ui {
u.phase(Phase::UpdatingPackageLists);
}
enter_phase(view, Phase::UpdatingPackageLists);
let status = cap(
ctx.command("apt-get").envs(env.clone()).arg("update"),
&sink,
@@ -234,9 +230,7 @@ pub async fn build(
cmd.arg(format!("libc6:{arch}"));
cmd.arg(format!("libc6-dev:{arch}"));
}
if let Some(u) = &ui {
u.phase(Phase::InstallingEssentials);
}
enter_phase(view, Phase::InstallingEssentials);
let status = cap(&mut cmd, &sink).status()?;
if !status.success() {
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(package_dir_str, &env, ctx.clone(), &ui, &sink)?;
apply_quilt_patches(package_dir_str, &env, ctx.clone(), view, &sink)?;
// Install injected packages if specified
if let Some(packages) = inject_packages {
install_injected_packages(packages, &env, ctx.clone(), &ui, &sink)?;
if !inject_packages.is_empty() {
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
}
// Install arch-specific build dependencies
log::debug!("Installing arch-specific build dependencies...");
if let Some(u) = &ui {
u.phase(Phase::InstallingBuildDeps);
}
enter_phase(view, Phase::InstallingBuildDeps);
let mut cmd = ctx.command("apt-get");
cmd.current_dir(package_dir_str)
.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 !status.success() {
if let Some(u) = &ui {
u.suspend();
}
view.suspend();
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
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 !status.success() {
if let Some(u) = &ui {
u.suspend();
}
view.suspend();
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
return Err("Could not install build-dependencies for the build".into());
}
@@ -330,9 +318,7 @@ pub async fn build(
// Run the build step
log::debug!("Building (debian/rules build) package...");
if let Some(u) = &ui {
u.phase(Phase::Building);
}
enter_phase(view, Phase::Building);
let status = cap(
ctx.command("debian/rules")
.current_dir(package_dir_str)
@@ -346,9 +332,7 @@ pub async fn build(
}
// Run the 'binary' step to produce deb
if let Some(u) = &ui {
u.phase(Phase::ProducingBinaries);
}
enter_phase(view, Phase::ProducingBinaries);
let status = cap(
ctx.command("fakeroot")
.current_dir(package_dir_str)
@@ -495,7 +479,7 @@ fn apply_quilt_patches(
package_dir: &str,
env: &HashMap<String, String>,
ctx: Arc<Context>,
ui: &Option<Arc<DebUi>>,
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
) -> Result<(), Box<dyn Error>> {
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
if let Some(u) = ui {
u.phase_with(
Phase::ApplyingPatches,
view.phase(
Phase::ApplyingPatches.label(),
Box::new(QuiltClassifier::new(total_patches)),
);
}
let mut patch_env = env.clone();
patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string());
let status = cap(
@@ -631,17 +613,15 @@ fn pin_pocket(pocket_suite: &str, ctx: &Arc<Context>) -> Result<(), Box<dyn Erro
}
fn install_injected_packages(
packages: &[&str],
packages: &[String],
env: &HashMap<String, String>,
ctx: Arc<Context>,
ui: &Option<Arc<DebUi>>,
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
) -> Result<(), Box<dyn Error>> {
log::info!("Installing injected packages: {:?}", packages);
if let Some(u) = ui {
u.phase(Phase::InjectingPackages);
}
enter_phase(view, Phase::InjectingPackages);
// Separate .deb files from package names
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());
} else {
package_names.push(pkg);
package_names.push(pkg.as_str());
}
}
@@ -855,6 +835,13 @@ mod tests {
)
.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;
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::path::{Path, PathBuf};
use std::sync::Arc;
@@ -17,67 +21,163 @@ pub enum BuildMode {
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
///
/// Returns the list of produced artifacts (.deb files plus the upload
/// metadata `.buildinfo`/`.changes`) retrieved locally, identified from
/// `debian/files` and the native metadata generation rather than by
/// globbing the build root (which would surface stale files). When `ui` is
/// set, a live view (status bar + rolling log pane) is displayed and all
/// subprocess output is captured through it; on failure the widget is
/// cleared and a summary of captured errors is printed.
#[allow(clippy::too_many_arguments)]
/// globbing the build root (which would surface stale files). Subprocess
/// output is captured through the view's sink (live view + tee log for the
/// terminal adapter); on failure the view is cleared and prints a summary
/// of captured errors.
pub async fn build_binary_package(
arch: Option<&str>,
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>,
opts: DebBuildOptions<'_>,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let result = build_binary_package_impl(
arch,
series,
pocket,
cwd,
cross,
mode,
ppa,
inject_packages,
ctx,
&ui,
jobs,
)
.await;
let view = opts.view;
let result = build_binary_package_impl(opts).await;
if let (Some(u), Err(_)) = (&ui, &result) {
u.finish_failure();
if result.is_err() {
view.finish_failure();
}
result
}
/// Implementation of [`build_binary_package`], without failure handling
#[allow(clippy::too_many_arguments)]
async fn build_binary_package_impl(
arch: Option<&str>,
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>,
opts: DebBuildOptions<'_>,
) -> 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
let changelog_path = cwd.join("debian/changelog");
@@ -101,44 +201,43 @@ async fn build_binary_package_impl(
&package_series
};
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
// or by using default for user-supplied parameters
let mode = if let Some(m) = mode {
m
} else {
// By default, we use local build
BuildMode::Local
};
let default_mode = BuildMode::Local;
let mode = mode.as_ref().unwrap_or(&default_mode);
// Create an ephemeral unshare context for all Local builds
// 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)
} else {
None
};
// 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
if let Some(u) = ui {
u.set_target(&package, &version, series, arch);
}
view.target(BuildTarget {
package: &package,
version: &version,
target: &format!("{series}/{arch}"),
source_only: false,
});
// 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
// once the live view has been cleared.
let mut guard = if mode == BuildMode::Local {
let mut guard = if *mode == BuildMode::Local {
Some(
ephemeral::EphemeralContextGuard::new_with_context(
series,
chroot_arch,
base_ctx.clone(),
ui.clone(),
view,
)
.await?,
)
@@ -179,14 +278,14 @@ async fn build_binary_package_impl(
&version,
arch,
series,
pocket,
pocket.as_deref(),
&build_root,
cross,
ppa,
inject_packages,
inject,
build_ctx.clone(),
ui.clone(),
jobs,
view,
*jobs,
)
.await?
}
@@ -194,9 +293,7 @@ async fn build_binary_package_impl(
// Retrieve the produced artifacts (binary packages plus the upload
// metadata) to the parent directory.
if let Some(u) = ui {
u.phase(Phase::RetrievingArtifacts);
}
enter_phase(view, Phase::RetrievingArtifacts);
let total_debs = remote_files.len();
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)?;
artifacts.push(local_dest);
if let Some(u) = ui {
u.count_progress("Retrieving artifacts", idx + 1, total_debs);
}
view.progress("Retrieving artifacts", idx + 1, total_debs);
}
if let Some(u) = ui {
u.finish_success(&artifacts, u.elapsed());
}
view.finish_success(&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
// 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.
if let Some(u) = ui {
u.suspend();
}
view.suspend();
// Mark build as successful to trigger chroot cleanup
if result.is_ok()
@@ -446,19 +537,14 @@ mod tests {
log::debug!("Package directory: {}", cwd.display());
log::info!("Starting binary package build...");
crate::deb::build_binary_package(
arch,
Some(series),
None,
Some(&cwd),
crate::deb::build_binary_package(DebBuildOptions {
arch: arch.map(str::to_string),
series: Some(series.to_string()),
cross,
None,
None,
None,
Some(ctx),
None,
None,
)
cwd: Some(cwd.to_path_buf()),
ctx: Some(ctx),
..Default::default()
})
.await
.expect("Cannot build binary package (deb)");
log::info!("Successfully built binary package");
@@ -620,19 +706,14 @@ mod tests {
let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap());
crate::deb::build_binary_package(
Some("arm64"),
Some("noble"),
None,
Some(&pkg_dir),
true,
None,
None,
None,
Some(ctx),
None,
None,
)
crate::deb::build_binary_package(DebBuildOptions {
arch: Some("arm64".to_string()),
series: Some("noble".to_string()),
cwd: Some(pkg_dir),
cross: true,
ctx: Some(ctx),
..Default::default()
})
.await
.expect("Cannot cross-build package declaring Build-Depends-Indep");
+24 -36
View File
@@ -573,28 +573,21 @@ fn main() {
}
Some(("deb", sub_matches)) => {
let cwd = current_dir_or_exit();
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
let pocket = sub_matches.get_one::<String>("pocket").map(|s| s.as_str());
let arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str());
let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false);
let ppa: Vec<&str> = sub_matches
let series = sub_matches.get_one::<String>("series").cloned();
let pocket = sub_matches.get_one::<String>("pocket").cloned();
let arch = sub_matches.get_one::<String>("arch").cloned();
let cross = sub_matches
.get_one::<bool>("cross")
.copied()
.unwrap_or(false);
let ppa: Vec<String> = sub_matches
.get_many::<String>("ppa")
.map(|v| v.map(|s| s.as_str()).collect())
.map(|v| v.cloned().collect())
.unwrap_or_default();
let ppa = if ppa.is_empty() {
None
} else {
Some(ppa.as_slice())
};
let inject_packages: Vec<&str> = sub_matches
let inject: Vec<String> = sub_matches
.get_many::<String>("inject")
.map(|v| v.map(|s| s.as_str()).collect())
.map(|v| v.cloned().collect())
.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<pkh::deb::BuildMode> = match mode {
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
// terminal (DebUi handles the non-TTY case itself)
let ui = if verbose {
None
} else {
Some(std::sync::Arc::new(pkh::ui::deb::DebUi::new(&multi)))
};
// Live build view, unless --verbose (DebUi additionally disables
// itself when stdout is not a terminal)
let quiet = pkh::report::Quiet;
let live = pkh::ui::deb::DebUi::new(&multi);
let view: &dyn pkh::report::BuildView = if verbose { &quiet } else { &live };
let result = rt.block_on(async {
pkh::deb::build_binary_package(
pkh::deb::build_binary_package(pkh::deb::DebBuildOptions {
arch,
series,
pocket,
Some(cwd.as_path()),
*cross,
cwd: Some(cwd.clone()),
cross,
mode,
ppa,
inject_packages,
None,
ui.clone(),
inject,
jobs,
)
view,
..Default::default()
})
.await
});
match result {
Ok(artifacts) => {
let _ = artifacts;
info!("Done.");
}
Ok(_) => info!("Done."),
Err(e) => {
error!("{}", e);
std::process::exit(1);
+7 -14
View File
@@ -624,20 +624,13 @@ pub async fn offer_verification(
return;
}
let ui = Some(std::sync::Arc::new(crate::ui::deb::DebUi::new(multi)));
if let Err(e) = crate::deb::build_binary_package(
None,
Some(&opts.series),
None,
Some(&tree),
false,
None,
None,
None,
None,
ui,
None,
)
let view = crate::ui::deb::DebUi::new(multi);
if let Err(e) = crate::deb::build_binary_package(crate::deb::DebBuildOptions {
series: Some(opts.series.clone()),
cwd: Some(tree.clone()),
view: &view,
..Default::default()
})
.await
{
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::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
/// 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.
/// 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: `package` at `version`, built for
/// `target` (a distribution series, optionally with an architecture).
fn target(&self, _package: &str, _version: &str, _target: &str) {}
/// 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`])
@@ -63,6 +80,12 @@ pub trait BuildView: Send + Sync {
/// 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).
+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
}