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:
+7
-2
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
+75
-88
@@ -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,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<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,
|
||||
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<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
@@ -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(¤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");
|
||||
|
||||
|
||||
+24
-36
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user