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:
+94
-113
@@ -14,7 +14,6 @@ pub mod env;
|
|||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::IsTerminal;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -24,8 +23,8 @@ use crate::context::{LineSink, Stream};
|
|||||||
use crate::debian::{
|
use crate::debian::{
|
||||||
ChecksumEntry, ControlInfo, FileChecksums, FilesEntry, FilesList, parse_paragraphs,
|
ChecksumEntry, ControlInfo, FileChecksums, FilesEntry, FilesList, parse_paragraphs,
|
||||||
};
|
};
|
||||||
use crate::ui::deb::DebUi;
|
use crate::logfmt::{DpkgSourceClassifier, GenericClassifier};
|
||||||
use crate::ui::logfmt::{DpkgSourceClassifier, GenericClassifier};
|
use crate::report::{BuildView, Prompter};
|
||||||
|
|
||||||
/// Whether the upload distributes the upstream orig tarballs (`--orig`),
|
/// Whether the upload distributes the upstream orig tarballs (`--orig`),
|
||||||
/// mirroring the `dpkg-genchanges` source styles.
|
/// mirroring the `dpkg-genchanges` source styles.
|
||||||
@@ -74,105 +73,108 @@ pub struct SourceBuildOutput {
|
|||||||
pub signed: bool,
|
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.
|
/// 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
|
/// Subprocess output is captured into the view (status line + rolling
|
||||||
/// bar + rolling pane) and tee'd to a log file; on failure the view prints a
|
/// pane for the terminal adapter) and tee'd to a log file; on failure the
|
||||||
/// summary of the last captured errors. Without a UI, commands inherit the
|
/// view prints a summary of the last captured errors. Headless callers use
|
||||||
/// terminal as before.
|
/// [`crate::report::Quiet`], in which case commands still run with captured
|
||||||
|
/// output in test builds.
|
||||||
///
|
///
|
||||||
/// A `dpkg-source -b` failure is classified (see
|
/// A `dpkg-source -b` failure is classified (see
|
||||||
/// [`classify_dpkg_source_failure`]); when the vendored rust dependencies
|
/// [`classify_dpkg_source_failure`]); when the vendored rust dependencies
|
||||||
/// diverged from the orig-vendor component and a terminal is attached, the
|
/// diverged from the orig-vendor component, the flow asks the prompter
|
||||||
/// flow offers to re-vendor, recreate the component and retry the build
|
/// whether to re-vendor, recreates the component and retries the build
|
||||||
/// exactly once.
|
/// exactly once.
|
||||||
|
///
|
||||||
|
/// On success the produced artifacts are reported through the view and
|
||||||
|
/// returned.
|
||||||
pub fn build_source_package(
|
pub fn build_source_package(
|
||||||
cwd: Option<&Path>,
|
opts: BuildSourceOptions<'_>,
|
||||||
opts: SourceBuildOptions,
|
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||||
ui: Option<Arc<DebUi>>,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
// Default to the process's current working directory, resolved to an
|
// Default to the process's current working directory, resolved to an
|
||||||
// absolute path: the output directory is derived from `cwd.parent()`
|
// absolute path: the output directory is derived from `cwd.parent()`
|
||||||
// downstream, which only yields a real directory for an absolute `cwd`
|
// downstream, which only yields a real directory for an absolute `cwd`
|
||||||
// (the parent of "." is the empty path).
|
// (the parent of "." is the empty path).
|
||||||
let cwd = match cwd {
|
let cwd = match opts.source {
|
||||||
Some(p) => p.to_path_buf(),
|
Some(ref p) => p.clone(),
|
||||||
None => std::env::current_dir()
|
None => std::env::current_dir()
|
||||||
.map_err(|e| format!("cannot determine the current working directory: {e}"))?,
|
.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,
|
Ok(output) => output,
|
||||||
Err(e) if e.downcast_ref::<VendorDriftError>().is_some() => {
|
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) => {
|
Err(e) => {
|
||||||
if let Some(u) = &ui {
|
opts.view.finish_failure();
|
||||||
u.finish_failure();
|
|
||||||
}
|
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Artifact listing in dpkg order: dsc → tarballs → buildinfo → changes.
|
opts.view.finish_success(&output.artifacts());
|
||||||
let mut artifacts = Vec::with_capacity(3 + output.tarballs.len());
|
Ok(output)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The re-vendor retry hook for a [`VendorDriftError`]: on an interactive
|
/// The re-vendor retry hook for a [`VendorDriftError`]: offer to re-run the
|
||||||
/// terminal, offer to re-run the vendoring step (the same helper the rust
|
/// vendoring step through the prompter (the same helper the rust template
|
||||||
/// template uses at scaffold time), recreate the `orig-vendor` component
|
/// uses at scaffold time), recreate the `orig-vendor` component from the
|
||||||
/// from the fresh `vendor/` tree and retry the source build exactly once.
|
/// fresh `vendor/` tree and retry the source build exactly once. Without an
|
||||||
/// Without a terminal (or on a declined offer) the original error is
|
/// accepting answer (headless prompters answer with the default, `false`)
|
||||||
/// returned untouched.
|
/// the original error is returned untouched.
|
||||||
fn retry_after_revendor(
|
fn retry_after_revendor(
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
ui: Option<Arc<DebUi>>,
|
view: &dyn BuildView,
|
||||||
opts: SourceBuildOptions,
|
prompter: &dyn Prompter,
|
||||||
|
opts: &SourceBuildOptions,
|
||||||
original: Box<dyn Error>,
|
original: Box<dyn Error>,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<SourceBuildOutput, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::error!("{original}");
|
log::error!("{original}");
|
||||||
let retry = crate::ui::prompt::confirm(
|
if !prompter.confirm(
|
||||||
"Re-vendor the Cargo dependencies and retry the build?",
|
"Re-vendor the Cargo dependencies and retry the build?",
|
||||||
false,
|
false,
|
||||||
)
|
) {
|
||||||
.unwrap_or(false);
|
view.finish_failure();
|
||||||
if !retry {
|
|
||||||
if let Some(u) = &ui {
|
|
||||||
u.finish_failure();
|
|
||||||
}
|
|
||||||
return Err(original);
|
return Err(original);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +191,7 @@ fn retry_after_revendor(
|
|||||||
std::fs::remove_file(&config)?;
|
std::fs::remove_file(&config)?;
|
||||||
}
|
}
|
||||||
if !crate::new::templates::rust::vendor_dependencies(cwd)? {
|
if !crate::new::templates::rust::vendor_dependencies(cwd)? {
|
||||||
if let Some(u) = &ui {
|
view.finish_failure();
|
||||||
u.finish_failure();
|
|
||||||
}
|
|
||||||
return Err(
|
return Err(
|
||||||
"Re-vendoring did not complete: the tree is unchanged, fix the \
|
"Re-vendoring did not complete: the tree is unchanged, fix the \
|
||||||
vendoring by hand and build again."
|
vendoring by hand and build again."
|
||||||
@@ -218,7 +218,7 @@ fn retry_after_revendor(
|
|||||||
crate::new::orig::create_vendor_component(cwd, &entry.source, uversion)?;
|
crate::new::orig::create_vendor_component(cwd, &entry.source, uversion)?;
|
||||||
|
|
||||||
// 3. One retry, with the options of the original attempt.
|
// 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`.
|
/// Run the full native source-build pipeline in `cwd`.
|
||||||
@@ -237,14 +237,12 @@ fn retry_after_revendor(
|
|||||||
pub fn run_source_build(
|
pub fn run_source_build(
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
opts: &SourceBuildOptions,
|
opts: &SourceBuildOptions,
|
||||||
ui: Option<Arc<DebUi>>,
|
view: &dyn BuildView,
|
||||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||||
// Without a live UI, test runs still capture command output into the
|
// The view consumes the captured lines itself (live view + tee log);
|
||||||
// per-test log file instead of letting it inherit the terminal
|
// without one, test runs still capture command output into the per-test
|
||||||
let sink: Option<Arc<dyn LineSink>> = ui
|
// log file instead of letting it inherit the terminal
|
||||||
.as_ref()
|
let sink: Option<Arc<dyn LineSink>> = view.sink().or_else(crate::test_support::subprocess_sink);
|
||||||
.map(|u| u.sink())
|
|
||||||
.or_else(crate::test_support::subprocess_sink);
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 1. Sanity checks
|
// 1. Sanity checks
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -296,9 +294,7 @@ pub fn run_source_build(
|
|||||||
|
|
||||||
let ctrl = ControlInfo::parse(&control_path)?;
|
let ctrl = ControlInfo::parse(&control_path)?;
|
||||||
|
|
||||||
if let Some(u) = &ui {
|
view.target(&entry.source, &entry.version.full(), &entry.distribution);
|
||||||
u.set_build_target(&entry.source, &entry.version.full(), &entry.distribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
let source_display = entry.source.clone();
|
let source_display = entry.source.clone();
|
||||||
|
|
||||||
@@ -357,9 +353,7 @@ pub fn run_source_build(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 5. dpkg-source lifecycle: before-build + source build
|
// 5. dpkg-source lifecycle: before-build + source build
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
if let Some(u) = &ui {
|
view.phase("Applying patches", Box::new(DpkgSourceClassifier::new()));
|
||||||
u.phase_custom("Applying patches", Box::new(DpkgSourceClassifier::new()));
|
|
||||||
}
|
|
||||||
run_command(
|
run_command(
|
||||||
cwd,
|
cwd,
|
||||||
"dpkg-source",
|
"dpkg-source",
|
||||||
@@ -372,9 +366,7 @@ pub fn run_source_build(
|
|||||||
// dpkg-buildpackage skips it entirely for source-only builds unless
|
// dpkg-buildpackage skips it entirely for source-only builds unless
|
||||||
// forced with -D; unsatisfied dependencies abort with exit status 3.
|
// forced with -D; unsatisfied dependencies abort with exit status 3.
|
||||||
if opts.force_dep_check {
|
if opts.force_dep_check {
|
||||||
if let Some(u) = &ui {
|
view.message("Checking build dependencies");
|
||||||
u.progress_message("Checking build dependencies");
|
|
||||||
}
|
|
||||||
let check_opts = crate::debian::deps::CheckOpts {
|
let check_opts = crate::debian::deps::CheckOpts {
|
||||||
host_arch: arch_vars
|
host_arch: arch_vars
|
||||||
.get("DEB_HOST_ARCH")
|
.get("DEB_HOST_ARCH")
|
||||||
@@ -396,12 +388,10 @@ pub fn run_source_build(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(u) = &ui {
|
view.phase(
|
||||||
u.phase_custom(
|
|
||||||
"Building source package",
|
"Building source package",
|
||||||
Box::new(DpkgSourceClassifier::new()),
|
Box::new(DpkgSourceClassifier::new()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if let Err(failure) = run_command_capturing(
|
if let Err(failure) = run_command_capturing(
|
||||||
cwd,
|
cwd,
|
||||||
"dpkg-source",
|
"dpkg-source",
|
||||||
@@ -427,9 +417,7 @@ pub fn run_source_build(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 6. .buildinfo generation (native dpkg-genbuildinfo equivalent)
|
// 6. .buildinfo generation (native dpkg-genbuildinfo equivalent)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
if let Some(u) = &ui {
|
view.message("Generating .buildinfo");
|
||||||
u.progress_message("Generating .buildinfo");
|
|
||||||
}
|
|
||||||
// What the .buildinfo itself records: like dpkg-genbuildinfo, only the
|
// What the .buildinfo itself records: like dpkg-genbuildinfo, only the
|
||||||
// referenced .dsc — not the tarballs, and never the buildinfo itself.
|
// referenced .dsc — not the tarballs, and never the buildinfo itself.
|
||||||
let mut buildinfo_checksums = FileChecksums::new();
|
let mut buildinfo_checksums = FileChecksums::new();
|
||||||
@@ -479,9 +467,7 @@ pub fn run_source_build(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 7. .changes generation (native dpkg-genchanges equivalent)
|
// 7. .changes generation (native dpkg-genchanges equivalent)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
if let Some(u) = &ui {
|
view.message("Generating .changes");
|
||||||
u.progress_message("Generating .changes");
|
|
||||||
}
|
|
||||||
// What the .changes distributes: the .dsc (with its recorded digests),
|
// What the .changes distributes: the .dsc (with its recorded digests),
|
||||||
// the tarballs listed in it (below), and the .buildinfo (last, as
|
// the tarballs listed in it (below), and the .buildinfo (last, as
|
||||||
// dpkg-genchanges does when it consumes debian/files).
|
// 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 {
|
if opts.orig_source == OrigSourceMode::Never && !has_debian_part {
|
||||||
log::warn!("ignoring --orig never for a native Debian package");
|
log::warn!("ignoring --orig never for a native Debian package");
|
||||||
}
|
}
|
||||||
if let Some(u) = &ui {
|
view.message(if strip_origs {
|
||||||
u.progress_message(if strip_origs {
|
|
||||||
"Not including original source code in upload"
|
"Not including original source code in upload"
|
||||||
} else {
|
} else {
|
||||||
"Including full source code in upload"
|
"Including full source code in upload"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
// Stripped orig tarballs (and their detached .asc signatures) are not
|
// Stripped orig tarballs (and their detached .asc signatures) are not
|
||||||
// distributed at all: not hashed, not required on disk, like
|
// distributed at all: not hashed, not required on disk, like
|
||||||
// dpkg-genchanges.
|
// dpkg-genchanges.
|
||||||
@@ -634,9 +618,7 @@ pub fn run_source_build(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 8. dpkg-source after-build (unapplies quilt patches it applied)
|
// 8. dpkg-source after-build (unapplies quilt patches it applied)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
if let Some(u) = &ui {
|
view.phase("Restoring patches", Box::new(DpkgSourceClassifier::new()));
|
||||||
u.phase_custom("Restoring patches", Box::new(DpkgSourceClassifier::new()));
|
|
||||||
}
|
|
||||||
run_command(
|
run_command(
|
||||||
cwd,
|
cwd,
|
||||||
"dpkg-source",
|
"dpkg-source",
|
||||||
@@ -652,9 +634,7 @@ pub fn run_source_build(
|
|||||||
if let Some(keyid) = signing_key.filter(|_| do_sign) {
|
if let Some(keyid) = signing_key.filter(|_| do_sign) {
|
||||||
crate::utils::gpg::validate_key_id(&keyid)?;
|
crate::utils::gpg::validate_key_id(&keyid)?;
|
||||||
|
|
||||||
if let Some(u) = &ui {
|
view.phase("Signing artifacts", Box::new(GenericClassifier::new()));
|
||||||
u.phase_custom("Signing artifacts", Box::new(GenericClassifier::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Signing {}", dsc_name);
|
log::info!("Signing {}", dsc_name);
|
||||||
crate::utils::gpg::clearsign_file(&dsc_path, &keyid)?;
|
crate::utils::gpg::clearsign_file(&dsc_path, &keyid)?;
|
||||||
@@ -1039,7 +1019,7 @@ mod tests {
|
|||||||
.expect("write control");
|
.expect("write control");
|
||||||
std::fs::write(tree.join("debian/rules"), "#!/usr/bin/make -f\n").expect("write rules");
|
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");
|
.expect_err("binary-only entries must not build a source package");
|
||||||
let err = err.to_string();
|
let err = err.to_string();
|
||||||
assert!(err.contains("binary-only"), "{err}");
|
assert!(err.contains("binary-only"), "{err}");
|
||||||
@@ -1575,7 +1555,8 @@ mod differential_tests {
|
|||||||
OrigSourceMode::Never => &["-sd"],
|
OrigSourceMode::Never => &["-sd"],
|
||||||
};
|
};
|
||||||
run_dpkg(&golden_tree, source_style);
|
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 =
|
let entry =
|
||||||
crate::debian::parse_changelog_entry(&ours_tree.join("debian/changelog")).unwrap();
|
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.
|
// 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");
|
.expect_err("native source build must refuse a binary-only entry");
|
||||||
assert!(err.to_string().contains("binary-only"), "{err}");
|
assert!(err.to_string().contains("binary-only"), "{err}");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,8 +2,8 @@
|
|||||||
/// Directly calling 'debian/rules' in current context
|
/// Directly calling 'debian/rules' in current context
|
||||||
use crate::context::{Context, ContextCommand, LineSink};
|
use crate::context::{Context, ContextCommand, LineSink};
|
||||||
use crate::deb::find_dsc_file;
|
use crate::deb::find_dsc_file;
|
||||||
|
use crate::logfmt::QuiltClassifier;
|
||||||
use crate::ui::deb::{DebUi, Phase};
|
use crate::ui::deb::{DebUi, Phase};
|
||||||
use crate::ui::logfmt::QuiltClassifier;
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ pub mod put;
|
|||||||
/// Handle package-specific quirks and workarounds
|
/// Handle package-specific quirks and workarounds
|
||||||
pub mod quirks;
|
pub mod quirks;
|
||||||
|
|
||||||
|
/// Line classifiers rewriting raw subprocess output into display actions
|
||||||
|
/// and countable progress (pure logic, shared by build views)
|
||||||
|
pub mod logfmt;
|
||||||
|
|
||||||
|
/// Reporting ports: environment-agnostic build observation ([`BuildView`])
|
||||||
|
/// and question answering ([`Prompter`]), implemented by terminal views,
|
||||||
|
/// server bridges or the inert [`Quiet`]
|
||||||
|
pub mod report;
|
||||||
|
|
||||||
/// Terminal UI helpers (progress bars, live build views, prompts)
|
/// Terminal UI helpers (progress bars, live build views, prompts)
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
|||||||
+29
-12
@@ -490,13 +490,12 @@ fn main() {
|
|||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Live build view: disabled by --verbose or when stdout is not a
|
// Live build view, unless --verbose (DebUi additionally disables
|
||||||
// terminal (DebUi handles the non-TTY case itself)
|
// itself when stdout is not a terminal)
|
||||||
let ui = if verbose {
|
let quiet = pkh::report::Quiet;
|
||||||
None
|
let live = pkh::ui::deb::DebUi::new(&multi);
|
||||||
} else {
|
let view: &dyn pkh::report::BuildView = if verbose { &quiet } else { &live };
|
||||||
Some(std::sync::Arc::new(pkh::ui::deb::DebUi::new(&multi)))
|
let prompter = pkh::ui::prompt::TerminalPrompter;
|
||||||
};
|
|
||||||
|
|
||||||
let orig_source = match sub_matches.get_one::<String>("orig").map(String::as_str) {
|
let orig_source = match sub_matches.get_one::<String>("orig").map(String::as_str) {
|
||||||
Some("always") => pkh::build::OrigSourceMode::Always,
|
Some("always") => pkh::build::OrigSourceMode::Always,
|
||||||
@@ -504,14 +503,31 @@ fn main() {
|
|||||||
_ => pkh::build::OrigSourceMode::Auto,
|
_ => pkh::build::OrigSourceMode::Auto,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = pkh::build::build_source_package(
|
match pkh::build::build_source_package(pkh::build::BuildSourceOptions {
|
||||||
Some(&cwd),
|
source: Some(cwd),
|
||||||
pkh::build::SourceBuildOptions {
|
options: pkh::build::SourceBuildOptions {
|
||||||
orig_source,
|
orig_source,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
ui,
|
view,
|
||||||
) {
|
prompter: &prompter,
|
||||||
|
}) {
|
||||||
|
Ok(output) => {
|
||||||
|
// The live view lists the artifacts itself when it
|
||||||
|
// renders; otherwise (verbose mode or non-TTY stdout)
|
||||||
|
// print them as plain lines.
|
||||||
|
if !view.is_enabled() {
|
||||||
|
for artifact in output.artifacts() {
|
||||||
|
println!(" {}", pkh::ui::display_path(&artifact));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if output.signed {
|
||||||
|
info!("Package built and signed successfully!");
|
||||||
|
} else {
|
||||||
|
info!("Package built successfully (unsigned).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
// Unmet build dependencies/conflicts exit with status 3,
|
// Unmet build dependencies/conflicts exit with status 3,
|
||||||
// like dpkg-buildpackage does.
|
// like dpkg-buildpackage does.
|
||||||
@@ -523,6 +539,7 @@ fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Some(("put", sub_matches)) => {
|
Some(("put", sub_matches)) => {
|
||||||
let cwd = current_dir_or_exit();
|
let cwd = current_dir_or_exit();
|
||||||
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
|
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
|
||||||
|
|||||||
+3
-3
@@ -819,7 +819,7 @@ mod tests {
|
|||||||
let output = crate::build::run_source_build(
|
let output = crate::build::run_source_build(
|
||||||
&source,
|
&source,
|
||||||
&crate::build::SourceBuildOptions::default(),
|
&crate::build::SourceBuildOptions::default(),
|
||||||
None,
|
&crate::report::Quiet,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let dsc = std::fs::read_to_string(&output.dsc).unwrap();
|
let dsc = std::fs::read_to_string(&output.dsc).unwrap();
|
||||||
@@ -885,7 +885,7 @@ mod tests {
|
|||||||
let output = crate::build::run_source_build(
|
let output = crate::build::run_source_build(
|
||||||
&dir.path().join("mytool"),
|
&dir.path().join("mytool"),
|
||||||
&crate::build::SourceBuildOptions::default(),
|
&crate::build::SourceBuildOptions::default(),
|
||||||
None,
|
&crate::report::Quiet,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -916,7 +916,7 @@ mod tests {
|
|||||||
let output = crate::build::run_source_build(
|
let output = crate::build::run_source_build(
|
||||||
&dir.path().join("mytool"),
|
&dir.path().join("mytool"),
|
||||||
&crate::build::SourceBuildOptions::default(),
|
&crate::build::SourceBuildOptions::default(),
|
||||||
None,
|
&crate::report::Quiet,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(output.dsc.exists(), "{:?} missing", output.dsc);
|
assert!(output.dsc.exists(), "{:?} missing", output.dsc);
|
||||||
|
|||||||
@@ -593,12 +593,13 @@ pub async fn offer_verification(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ui = Some(std::sync::Arc::new(crate::ui::deb::DebUi::new(multi)));
|
let ui = std::sync::Arc::new(crate::ui::deb::DebUi::new(multi));
|
||||||
if let Err(e) = crate::build::build_source_package(
|
if let Err(e) = crate::build::build_source_package(crate::build::BuildSourceOptions {
|
||||||
Some(&tree),
|
source: Some(tree.clone()),
|
||||||
crate::build::SourceBuildOptions::default(),
|
options: crate::build::SourceBuildOptions::default(),
|
||||||
ui,
|
view: &*ui,
|
||||||
) {
|
prompter: &crate::ui::prompt::TerminalPrompter,
|
||||||
|
}) {
|
||||||
log::error!("Verification source build failed: {e}");
|
log::error!("Verification source build failed: {e}");
|
||||||
log::info!(
|
log::info!(
|
||||||
"The scaffolded tree is intact. Inspect it, then retry with \
|
"The scaffolded tree is intact. Inspect it, then retry with \
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//! Environment-agnostic reporting ports.
|
||||||
|
//!
|
||||||
|
//! The core flows report progress and ask questions exclusively through the
|
||||||
|
//! traits in this module, so the same pipeline can drive a terminal live
|
||||||
|
//! view, a headless library consumer, or a remote frontend (e.g. a builder
|
||||||
|
//! server forwarding build events to a web UI over server-sent events): every
|
||||||
|
//! event carries plain data — strings, numbers, paths — with no terminal,
|
||||||
|
//! styling or locale assumptions. Adapters decide how events reach the user:
|
||||||
|
//! the terminal live view ([`crate::ui::deb::DebUi`]) renders them in place,
|
||||||
|
//! while another embedding maps each method onto its own wire format.
|
||||||
|
//!
|
||||||
|
//! Every [`BuildView`] method defaults to doing nothing, so implementations
|
||||||
|
//! only override the events they care about; [`Quiet`] provides the inert
|
||||||
|
//! implementations used by headless runs and tests. [`Prompter`] is
|
||||||
|
//! deliberately blocking: an implementation may round-trip each question to
|
||||||
|
//! a remote user, as long as it eventually answers (or takes the default).
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::context::LineSink;
|
||||||
|
use crate::logfmt::Classifier;
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
/// A named phase started (e.g. "Applying patches"). `classifier`
|
||||||
|
/// rewrites the phase's raw subprocess lines (see [`crate::logfmt`])
|
||||||
|
/// into display actions and countable progress; views that do not
|
||||||
|
/// rewrite lines locally can ignore it and forward raw lines from
|
||||||
|
/// [`BuildView::sink`] instead.
|
||||||
|
fn phase(&self, _name: &str, _classifier: Box<dyn Classifier>) {}
|
||||||
|
|
||||||
|
/// A status message about in-process work that produces no subprocess
|
||||||
|
/// output (e.g. "Generating .changes").
|
||||||
|
fn message(&self, _text: &str) {}
|
||||||
|
|
||||||
|
/// Determinate progress within the current phase (e.g. artifact
|
||||||
|
/// retrieval); `pos` runs from 0 to `total`.
|
||||||
|
fn progress(&self, _label: &str, _pos: usize, _total: usize) {}
|
||||||
|
|
||||||
|
/// Sink receiving every raw subprocess line while a build command runs,
|
||||||
|
/// when this view consumes the lines itself (live rewriting, tee to a
|
||||||
|
/// log file, forwarding over the network). `None` lets the caller fall
|
||||||
|
/// back to its default handling (e.g. test capture).
|
||||||
|
fn sink(&self) -> Option<Arc<dyn LineSink>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The build succeeded; `artifacts` lists the produced files in
|
||||||
|
/// distribution order (dsc → tarballs → buildinfo → changes).
|
||||||
|
fn finish_success(&self, _artifacts: &[PathBuf]) {}
|
||||||
|
|
||||||
|
/// The build failed; the view should release the display and may report
|
||||||
|
/// diagnostics it collected through [`BuildView::sink`].
|
||||||
|
fn finish_failure(&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).
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Answerer of the questions a core flow may ask mid-run (confirmations
|
||||||
|
/// now, selections and free-text input as more flows migrate).
|
||||||
|
///
|
||||||
|
/// Questions are blocking on purpose: a terminal implementation waits for
|
||||||
|
/// key presses, and a builder-server implementation may forward the question
|
||||||
|
/// to a web client and await the answer on a channel. Implementations that
|
||||||
|
/// cannot ask anyone answer with the question's default.
|
||||||
|
pub trait Prompter: Send + Sync {
|
||||||
|
/// Ask a yes/no question. `default` is the answer to take when no user
|
||||||
|
/// can be reached or the question is cancelled.
|
||||||
|
fn confirm(&self, _question: &str, default: bool) -> bool {
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inert view and prompter: drops every event and answers every question
|
||||||
|
/// with its default. The stand-in for headless library runs, verbose mode
|
||||||
|
/// and tests.
|
||||||
|
pub struct Quiet;
|
||||||
|
|
||||||
|
impl BuildView for Quiet {}
|
||||||
|
impl Prompter for Quiet {}
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
/// Live build view for `pkh deb` (status bar + rolling log pane)
|
/// Live build view for `pkh deb` (status bar + rolling log pane)
|
||||||
pub mod deb;
|
pub mod deb;
|
||||||
/// Line classifiers rewriting raw subprocess output for the live views
|
|
||||||
pub mod logfmt;
|
|
||||||
/// Interactive raw-mode prompts: free-text input, option selection and
|
/// Interactive raw-mode prompts: free-text input, option selection and
|
||||||
/// yes/no confirmation
|
/// yes/no confirmation
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
|
|||||||
+71
-27
@@ -3,7 +3,7 @@
|
|||||||
//! ("a terminal in the terminal").
|
//! ("a terminal in the terminal").
|
||||||
//!
|
//!
|
||||||
//! Subprocess output is captured through a [`LineSink`] implementation,
|
//! Subprocess output is captured through a [`LineSink`] implementation,
|
||||||
//! rewritten by classifiers ([`crate::ui::logfmt`]) and rendered in place
|
//! rewritten by classifiers ([`crate::logfmt`]) and rendered in place
|
||||||
//! with indicatif, so pkh's own log lines keep printing above the widget via
|
//! with indicatif, so pkh's own log lines keep printing above the widget via
|
||||||
//! `indicatif-log-bridge`. Every raw captured line is also tee'd to a log
|
//! `indicatif-log-bridge`. Every raw captured line is also tee'd to a log
|
||||||
//! file under the pkh cache directory.
|
//! file under the pkh cache directory.
|
||||||
@@ -21,7 +21,7 @@ use directories::ProjectDirs;
|
|||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
use crate::context::{LineSink, Stream};
|
use crate::context::{LineSink, Stream};
|
||||||
use crate::ui::logfmt::{
|
use crate::logfmt::{
|
||||||
Action, AptInstallClassifier, AptUpdateClassifier, Classifier, GenericClassifier,
|
Action, AptInstallClassifier, AptUpdateClassifier, Classifier, GenericClassifier,
|
||||||
MakeClassifier, MmdebstrapClassifier, QuiltClassifier,
|
MakeClassifier, MmdebstrapClassifier, QuiltClassifier,
|
||||||
};
|
};
|
||||||
@@ -130,8 +130,10 @@ struct Shared {
|
|||||||
/// Live build view for `pkh deb` / `pkh build`
|
/// Live build view for `pkh deb` / `pkh build`
|
||||||
///
|
///
|
||||||
/// Create one per build (disabled automatically when stdout is not a TTY or
|
/// Create one per build (disabled automatically when stdout is not a TTY or
|
||||||
/// when the user requests verbose output), pass it down as
|
/// when the user requests verbose output), then either pass it down through
|
||||||
/// `Option<Arc<DebUi>>`, and feed subprocess output through [`DebUi::sink`].
|
/// 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`].
|
||||||
pub struct DebUi {
|
pub struct DebUi {
|
||||||
shared: Arc<Shared>,
|
shared: Arc<Shared>,
|
||||||
}
|
}
|
||||||
@@ -213,17 +215,6 @@ impl DebUi {
|
|||||||
self.open_log("deb", package, version, &format!("for {series}/{arch}"));
|
self.open_log("deb", package, version, &format!("for {series}/{arch}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identify the source package being built; names the log file
|
|
||||||
/// (`build-<package>-<version>-<timestamp>.log`) and the status bar
|
|
||||||
pub fn set_build_target(&self, package: &str, version: &str, distribution: &str) {
|
|
||||||
if self.shared.enabled {
|
|
||||||
self.shared.top.set_prefix(format!(
|
|
||||||
"Building source package {package} ({version}) for {distribution}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.open_log("build", package, version, &format!("for {distribution}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rename the placeholder log file to include the build identity
|
/// Rename the placeholder log file to include the build identity
|
||||||
/// (best-effort), then open it so subsequent captured lines are tee'd
|
/// (best-effort), then open it so subsequent captured lines are tee'd
|
||||||
fn open_log(&self, kind: &str, package: &str, version: &str, detail: &str) {
|
fn open_log(&self, kind: &str, package: &str, version: &str, detail: &str) {
|
||||||
@@ -272,9 +263,10 @@ impl DebUi {
|
|||||||
self.phase_custom(phase.label(), classifier);
|
self.phase_custom(phase.label(), classifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to an arbitrary status label with a custom classifier; used by
|
/// Switch to an arbitrary status label with a custom classifier; the
|
||||||
/// flows whose phases are not part of [`Phase`] (e.g. source builds)
|
/// shared implementation behind [`DebUi::phase`], [`DebUi::phase_with`]
|
||||||
pub fn phase_custom(&self, label: &str, classifier: Box<dyn Classifier>) {
|
/// and the [`BuildView`] port
|
||||||
|
fn phase_custom(&self, label: &str, classifier: Box<dyn Classifier>) {
|
||||||
{
|
{
|
||||||
let mut st = self.shared.state.lock().unwrap();
|
let mut st = self.shared.state.lock().unwrap();
|
||||||
st.classifier = classifier;
|
st.classifier = classifier;
|
||||||
@@ -316,12 +308,6 @@ impl DebUi {
|
|||||||
self.shared.enabled && !self.shared.suspended.load(Ordering::SeqCst)
|
self.shared.enabled && !self.shared.suspended.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the widget renders at all (false on non-TTY stdout); callers
|
|
||||||
/// use this to fall back to plain-line summaries
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
|
||||||
self.shared.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain a sink feeding this view; pass it to `ContextCommand::capture`
|
/// Obtain a sink feeding this view; pass it to `ContextCommand::capture`
|
||||||
pub fn sink(self: &Arc<Self>) -> Arc<dyn LineSink> {
|
pub fn sink(self: &Arc<Self>) -> Arc<dyn LineSink> {
|
||||||
Arc::new(Sink {
|
Arc::new(Sink {
|
||||||
@@ -351,6 +337,18 @@ impl DebUi {
|
|||||||
/// Clear the widget and print a success summary with the artifacts,
|
/// Clear the widget and print a success summary with the artifacts,
|
||||||
/// rendered relative to the working directory when possible
|
/// rendered relative to the working directory when possible
|
||||||
pub fn finish_success(&self, artifacts: &[PathBuf], elapsed: Duration) {
|
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();
|
self.suspend();
|
||||||
if self.shared.enabled && !artifacts.is_empty() {
|
if self.shared.enabled && !artifacts.is_empty() {
|
||||||
println!("Built in {}s:", elapsed.as_secs());
|
println!("Built in {}s:", elapsed.as_secs());
|
||||||
@@ -360,9 +358,9 @@ impl DebUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the widget and print a failure summary (recent captured errors
|
/// Shared body of the failure outcome ([`DebUi::finish_failure`] and the
|
||||||
/// and the path to the full log)
|
/// [`BuildView`] port)
|
||||||
pub fn finish_failure(&self) {
|
fn failure_summary(&self) {
|
||||||
self.suspend();
|
self.suspend();
|
||||||
|
|
||||||
let st = self.shared.state.lock().unwrap();
|
let st = self.shared.state.lock().unwrap();
|
||||||
@@ -397,6 +395,52 @@ impl DebUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`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.
|
||||||
|
impl crate::report::BuildView for DebUi {
|
||||||
|
fn target(&self, package: &str, version: &str, target: &str) {
|
||||||
|
if self.shared.enabled {
|
||||||
|
self.shared.top.set_prefix(format!(
|
||||||
|
"Building source package {package} ({version}) for {target}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.open_log("build", package, version, &format!("for {target}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn phase(&self, name: &str, classifier: Box<dyn Classifier>) {
|
||||||
|
self.phase_custom(name, classifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(&self, text: &str) {
|
||||||
|
self.progress_message(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress(&self, label: &str, pos: usize, total: usize) {
|
||||||
|
self.count_progress(label, pos, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink(&self) -> Option<Arc<dyn LineSink>> {
|
||||||
|
Some(Arc::new(Sink {
|
||||||
|
shared: self.shared.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_success(&self, artifacts: &[PathBuf]) {
|
||||||
|
self.success_summary(artifacts, self.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_failure(&self) {
|
||||||
|
self.failure_summary();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.shared.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for DebUi {
|
impl Drop for DebUi {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Safety net: clear the widget on early returns/unwinds
|
// Safety net: clear the widget on early returns/unwinds
|
||||||
|
|||||||
@@ -127,6 +127,17 @@ pub fn confirm(label: &str, default: bool) -> Result<bool, Box<dyn std::error::E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`crate::report::Prompter`] answered by the interactive terminal: each
|
||||||
|
/// question runs the matching raw-mode prompt, falling back to the default
|
||||||
|
/// answer when no interactive terminal is attached (CI, piped input).
|
||||||
|
pub struct TerminalPrompter;
|
||||||
|
|
||||||
|
impl crate::report::Prompter for TerminalPrompter {
|
||||||
|
fn confirm(&self, question: &str, default: bool) -> bool {
|
||||||
|
confirm(question, default).unwrap_or(default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `prompt` with the terminal in raw mode, always restoring it
|
/// Run `prompt` with the terminal in raw mode, always restoring it
|
||||||
/// afterwards. Fails with [`PromptError::NotATty`] when raw mode cannot be
|
/// afterwards. Fails with [`PromptError::NotATty`] when raw mode cannot be
|
||||||
/// enabled.
|
/// enabled.
|
||||||
|
|||||||
Reference in New Issue
Block a user