pull,package_info,ui,cli: report the pull flow via BuildView

Retire the ProgressCallback option-of-closure, the one pre-port
reporting idiom left in the tree: pull and package_info::lookup now
take &dyn BuildView like every other core flow, and the announcements
map onto message (unsized work), progress (item counts: series probes,
git objects, extraction) and progress again for byte downloads, which
the old callback carried in the same 4-argument shape.

The terminal side swaps the (ProgressBar, closure) pair for PullView, a
BuildView implementation owning one bar of the shared MultiProgress
with the same spinner/transfer style switching; it releases the line on
drop, which the CLI triggers explicitly on both outcomes since
exit(1) skips destructors. Headless callers (tests, internal lookups)
pass &report::Quiet. git clone sideband parsing is skipped entirely
when the view is not enabled, matching the old None-callback shortcut.
This commit is contained in:
2026-10-03 02:17:22 +02:00
parent 9004a33bae
commit 8c8ee5beee
8 changed files with 203 additions and 191 deletions
+2 -2
View File
@@ -2284,13 +2284,13 @@ Provides: virtual-thing (= 2.0), plain-virtual
Some(dist),
None,
None,
None,
&crate::report::Quiet,
))
.unwrap_or_else(|e| panic!("package lookup failed for {}: {}", package, e));
rt.block_on(crate::pull::pull(
&package_info,
Some(fetch_dir.path()),
None,
&crate::report::Quiet,
true,
))
.unwrap_or_else(|e| panic!("pull failed for {}: {}", package, e));
+11 -3
View File
@@ -1323,11 +1323,19 @@ mod tests {
log::debug!("Created temporary directory: {}", cwd.display());
log::info!("Pulling package {} from {}...", package, series);
let package_info =
crate::package_info::lookup(package, None, Some(series), "", dist, None, None, None)
let package_info = crate::package_info::lookup(
package,
None,
Some(series),
"",
dist,
None,
None,
&crate::report::Quiet,
)
.await
.expect("Cannot lookup package information");
crate::pull::pull(&package_info, Some(cwd), None, true)
crate::pull::pull(&package_info, Some(cwd), &crate::report::Quiet, true)
.await
.expect("Cannot pull package");
log::info!("Successfully pulled package {}", package);
-7
View File
@@ -63,13 +63,6 @@ pub(crate) mod test_support;
/// Utility functions
pub(crate) mod utils;
/// Optional callback function (taking 4 arguments)
/// - Name of the current main operation (e.g. pulling package)
/// - Name of the current nested operation (e.g. cloning git repo)
/// - Progress, position, index of current operation (e.g. amount of data downloaded)
/// - Total amount for current operation (e.g. size of the file to download)
pub type ProgressCallback<'a> = Option<&'a dyn Fn(&str, &str, usize, usize)>;
/// Returns the architecture of current CPU, debian-compatible
pub fn get_current_arch() -> String {
match std::env::consts::ARCH {
+6 -6
View File
@@ -670,7 +670,7 @@ fn main() {
.unwrap_or("");
let archive = sub_matches.get_one::<bool>("archive").unwrap_or(&false);
let (pb, progress_callback) = pkh::ui::create_progress_bar(&multi);
let view = pkh::ui::PullView::new(&multi);
// Convert PPA to base URL if provided
let base_url = match ppa.map(pkh::package_info::split_ppa) {
@@ -692,17 +692,17 @@ fn main() {
dist,
base_url.as_deref(),
repository,
Some(&progress_callback),
&view,
)
.await?;
pkh::pull::pull(&package_info, None, Some(&progress_callback), *archive).await
pkh::pull::pull(&package_info, None, &view, *archive).await
}) {
pb.finish_and_clear();
// exit(1) below would skip the view's Drop
drop(view);
error!("{}", e);
std::process::exit(1);
}
pb.finish_and_clear();
multi.remove(&pb);
drop(view);
info!("Done.");
}
Some(("chlog", sub_matches)) => {
+32 -48
View File
@@ -4,8 +4,8 @@ use std::error::Error;
use std::io::Read;
use xz2::read::XzDecoder;
use crate::ProgressCallback;
use crate::apt::release::{self, VerifiedRelease};
use crate::report::BuildView;
use crossterm::style::Stylize;
use log::{debug, warn};
@@ -490,7 +490,7 @@ async fn find_package(
version: Option<&str>,
base_url: Option<&str>,
launchpad_vcs: Option<String>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<PackageInfo, Box<dyn Error>> {
let series_list = crate::distro_info::get_ordered_series_name(dist).await?;
@@ -500,9 +500,7 @@ async fn find_package(
let mut attempt_errors: Vec<String> = Vec::new();
for (i, series) in series_list.iter().enumerate() {
if let Some(cb) = progress {
cb("", &format!("Checking {}...", series), i, series_list.len());
}
view.progress(&format!("Checking {series}..."), i, series_list.len());
let pockets = if pocket.is_empty() {
crate::distro_info::get_dist_pockets(dist)?
@@ -770,16 +768,9 @@ pub async fn lookup_repository(
version: Option<&str>,
repo_url: &str,
series: Option<&str>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<PackageInfo, Box<dyn Error>> {
if let Some(cb) = progress {
cb(
&format!("Fetching repository info from {}...", repo_url),
"",
0,
0,
);
}
view.message(&format!("Fetching repository info from {repo_url}..."));
// Attempt to authenticate the repository. In non-strict mode only the
// absence of a Release file (or of a way to verify it) is tolerated:
@@ -814,14 +805,7 @@ pub async fn lookup_repository(
get_flat_repo_series(repo_url).await?
};
if let Some(cb) = progress {
cb(
&format!("Resolving package info for {}...", package),
"",
0,
0,
);
}
view.message(&format!("Resolving package info for {package}..."));
let sources = get_flat_repo_sources(repo_url, verified.as_ref()).await?;
let stanza = parse_sources(&sources, package, version)?
@@ -855,7 +839,7 @@ pub async fn lookup_repository(
/// * `dist` - Optional distribution name (e.g., "ubuntu", "debian")
/// * `base_url` - Optional base URL for the package archive (e.g., "https://ppa.launchpadcontent.net/user/ppa/ubuntu/")
/// * `repository` - Optional URL of an external flat repository suite (e.g., "https://pkg.noctalia.dev/deb/resolute/")
/// * `progress` - Optional progress callback
/// * `view` - Build view receiving the lookup progress
#[allow(clippy::too_many_arguments)]
pub async fn lookup(
package: &str,
@@ -865,12 +849,12 @@ pub async fn lookup(
dist: Option<&str>,
base_url: Option<&str>,
repository: Option<&str>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<PackageInfo, Box<dyn Error>> {
// External flat repository: the URL is the suite root, distribution
// resolution and pocket logic do not apply
if let Some(repo_url) = repository {
return lookup_repository(package, version, repo_url, series, progress).await;
return lookup_repository(package, version, repo_url, series, view).await;
}
// Resolve the distribution early so we can check Launchpad once
@@ -887,9 +871,7 @@ pub async fn lookup(
// Check Launchpad repository once for Ubuntu packages, rather than
// once per series/pocket combination inside get().
let launchpad_vcs = if resolved_dist == "ubuntu" && base_url.is_none() {
if let Some(cb) = progress {
cb("", &format!("Checking Launchpad for {}...", package), 0, 0);
}
view.message(&format!("Checking Launchpad for {package}..."));
match check_launchpad_repo(package).await {
Ok(Some(lp_url)) => {
debug!("Found Launchpad URL: {}", lp_url);
@@ -910,21 +892,12 @@ pub async fn lookup(
// Obtain the package information, either directly in a series or with a search in all series
let package_info = if let Some(s) = series {
if let Some(cb) = progress {
cb(
&format!("Resolving package info for {}...", package),
"",
0,
0,
);
}
view.message(&format!("Resolving package info for {package}..."));
// Get the package information from that series and pocket
get(package, s, pocket, version, base_url, launchpad_vcs).await?
} else {
if let Some(cb) = progress {
cb(
&format!(
view.message(&format!(
"Searching for package {} in {}...",
package,
if base_url.is_none() {
@@ -932,12 +905,7 @@ pub async fn lookup(
} else {
"ppa"
}
),
"",
0,
0,
);
}
));
// Try to find the package in all series from that dist
find_package(
@@ -947,7 +915,7 @@ pub async fn lookup(
version,
base_url,
launchpad_vcs,
progress,
view,
)
.await?
};
@@ -1142,7 +1110,15 @@ Directory: pool/main/h/hello
#[tokio::test]
async fn test_find_package_fallback() {
// python2.7 is in bullseye but not above
let info = find_package("python2.7", "debian", "", None, None, None, None)
let info = find_package(
"python2.7",
"debian",
"",
None,
None,
None,
&crate::report::Quiet,
)
.await
.unwrap();
assert_eq!(info.stanza.package, "python2.7");
@@ -1152,7 +1128,15 @@ Directory: pool/main/h/hello
#[tokio::test]
async fn test_find_package_devel() {
// hello is in sid
let info = find_package("hello", "debian", "", None, None, None, None)
let info = find_package(
"hello",
"debian",
"",
None,
None,
None,
&crate::report::Quiet,
)
.await
.unwrap();
assert_eq!(info.stanza.package, "hello");
+76 -73
View File
@@ -13,14 +13,14 @@ use log::debug;
use regex::Regex;
use crate::ProgressCallback;
use crate::report::BuildView;
fn clone_repo(
url: &str,
package: &str,
branch: Option<&str>,
cwd: Option<&Path>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
let target_path = if let Some(path) = cwd {
path.join(package)
@@ -29,11 +29,11 @@ fn clone_repo(
};
let mut callbacks = git2::RemoteCallbacks::new();
if let Some(ref progress_cb) = progress {
// The sideband parser is only worth running when something listens
if view.is_enabled() {
// Download progress
callbacks.transfer_progress(move |stats| {
(progress_cb)(
"",
view.progress(
"Receiving objects...",
stats.received_objects(),
stats.total_objects(),
@@ -42,7 +42,7 @@ fn clone_repo(
});
// Remote progress: messages 'Remote: compressing objects 10% (34/340)'
// Parse progress informations to display them in callbacks
// Parse progress informations to display them in the view
callbacks.sideband_progress(move |data| {
let msg = String::from_utf8_lossy(data);
let re = Regex::new(r"(.*):[ ]*([0-9]*)% \(([0-9]*)/([0-9]*)\)").unwrap();
@@ -61,7 +61,7 @@ fn clone_repo(
.parse::<usize>()
.unwrap_or(0);
(progress_cb)("", msg.as_str(), objects, total);
view.progress(msg.as_str(), objects, total);
}
true
@@ -232,7 +232,7 @@ struct ExtractedArchive {
fn extract_tar_archive<D, F>(
file_path: &Path,
dest: &Path,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
decoder_factory: F,
) -> Result<ExtractedArchive, Box<dyn Error>>
where
@@ -373,9 +373,7 @@ where
current_entry += 1;
// Report progress
if let Some(cb) = progress {
cb("", "Extracting...", current_entry, total_entries);
}
view.progress("Extracting...", current_entry, total_entries);
}
let wrapper = if multiple_roots {
@@ -389,16 +387,16 @@ where
fn extract_archive(
path: &Path,
dest: &Path,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<ExtractedArchive, Box<dyn Error>> {
let filename = path.file_name().unwrap().to_string_lossy();
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
extract_tar_archive(path, dest, progress, GzDecoder::new)
extract_tar_archive(path, dest, view, GzDecoder::new)
} else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
extract_tar_archive(path, dest, progress, XzDecoder::new)
extract_tar_archive(path, dest, view, XzDecoder::new)
} else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
extract_tar_archive(path, dest, progress, bzip2_rs::DecoderReader::new)
extract_tar_archive(path, dest, view, bzip2_rs::DecoderReader::new)
} else {
Err(format!("Unsupported archive format: {}", filename).into())
}
@@ -439,7 +437,7 @@ async fn download_file_checksum(
checksum: &str,
algo: crate::package_info::ChecksumAlgo,
target_dir: &Path,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
// Archive mirrors and CDNs are busy enough that single attempts fail
// spuriously (dropped connections, truncated bodies, index generations
@@ -448,7 +446,7 @@ async fn download_file_checksum(
const ATTEMPTS: u32 = 3;
let mut last_error: Box<dyn Error> = String::new().into();
for attempt in 1..=ATTEMPTS {
match download_file_checksum_once(url, checksum, algo, target_dir, progress).await {
match download_file_checksum_once(url, checksum, algo, target_dir, view).await {
Ok(()) => return Ok(()),
Err(e) => {
log::warn!("download of '{url}' failed (attempt {attempt}/{ATTEMPTS}): {e}");
@@ -468,7 +466,7 @@ async fn download_file_checksum_once(
checksum: &str,
algo: crate::package_info::ChecksumAlgo,
target_dir: &Path,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
// Download with the shared client (connect timeout). Large orig tarballs
// can legitimately take longer than the client's default total timeout,
@@ -504,9 +502,9 @@ async fn download_file_checksum_once(
file.write_all(&chunk)?;
buffer.extend_from_slice(&chunk);
if let (Some(cb), Some(total)) = (progress, total_size) {
if let Some(total) = total_size {
let index = min(buffer.len(), total as usize);
cb("", "Downloading...", index, total as usize);
view.progress("Downloading...", index, total as usize);
}
}
@@ -575,7 +573,7 @@ fn setup_pristine_tar_branch(package_dir: &Path, dist: &str) -> Result<(), Box<d
async fn fetch_orig_tarball(
info: &PackageInfo,
cwd: Option<&Path>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
let package_dir = if let Some(path) = cwd {
path.join(&info.stanza.package)
@@ -630,7 +628,7 @@ async fn fetch_orig_tarball(
&orig_file.checksum,
orig_file.checksum_algo,
target_dir,
progress,
view,
)
.await?;
}
@@ -642,7 +640,7 @@ async fn fetch_orig_tarball(
async fn fetch_dsc_file(
info: &PackageInfo,
cwd: Option<&Path>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
let target_dir = cwd.unwrap_or_else(|| Path::new("."));
@@ -662,7 +660,7 @@ async fn fetch_dsc_file(
&dsc_file.checksum,
dsc_file.checksum_algo,
target_dir,
progress,
view,
)
.await?;
@@ -672,7 +670,7 @@ async fn fetch_dsc_file(
async fn fetch_archive_sources(
info: &PackageInfo,
cwd: Option<&Path>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> {
let package_dir = if let Some(path) = cwd {
path
@@ -702,14 +700,7 @@ async fn fetch_archive_sources(
for file in &files {
let url = format!("{}/{}", info.archive_url, file.name);
download_file_checksum(
&url,
&file.checksum,
file.checksum_algo,
package_dir,
progress,
)
.await?;
download_file_checksum(&url, &file.checksum, file.checksum_algo, package_dir, view).await?;
// Extract all tar archives. Every tar of the source package matches
// (orig, debian tar, native tar, extra components); a tarball in an
@@ -725,7 +716,7 @@ async fn fetch_archive_sources(
let path = package_dir.join(&file.name);
let extract_dir = package_dir.join(&info.stanza.package);
let extracted = extract_archive(&path, &extract_dir, progress)?;
let extracted = extract_archive(&path, &extract_dir, view)?;
// Special case: the debian tar does only contain 'debian'
if file.name.contains("debian.tar.") {
@@ -807,7 +798,7 @@ async fn fetch_archive_sources(
pub async fn pull(
package_info: &PackageInfo,
cwd: Option<&Path>,
progress: ProgressCallback<'_>,
view: &dyn BuildView,
force_archive: bool,
) -> Result<(), Box<dyn Error>> {
let package = &package_info.stanza.package;
@@ -861,9 +852,7 @@ pub async fn pull(
None
};
if let Some(cb) = progress {
cb(
&format!(
view.message(&format!(
"Cloning {}{}...",
url,
if let Some(b) = &branch_name {
@@ -871,40 +860,29 @@ pub async fn pull(
} else {
String::new()
}
),
"",
0,
0,
);
}
));
clone_repo(
url.as_str(),
package,
branch_name.as_deref(),
Some(&package_dir),
progress,
view,
)?;
if !package_info.is_native() {
if let Some(cb) = progress {
cb("Fetching orig tarball...", "", 0, 0);
}
fetch_orig_tarball(package_info, Some(&package_dir), progress).await?;
view.message("Fetching orig tarball...");
fetch_orig_tarball(package_info, Some(&package_dir), view).await?;
} else {
debug!("Native package, skipping orig tarball fetch.");
}
if let Some(cb) = progress {
cb("Fetching dsc file...", "", 0, 0);
}
fetch_dsc_file(package_info, Some(&package_dir), progress).await?;
view.message("Fetching dsc file...");
fetch_dsc_file(package_info, Some(&package_dir), view).await?;
} else {
// Fallback to archive fetching
if let Some(cb) = progress {
cb("Downloading from archive...", "", 0, 0);
}
fetch_archive_sources(package_info, Some(&package_dir), progress).await?;
view.message("Downloading from archive...");
fetch_archive_sources(package_info, Some(&package_dir), view).await?;
}
Ok(())
@@ -931,10 +909,24 @@ mod tests {
let cwd = temp_dir.path();
// Main 'pull' command: the one we want to test
let info = crate::package_info::lookup(package, None, series, "", dist, None, None, None)
let info = crate::package_info::lookup(
package,
None,
series,
"",
dist,
None,
None,
&crate::report::Quiet,
)
.await
.unwrap();
pull(&info, Some(cwd), None, archive.unwrap_or(false))
pull(
&info,
Some(cwd),
&crate::report::Quiet,
archive.unwrap_or(false),
)
.await
.unwrap();
@@ -1136,7 +1128,7 @@ mod tests {
.unwrap();
let dest = temp_dir.path().join("dest");
let result = extract_tar_archive(&tar_path, &dest, None, |f| f);
let result = extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f);
let err = match result {
Ok(_) => panic!("extraction of a traversal archive should fail"),
@@ -1169,7 +1161,7 @@ mod tests {
.unwrap();
let dest = temp_dir.path().join("dest");
let result = extract_tar_archive(&tar_path, &dest, None, |f| f);
let result = extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f);
let err = match result {
Ok(_) => panic!("extraction of an absolute-path archive should fail"),
@@ -1202,7 +1194,7 @@ mod tests {
.unwrap();
let dest = temp_dir.path().join("dest");
extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap();
extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f).unwrap();
assert_eq!(
std::fs::read_to_string(dest.join("hello-1.0/file.txt")).unwrap(),
@@ -1235,7 +1227,7 @@ mod tests {
std::fs::write(&tar_path, tar_bytes).unwrap();
let dest = temp_dir.path().join("dest");
extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap();
extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f).unwrap();
assert_eq!(
std::fs::read_to_string(dest.join("pkg-1.0/docs/v1/manual.txt")).unwrap(),
@@ -1276,7 +1268,8 @@ mod tests {
std::fs::write(&tar_path, tar_bytes).unwrap();
let tree = temp_dir.path().join("tree");
let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
let extracted =
extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap();
unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
assert!(!tree.join("pax_global_header").exists());
@@ -1297,7 +1290,7 @@ mod tests {
std::fs::write(&tar_path, tar_bytes).unwrap();
let dest = temp_dir.path().join("dest");
let result = extract_tar_archive(&tar_path, &dest, None, |f| f);
let result = extract_tar_archive(&tar_path, &dest, &crate::report::Quiet, |f| f);
let err = match result {
Ok(_) => panic!("extraction of an escaping hard link should fail"),
@@ -1332,7 +1325,8 @@ mod tests {
.unwrap();
let tree = temp_dir.path().join("tree");
let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
let extracted =
extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap();
unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
assert_eq!(
@@ -1366,7 +1360,8 @@ mod tests {
std::fs::write(&tar_path, tar_bytes).unwrap();
let tree = temp_dir.path().join("tree");
let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
let extracted =
extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap();
unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
let meta = std::fs::symlink_metadata(tree.join("README")).unwrap();
@@ -1392,7 +1387,8 @@ mod tests {
.unwrap();
let tree = temp_dir.path().join("tree");
let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
let extracted =
extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap();
assert!(extracted.wrapper.is_none());
assert!(tree.join("Makefile").exists());
@@ -1413,7 +1409,7 @@ mod tests {
let main = temp_dir.path().join("main.orig.tar");
std::fs::write(&main, build_tar(&[("dovecot-2.4.5/README", b"main")])).unwrap();
let extracted = extract_tar_archive(&main, &tree, None, |f| f).unwrap();
let extracted = extract_tar_archive(&main, &tree, &crate::report::Quiet, |f| f).unwrap();
unwrap_into(
&tree,
&extracted.wrapper.unwrap(),
@@ -1430,7 +1426,8 @@ mod tests {
]),
)
.unwrap();
let extracted = extract_tar_archive(&component, &tree, None, |f| f).unwrap();
let extracted =
extract_tar_archive(&component, &tree, &crate::report::Quiet, |f| f).unwrap();
unwrap_into(
&tree,
&extracted.wrapper.unwrap(),
@@ -1472,7 +1469,8 @@ mod tests {
.unwrap();
let tree = temp_dir.path().join("tree");
let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
let extracted =
extract_tar_archive(&tar_path, &tree, &crate::report::Quiet, |f| f).unwrap();
unwrap_into(
&tree,
&extracted.wrapper.unwrap(),
@@ -1512,8 +1510,13 @@ mod tests {
std::fs::write(&tar_path, BZ2_TAR_WRAPPED).unwrap();
let tree = temp_dir.path().join("tree");
let extracted =
extract_tar_archive(&tar_path, &tree, None, bzip2_rs::DecoderReader::new).unwrap();
let extracted = extract_tar_archive(
&tar_path,
&tree,
&crate::report::Quiet,
bzip2_rs::DecoderReader::new,
)
.unwrap();
unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
assert_eq!(
+6 -4
View File
@@ -68,10 +68,12 @@ pub struct BuildTarget<'a> {
/// Observer of a running build: target identification, phases, status
/// messages, progress and the final outcome.
///
/// 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.
/// Implement this to observe the [`crate::build`], [`crate::deb`] and
/// [`crate::pull`] 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. The pull flow only emits the
/// [`BuildView::message`], [`BuildView::progress`] and
/// [`BuildView::progress`] events — no target, phases or outcome.
pub trait BuildView: Send + Sync {
/// The build target was identified; the events that follow belong to it
/// (including the earliest subprocess output, e.g. a chroot download).
+42 -20
View File
@@ -51,34 +51,56 @@ pub fn wait_spinner(msg: &str) -> ProgressBar {
pb
}
/// Create a spinner-style progress bar attached to `multi`, returning the bar
/// and a callback compatible with [`crate::ProgressCallback`]
pub fn create_progress_bar(
multi: &MultiProgress,
) -> (ProgressBar, impl Fn(&str, &str, usize, usize) + '_) {
/// Terminal build view for the pull flow: one bar line attached to the
/// shared [`MultiProgress`], spinner-style while the work is unsized,
/// transfer-style once a byte or item total is known. The bar is created
/// eagerly (an empty spinner, so the line exists from the first moment)
/// and released on drop.
pub struct PullView {
multi: MultiProgress,
pb: ProgressBar,
}
impl PullView {
/// Attach a pull view to `multi`
pub fn new(multi: &MultiProgress) -> Self {
let pb = multi.add(ProgressBar::new(0));
pb.enable_steady_tick(Duration::from_millis(50));
pb.set_style(spinner_style());
Self {
multi: multi.clone(),
pb,
}
}
}
let pb_clone = pb.clone();
let callback = move |prefix: &str, msg: &str, progress: usize, total: usize| {
let pb = &pb_clone;
if progress != 0 && total != 0 {
pb.set_style(transfer_style());
impl Drop for PullView {
fn drop(&mut self) {
self.pb.finish_and_clear();
self.multi.remove(&self.pb);
}
}
impl crate::report::BuildView for PullView {
fn message(&self, text: &str) {
self.pb.set_style(spinner_style());
self.pb.set_message(text.to_string());
}
fn progress(&self, label: &str, pos: usize, total: usize) {
if pos != 0 && total != 0 {
self.pb.set_style(transfer_style());
} else {
pb.set_style(spinner_style());
self.pb.set_style(spinner_style());
}
self.pb.set_message(label.to_string());
self.pb.set_length(total as u64);
self.pb.set_position(pos as u64);
}
if !prefix.is_empty() {
pb.set_prefix(prefix.to_string());
fn is_enabled(&self) -> bool {
true
}
pb.set_message(msg.to_string());
pb.set_length(total as u64);
pb.set_position(progress as u64);
};
(pb, callback)
}
/// Label of the interactive series selector, rendered by [`prompt::select`]