report: add BuildView/Prompter ports and drive pkh build through them
Core flows no longer reach into the terminal UI: build_source_package takes a BuildSourceOptions struct (source tree, domain options, view, prompter) and reports phases, messages and outcomes through the environment-agnostic ports in the new report module. The classifiers move from ui/logfmt to the core logfmt module, DebUi becomes a BuildView adapter, the re-vendor retry asks the prompter instead of checking for a TTY, and artifact/success printing moves to the CLI. Headless consumers pass report::Quiet; an embedding (e.g. a builder server forwarding events to a web frontend) implements BuildView and maps the plain-data events onto its own wire format.
This commit is contained in:
+101
-120
@@ -14,7 +14,6 @@ pub mod env;
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::error::Error;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
@@ -24,8 +23,8 @@ use crate::context::{LineSink, Stream};
|
||||
use crate::debian::{
|
||||
ChecksumEntry, ControlInfo, FileChecksums, FilesEntry, FilesList, parse_paragraphs,
|
||||
};
|
||||
use crate::ui::deb::DebUi;
|
||||
use crate::ui::logfmt::{DpkgSourceClassifier, GenericClassifier};
|
||||
use crate::logfmt::{DpkgSourceClassifier, GenericClassifier};
|
||||
use crate::report::{BuildView, Prompter};
|
||||
|
||||
/// Whether the upload distributes the upstream orig tarballs (`--orig`),
|
||||
/// mirroring the `dpkg-genchanges` source styles.
|
||||
@@ -74,105 +73,108 @@ pub struct SourceBuildOutput {
|
||||
pub signed: bool,
|
||||
}
|
||||
|
||||
impl SourceBuildOutput {
|
||||
/// All produced artifacts in distribution order: dsc → tarballs →
|
||||
/// buildinfo → changes.
|
||||
pub fn artifacts(&self) -> Vec<PathBuf> {
|
||||
let mut artifacts = Vec::with_capacity(3 + self.tarballs.len());
|
||||
artifacts.push(self.dsc.clone());
|
||||
artifacts.extend(self.tarballs.iter().cloned());
|
||||
artifacts.push(self.buildinfo.clone());
|
||||
artifacts.push(self.changes.clone());
|
||||
artifacts
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters of one [`build_source_package`] call: where to build, the
|
||||
/// domain options, and the reporting ports (view and prompter).
|
||||
pub struct BuildSourceOptions<'a> {
|
||||
/// Source tree to package. When unset, the process's current working
|
||||
/// directory is used (resolved to an absolute path).
|
||||
pub source: Option<PathBuf>,
|
||||
/// Domain options (signing, orig-tarball inclusion, ...).
|
||||
pub options: SourceBuildOptions,
|
||||
/// Where build events (target, phases, messages, outcome) are reported.
|
||||
pub view: &'a dyn BuildView,
|
||||
/// Who answers the questions the flow may ask (e.g. the re-vendor
|
||||
/// retry offer).
|
||||
pub prompter: &'a dyn Prompter,
|
||||
}
|
||||
|
||||
impl Default for BuildSourceOptions<'_> {
|
||||
fn default() -> Self {
|
||||
static QUIET: crate::report::Quiet = crate::report::Quiet;
|
||||
BuildSourceOptions {
|
||||
source: None,
|
||||
options: SourceBuildOptions::default(),
|
||||
view: &QUIET,
|
||||
prompter: &QUIET,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a Debian source package (to a .dsc) using the native pipeline.
|
||||
///
|
||||
/// When `ui` is set, subprocess output is captured into a live view (status
|
||||
/// bar + rolling pane) and tee'd to a log file; on failure the view prints a
|
||||
/// summary of the last captured errors. Without a UI, commands inherit the
|
||||
/// terminal as before.
|
||||
/// Subprocess output is captured into the view (status line + rolling
|
||||
/// pane for the terminal adapter) and tee'd to a log file; on failure the
|
||||
/// view prints a summary of the last captured errors. Headless callers use
|
||||
/// [`crate::report::Quiet`], in which case commands still run with captured
|
||||
/// output in test builds.
|
||||
///
|
||||
/// A `dpkg-source -b` failure is classified (see
|
||||
/// [`classify_dpkg_source_failure`]); when the vendored rust dependencies
|
||||
/// diverged from the orig-vendor component and a terminal is attached, the
|
||||
/// flow offers to re-vendor, recreate the component and retry the build
|
||||
/// diverged from the orig-vendor component, the flow asks the prompter
|
||||
/// whether to re-vendor, recreates the component and retries the build
|
||||
/// exactly once.
|
||||
///
|
||||
/// On success the produced artifacts are reported through the view and
|
||||
/// returned.
|
||||
pub fn build_source_package(
|
||||
cwd: Option<&Path>,
|
||||
opts: SourceBuildOptions,
|
||||
ui: Option<Arc<DebUi>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
opts: BuildSourceOptions<'_>,
|
||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||
// Default to the process's current working directory, resolved to an
|
||||
// absolute path: the output directory is derived from `cwd.parent()`
|
||||
// downstream, which only yields a real directory for an absolute `cwd`
|
||||
// (the parent of "." is the empty path).
|
||||
let cwd = match cwd {
|
||||
Some(p) => p.to_path_buf(),
|
||||
let cwd = match opts.source {
|
||||
Some(ref p) => p.clone(),
|
||||
None => std::env::current_dir()
|
||||
.map_err(|e| format!("cannot determine the current working directory: {e}"))?,
|
||||
};
|
||||
let output = match run_source_build(&cwd, &opts, ui.clone()) {
|
||||
let output = match run_source_build(&cwd, &opts.options, opts.view) {
|
||||
Ok(output) => output,
|
||||
Err(e) if e.downcast_ref::<VendorDriftError>().is_some() => {
|
||||
return retry_after_revendor(&cwd, ui, opts, e);
|
||||
return retry_after_revendor(&cwd, opts.view, opts.prompter, &opts.options, e);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(u) = &ui {
|
||||
u.finish_failure();
|
||||
}
|
||||
opts.view.finish_failure();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Artifact listing in dpkg order: dsc → tarballs → buildinfo → changes.
|
||||
let mut artifacts = Vec::with_capacity(3 + output.tarballs.len());
|
||||
artifacts.push(output.dsc.clone());
|
||||
artifacts.extend(output.tarballs.iter().cloned());
|
||||
artifacts.push(output.buildinfo.clone());
|
||||
artifacts.push(output.changes.clone());
|
||||
|
||||
// The live view lists the artifacts itself when it renders; otherwise
|
||||
// (verbose mode or non-TTY stdout) print them as plain lines.
|
||||
let listed = match &ui {
|
||||
Some(u) if u.is_enabled() => {
|
||||
u.finish_success(&artifacts, u.elapsed());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !listed {
|
||||
for artifact in &artifacts {
|
||||
println!(" {}", crate::ui::display_path(artifact));
|
||||
}
|
||||
}
|
||||
|
||||
if output.signed {
|
||||
println!("Package built and signed successfully!");
|
||||
} else {
|
||||
println!("Package built successfully (unsigned).");
|
||||
}
|
||||
Ok(())
|
||||
opts.view.finish_success(&output.artifacts());
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// The re-vendor retry hook for a [`VendorDriftError`]: on an interactive
|
||||
/// terminal, offer to re-run the vendoring step (the same helper the rust
|
||||
/// template uses at scaffold time), recreate the `orig-vendor` component
|
||||
/// from the fresh `vendor/` tree and retry the source build exactly once.
|
||||
/// Without a terminal (or on a declined offer) the original error is
|
||||
/// returned untouched.
|
||||
/// The re-vendor retry hook for a [`VendorDriftError`]: offer to re-run the
|
||||
/// vendoring step through the prompter (the same helper the rust template
|
||||
/// uses at scaffold time), recreate the `orig-vendor` component from the
|
||||
/// fresh `vendor/` tree and retry the source build exactly once. Without an
|
||||
/// accepting answer (headless prompters answer with the default, `false`)
|
||||
/// the original error is returned untouched.
|
||||
fn retry_after_revendor(
|
||||
cwd: &Path,
|
||||
ui: Option<Arc<DebUi>>,
|
||||
opts: SourceBuildOptions,
|
||||
view: &dyn BuildView,
|
||||
prompter: &dyn Prompter,
|
||||
opts: &SourceBuildOptions,
|
||||
original: Box<dyn Error>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let interactive = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
|
||||
if !interactive {
|
||||
if let Some(u) = &ui {
|
||||
u.finish_failure();
|
||||
}
|
||||
return Err(original);
|
||||
}
|
||||
|
||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||
log::error!("{original}");
|
||||
let retry = crate::ui::prompt::confirm(
|
||||
if !prompter.confirm(
|
||||
"Re-vendor the Cargo dependencies and retry the build?",
|
||||
false,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if !retry {
|
||||
if let Some(u) = &ui {
|
||||
u.finish_failure();
|
||||
}
|
||||
) {
|
||||
view.finish_failure();
|
||||
return Err(original);
|
||||
}
|
||||
|
||||
@@ -189,9 +191,7 @@ fn retry_after_revendor(
|
||||
std::fs::remove_file(&config)?;
|
||||
}
|
||||
if !crate::new::templates::rust::vendor_dependencies(cwd)? {
|
||||
if let Some(u) = &ui {
|
||||
u.finish_failure();
|
||||
}
|
||||
view.finish_failure();
|
||||
return Err(
|
||||
"Re-vendoring did not complete: the tree is unchanged, fix the \
|
||||
vendoring by hand and build again."
|
||||
@@ -218,7 +218,7 @@ fn retry_after_revendor(
|
||||
crate::new::orig::create_vendor_component(cwd, &entry.source, uversion)?;
|
||||
|
||||
// 3. One retry, with the options of the original attempt.
|
||||
run_source_build(cwd, &opts, ui).map(|_| ())
|
||||
run_source_build(cwd, opts, view)
|
||||
}
|
||||
|
||||
/// Run the full native source-build pipeline in `cwd`.
|
||||
@@ -237,14 +237,12 @@ fn retry_after_revendor(
|
||||
pub fn run_source_build(
|
||||
cwd: &Path,
|
||||
opts: &SourceBuildOptions,
|
||||
ui: Option<Arc<DebUi>>,
|
||||
view: &dyn BuildView,
|
||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||
// Without a live UI, test runs still capture command output into the
|
||||
// per-test log file instead of letting it inherit the terminal
|
||||
let sink: Option<Arc<dyn LineSink>> = ui
|
||||
.as_ref()
|
||||
.map(|u| u.sink())
|
||||
.or_else(crate::test_support::subprocess_sink);
|
||||
// The view consumes the captured lines itself (live view + tee log);
|
||||
// without one, test runs still capture command output into the per-test
|
||||
// log file instead of letting it inherit the terminal
|
||||
let sink: Option<Arc<dyn LineSink>> = view.sink().or_else(crate::test_support::subprocess_sink);
|
||||
// ------------------------------------------------------------------
|
||||
// 1. Sanity checks
|
||||
// ------------------------------------------------------------------
|
||||
@@ -296,9 +294,7 @@ pub fn run_source_build(
|
||||
|
||||
let ctrl = ControlInfo::parse(&control_path)?;
|
||||
|
||||
if let Some(u) = &ui {
|
||||
u.set_build_target(&entry.source, &entry.version.full(), &entry.distribution);
|
||||
}
|
||||
view.target(&entry.source, &entry.version.full(), &entry.distribution);
|
||||
|
||||
let source_display = entry.source.clone();
|
||||
|
||||
@@ -357,9 +353,7 @@ pub fn run_source_build(
|
||||
// ------------------------------------------------------------------
|
||||
// 5. dpkg-source lifecycle: before-build + source build
|
||||
// ------------------------------------------------------------------
|
||||
if let Some(u) = &ui {
|
||||
u.phase_custom("Applying patches", Box::new(DpkgSourceClassifier::new()));
|
||||
}
|
||||
view.phase("Applying patches", Box::new(DpkgSourceClassifier::new()));
|
||||
run_command(
|
||||
cwd,
|
||||
"dpkg-source",
|
||||
@@ -372,9 +366,7 @@ pub fn run_source_build(
|
||||
// dpkg-buildpackage skips it entirely for source-only builds unless
|
||||
// forced with -D; unsatisfied dependencies abort with exit status 3.
|
||||
if opts.force_dep_check {
|
||||
if let Some(u) = &ui {
|
||||
u.progress_message("Checking build dependencies");
|
||||
}
|
||||
view.message("Checking build dependencies");
|
||||
let check_opts = crate::debian::deps::CheckOpts {
|
||||
host_arch: arch_vars
|
||||
.get("DEB_HOST_ARCH")
|
||||
@@ -396,12 +388,10 @@ pub fn run_source_build(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(u) = &ui {
|
||||
u.phase_custom(
|
||||
"Building source package",
|
||||
Box::new(DpkgSourceClassifier::new()),
|
||||
);
|
||||
}
|
||||
view.phase(
|
||||
"Building source package",
|
||||
Box::new(DpkgSourceClassifier::new()),
|
||||
);
|
||||
if let Err(failure) = run_command_capturing(
|
||||
cwd,
|
||||
"dpkg-source",
|
||||
@@ -427,9 +417,7 @@ pub fn run_source_build(
|
||||
// ------------------------------------------------------------------
|
||||
// 6. .buildinfo generation (native dpkg-genbuildinfo equivalent)
|
||||
// ------------------------------------------------------------------
|
||||
if let Some(u) = &ui {
|
||||
u.progress_message("Generating .buildinfo");
|
||||
}
|
||||
view.message("Generating .buildinfo");
|
||||
// What the .buildinfo itself records: like dpkg-genbuildinfo, only the
|
||||
// referenced .dsc — not the tarballs, and never the buildinfo itself.
|
||||
let mut buildinfo_checksums = FileChecksums::new();
|
||||
@@ -479,9 +467,7 @@ pub fn run_source_build(
|
||||
// ------------------------------------------------------------------
|
||||
// 7. .changes generation (native dpkg-genchanges equivalent)
|
||||
// ------------------------------------------------------------------
|
||||
if let Some(u) = &ui {
|
||||
u.progress_message("Generating .changes");
|
||||
}
|
||||
view.message("Generating .changes");
|
||||
// What the .changes distributes: the .dsc (with its recorded digests),
|
||||
// the tarballs listed in it (below), and the .buildinfo (last, as
|
||||
// dpkg-genchanges does when it consumes debian/files).
|
||||
@@ -538,13 +524,11 @@ pub fn run_source_build(
|
||||
if opts.orig_source == OrigSourceMode::Never && !has_debian_part {
|
||||
log::warn!("ignoring --orig never for a native Debian package");
|
||||
}
|
||||
if let Some(u) = &ui {
|
||||
u.progress_message(if strip_origs {
|
||||
"Not including original source code in upload"
|
||||
} else {
|
||||
"Including full source code in upload"
|
||||
});
|
||||
}
|
||||
view.message(if strip_origs {
|
||||
"Not including original source code in upload"
|
||||
} else {
|
||||
"Including full source code in upload"
|
||||
});
|
||||
// Stripped orig tarballs (and their detached .asc signatures) are not
|
||||
// distributed at all: not hashed, not required on disk, like
|
||||
// dpkg-genchanges.
|
||||
@@ -634,9 +618,7 @@ pub fn run_source_build(
|
||||
// ------------------------------------------------------------------
|
||||
// 8. dpkg-source after-build (unapplies quilt patches it applied)
|
||||
// ------------------------------------------------------------------
|
||||
if let Some(u) = &ui {
|
||||
u.phase_custom("Restoring patches", Box::new(DpkgSourceClassifier::new()));
|
||||
}
|
||||
view.phase("Restoring patches", Box::new(DpkgSourceClassifier::new()));
|
||||
run_command(
|
||||
cwd,
|
||||
"dpkg-source",
|
||||
@@ -652,9 +634,7 @@ pub fn run_source_build(
|
||||
if let Some(keyid) = signing_key.filter(|_| do_sign) {
|
||||
crate::utils::gpg::validate_key_id(&keyid)?;
|
||||
|
||||
if let Some(u) = &ui {
|
||||
u.phase_custom("Signing artifacts", Box::new(GenericClassifier::new()));
|
||||
}
|
||||
view.phase("Signing artifacts", Box::new(GenericClassifier::new()));
|
||||
|
||||
log::info!("Signing {}", dsc_name);
|
||||
crate::utils::gpg::clearsign_file(&dsc_path, &keyid)?;
|
||||
@@ -1039,7 +1019,7 @@ mod tests {
|
||||
.expect("write control");
|
||||
std::fs::write(tree.join("debian/rules"), "#!/usr/bin/make -f\n").expect("write rules");
|
||||
|
||||
let err = run_source_build(&tree, &SourceBuildOptions::default(), None)
|
||||
let err = run_source_build(&tree, &SourceBuildOptions::default(), &crate::report::Quiet)
|
||||
.expect_err("binary-only entries must not build a source package");
|
||||
let err = err.to_string();
|
||||
assert!(err.contains("binary-only"), "{err}");
|
||||
@@ -1575,7 +1555,8 @@ mod differential_tests {
|
||||
OrigSourceMode::Never => &["-sd"],
|
||||
};
|
||||
run_dpkg(&golden_tree, source_style);
|
||||
run_source_build(&ours_tree, opts, None).expect("native source pipeline failed");
|
||||
run_source_build(&ours_tree, opts, &crate::report::Quiet)
|
||||
.expect("native source pipeline failed");
|
||||
|
||||
let entry =
|
||||
crate::debian::parse_changelog_entry(&ours_tree.join("debian/changelog")).unwrap();
|
||||
@@ -2160,7 +2141,7 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
||||
);
|
||||
|
||||
// Ours: the native pipeline refuses likewise.
|
||||
let err = run_source_build(&tree, &SourceBuildOptions::default(), None)
|
||||
let err = run_source_build(&tree, &SourceBuildOptions::default(), &crate::report::Quiet)
|
||||
.expect_err("native source build must refuse a binary-only entry");
|
||||
assert!(err.to_string().contains("binary-only"), "{err}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user