diff --git a/src/build/mod.rs b/src/build/mod.rs index b2b1b29..f7e65ce 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -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(); diff --git a/src/deb/ephemeral.rs b/src/deb/ephemeral.rs index 4f265ad..571b557 100644 --- a/src/deb/ephemeral.rs +++ b/src/deb/ephemeral.rs @@ -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, - ui: Option>, + view: &dyn BuildView, ) -> Result> { // 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, - ui: &Option>, + view: &dyn BuildView, ) -> Result<(), Box> { // 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, - ui: &Option>, + view: &dyn BuildView, ) -> Result<(), Box> { // 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> { // 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(()) } diff --git a/src/deb/local.rs b/src/deb/local.rs index 5c5f6b3..e81f52b 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -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, - ui: Option>, + view: &dyn BuildView, jobs: Option, ) -> Result, Box> { - let sink: Option> = ui.as_ref().map(|u| u.sink()); + let sink: Option> = view.sink(); // Environment let mut env = HashMap::::new(); @@ -83,55 +83,53 @@ 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 { - // PPA format: user/ppa_name - let parts: Vec<&str> = ppa_str.split('/').collect(); - if parts.len() == 2 { - let base_url = crate::package_info::ppa_to_base_url(parts[0], parts[1]); + for ppa_str in ppa { + // PPA format: user/ppa_name + let parts: Vec<&str> = ppa_str.split('/').collect(); + if parts.len() == 2 { + let base_url = crate::package_info::ppa_to_base_url(parts[0], parts[1]); - // Add new PPA source if not found - if !sources.iter().any(|s| s.uri.contains(&base_url)) { - // Get host and target architectures - let host_arch = crate::get_current_arch(); - let target_arch = arch; + // Add new PPA source if not found + if !sources.iter().any(|s| s.uri.contains(&base_url)) { + // Get host and target architectures + let host_arch = crate::get_current_arch(); + let target_arch = arch; - // Create architectures list with both host and target if different - let mut architectures = vec![host_arch.clone()]; - if host_arch != *target_arch { - architectures.push(target_arch.to_string()); - } - - // Create suite list with all Ubuntu series - let suites = vec![series.to_string()]; - - let new_source = crate::apt::sources::SourceEntry { - enabled: true, - kind: crate::apt::sources::SourceKind::Deb, - components: vec!["main".to_string()], - architectures: architectures.clone(), - signed_by: None, - trusted: None, - suite: suites, - uri: base_url, - // No origin: saved to the pkh-owned added-sources file - origin: None, - }; - sources.push(new_source); - modified = true; - added_ppas.push((parts[0], parts[1])); - log::info!( - "Added PPA: {} for series {} with architectures {:?}", - ppa_str, - series, - architectures - ); + // Create architectures list with both host and target if different + let mut architectures = vec![host_arch.clone()]; + if host_arch != *target_arch { + architectures.push(target_arch.to_string()); } - } else { - return Err( - format!("Invalid PPA format: '{}'. Expected: user/ppa_name", ppa_str).into(), + + // Create suite list with all Ubuntu series + let suites = vec![series.to_string()]; + + let new_source = crate::apt::sources::SourceEntry { + enabled: true, + kind: crate::apt::sources::SourceKind::Deb, + components: vec!["main".to_string()], + architectures: architectures.clone(), + signed_by: None, + trusted: None, + suite: suites, + uri: base_url, + // No origin: saved to the pkh-owned added-sources file + origin: None, + }; + sources.push(new_source); + modified = true; + added_ppas.push((parts[0], parts[1])); + log::info!( + "Added PPA: {} for series {} with architectures {:?}", + ppa_str, + series, + architectures ); } + } else { + return Err( + format!("Invalid PPA format: '{}'. Expected: user/ppa_name", ppa_str).into(), + ); } } @@ -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, ctx: Arc, - ui: &Option>, + view: &dyn BuildView, sink: &Option>, ) -> Result<(), Box> { 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, - Box::new(QuiltClassifier::new(total_patches)), - ); - } + 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) -> Result<(), Box, ctx: Arc, - ui: &Option>, + view: &dyn BuildView, sink: &Option>, ) -> Result<(), Box> { 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 = 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(); } } diff --git a/src/deb/mod.rs b/src/deb/mod.rs index 3b197c9..2870807 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -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 { + 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, + /// Target distribution series; defaults to the changelog series + /// (UNRELEASED resolves to the vendor's development series). + pub series: Option, + /// Distribution pocket to resolve build-dependencies from. + pub pocket: Option, + /// Source tree to build; defaults to the process working directory. + pub cwd: Option, + /// Cross-compile for the target architecture instead of using + /// qemu-binfmt. + pub cross: bool, + /// Build mode; defaults to [`BuildMode::Local`]. + pub mode: Option, + /// PPAs to add for build-dependencies (`user/ppa_name`). + pub ppa: Vec, + /// Packages to inject into the build environment before build-dep + /// (.deb paths, archive names or PPA packages). + pub inject: Vec, + /// Parallel build jobs; defaults to the core count available in the + /// build context. + pub jobs: Option, + /// Explicit build context; defaults to the current context. + pub ctx: Option>, + /// 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, - ppa: Option<&[&str]>, - inject_packages: Option<&[&str]>, - ctx: Option>, - ui: Option>, - jobs: Option, + opts: DebBuildOptions<'_>, ) -> Result, Box> { - 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, - ppa: Option<&[&str]>, - inject_packages: Option<&[&str]>, - ctx: Option>, - ui: &Option>, - jobs: Option, + opts: DebBuildOptions<'_>, ) -> Result, Box> { - 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(¤t_arch); + let arch = arch.as_deref().unwrap_or(¤t_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"); diff --git a/src/main.rs b/src/main.rs index 6c7469a..6ae419e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -573,28 +573,21 @@ fn main() { } Some(("deb", sub_matches)) => { let cwd = current_dir_or_exit(); - let series = sub_matches.get_one::("series").map(|s| s.as_str()); - let pocket = sub_matches.get_one::("pocket").map(|s| s.as_str()); - let arch = sub_matches.get_one::("arch").map(|s| s.as_str()); - let cross = sub_matches.get_one::("cross").unwrap_or(&false); - let ppa: Vec<&str> = sub_matches + let series = sub_matches.get_one::("series").cloned(); + let pocket = sub_matches.get_one::("pocket").cloned(); + let arch = sub_matches.get_one::("arch").cloned(); + let cross = sub_matches + .get_one::("cross") + .copied() + .unwrap_or(false); + let ppa: Vec = sub_matches .get_many::("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 = sub_matches .get_many::("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::("mode").map(|s| s.as_str()); let mode: Option = 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); diff --git a/src/new/questions.rs b/src/new/questions.rs index 4cb0618..3436ecd 100644 --- a/src/new/questions.rs +++ b/src/new/questions.rs @@ -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}"); diff --git a/src/report.rs b/src/report.rs index 31a3efa..84529ca 100644 --- a/src/report.rs +++ b/src/report.rs @@ -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). diff --git a/src/ui/deb.rs b/src/ui/deb.rs index 0476e2f..925090b 100644 --- a/src/ui/deb.rs +++ b/src/ui/deb.rs @@ -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 { - 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>` (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, } @@ -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) { - 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) { { 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) -> Arc { - 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) { @@ -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> { @@ -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 }