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), Some(dist),
None, None,
None, None,
None, &crate::report::Quiet,
)) ))
.unwrap_or_else(|e| panic!("package lookup failed for {}: {}", package, e)); .unwrap_or_else(|e| panic!("package lookup failed for {}: {}", package, e));
rt.block_on(crate::pull::pull( rt.block_on(crate::pull::pull(
&package_info, &package_info,
Some(fetch_dir.path()), Some(fetch_dir.path()),
None, &crate::report::Quiet,
true, true,
)) ))
.unwrap_or_else(|e| panic!("pull failed for {}: {}", package, e)); .unwrap_or_else(|e| panic!("pull failed for {}: {}", package, e));
+13 -5
View File
@@ -1323,11 +1323,19 @@ mod tests {
log::debug!("Created temporary directory: {}", cwd.display()); log::debug!("Created temporary directory: {}", cwd.display());
log::info!("Pulling package {} from {}...", package, series); log::info!("Pulling package {} from {}...", package, series);
let package_info = let package_info = crate::package_info::lookup(
crate::package_info::lookup(package, None, Some(series), "", dist, None, None, None) package,
.await None,
.expect("Cannot lookup package information"); Some(series),
crate::pull::pull(&package_info, Some(cwd), None, true) "",
dist,
None,
None,
&crate::report::Quiet,
)
.await
.expect("Cannot lookup package information");
crate::pull::pull(&package_info, Some(cwd), &crate::report::Quiet, true)
.await .await
.expect("Cannot pull package"); .expect("Cannot pull package");
log::info!("Successfully pulled package {}", package); log::info!("Successfully pulled package {}", package);
-7
View File
@@ -63,13 +63,6 @@ pub(crate) mod test_support;
/// Utility functions /// Utility functions
pub(crate) mod utils; 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 /// Returns the architecture of current CPU, debian-compatible
pub fn get_current_arch() -> String { pub fn get_current_arch() -> String {
match std::env::consts::ARCH { match std::env::consts::ARCH {
+6 -6
View File
@@ -670,7 +670,7 @@ fn main() {
.unwrap_or(""); .unwrap_or("");
let archive = sub_matches.get_one::<bool>("archive").unwrap_or(&false); 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 // Convert PPA to base URL if provided
let base_url = match ppa.map(pkh::package_info::split_ppa) { let base_url = match ppa.map(pkh::package_info::split_ppa) {
@@ -692,17 +692,17 @@ fn main() {
dist, dist,
base_url.as_deref(), base_url.as_deref(),
repository, repository,
Some(&progress_callback), &view,
) )
.await?; .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); error!("{}", e);
std::process::exit(1); std::process::exit(1);
} }
pb.finish_and_clear(); drop(view);
multi.remove(&pb);
info!("Done."); info!("Done.");
} }
Some(("chlog", sub_matches)) => { Some(("chlog", sub_matches)) => {
+43 -59
View File
@@ -4,8 +4,8 @@ use std::error::Error;
use std::io::Read; use std::io::Read;
use xz2::read::XzDecoder; use xz2::read::XzDecoder;
use crate::ProgressCallback;
use crate::apt::release::{self, VerifiedRelease}; use crate::apt::release::{self, VerifiedRelease};
use crate::report::BuildView;
use crossterm::style::Stylize; use crossterm::style::Stylize;
use log::{debug, warn}; use log::{debug, warn};
@@ -490,7 +490,7 @@ async fn find_package(
version: Option<&str>, version: Option<&str>,
base_url: Option<&str>, base_url: Option<&str>,
launchpad_vcs: Option<String>, launchpad_vcs: Option<String>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<PackageInfo, Box<dyn Error>> { ) -> Result<PackageInfo, Box<dyn Error>> {
let series_list = crate::distro_info::get_ordered_series_name(dist).await?; 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(); let mut attempt_errors: Vec<String> = Vec::new();
for (i, series) in series_list.iter().enumerate() { for (i, series) in series_list.iter().enumerate() {
if let Some(cb) = progress { view.progress(&format!("Checking {series}..."), i, series_list.len());
cb("", &format!("Checking {}...", series), i, series_list.len());
}
let pockets = if pocket.is_empty() { let pockets = if pocket.is_empty() {
crate::distro_info::get_dist_pockets(dist)? crate::distro_info::get_dist_pockets(dist)?
@@ -770,16 +768,9 @@ pub async fn lookup_repository(
version: Option<&str>, version: Option<&str>,
repo_url: &str, repo_url: &str,
series: Option<&str>, series: Option<&str>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<PackageInfo, Box<dyn Error>> { ) -> Result<PackageInfo, Box<dyn Error>> {
if let Some(cb) = progress { view.message(&format!("Fetching repository info from {repo_url}..."));
cb(
&format!("Fetching repository info from {}...", repo_url),
"",
0,
0,
);
}
// Attempt to authenticate the repository. In non-strict mode only the // 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: // 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? get_flat_repo_series(repo_url).await?
}; };
if let Some(cb) = progress { view.message(&format!("Resolving package info for {package}..."));
cb(
&format!("Resolving package info for {}...", package),
"",
0,
0,
);
}
let sources = get_flat_repo_sources(repo_url, verified.as_ref()).await?; let sources = get_flat_repo_sources(repo_url, verified.as_ref()).await?;
let stanza = parse_sources(&sources, package, version)? let stanza = parse_sources(&sources, package, version)?
@@ -855,7 +839,7 @@ pub async fn lookup_repository(
/// * `dist` - Optional distribution name (e.g., "ubuntu", "debian") /// * `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/") /// * `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/") /// * `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)] #[allow(clippy::too_many_arguments)]
pub async fn lookup( pub async fn lookup(
package: &str, package: &str,
@@ -865,12 +849,12 @@ pub async fn lookup(
dist: Option<&str>, dist: Option<&str>,
base_url: Option<&str>, base_url: Option<&str>,
repository: Option<&str>, repository: Option<&str>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<PackageInfo, Box<dyn Error>> { ) -> Result<PackageInfo, Box<dyn Error>> {
// External flat repository: the URL is the suite root, distribution // External flat repository: the URL is the suite root, distribution
// resolution and pocket logic do not apply // resolution and pocket logic do not apply
if let Some(repo_url) = repository { 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 // 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 // Check Launchpad repository once for Ubuntu packages, rather than
// once per series/pocket combination inside get(). // once per series/pocket combination inside get().
let launchpad_vcs = if resolved_dist == "ubuntu" && base_url.is_none() { let launchpad_vcs = if resolved_dist == "ubuntu" && base_url.is_none() {
if let Some(cb) = progress { view.message(&format!("Checking Launchpad for {package}..."));
cb("", &format!("Checking Launchpad for {}...", package), 0, 0);
}
match check_launchpad_repo(package).await { match check_launchpad_repo(package).await {
Ok(Some(lp_url)) => { Ok(Some(lp_url)) => {
debug!("Found Launchpad URL: {}", lp_url); debug!("Found Launchpad URL: {}", lp_url);
@@ -910,34 +892,20 @@ pub async fn lookup(
// Obtain the package information, either directly in a series or with a search in all series // Obtain the package information, either directly in a series or with a search in all series
let package_info = if let Some(s) = series { let package_info = if let Some(s) = series {
if let Some(cb) = progress { view.message(&format!("Resolving package info for {package}..."));
cb(
&format!("Resolving package info for {}...", package),
"",
0,
0,
);
}
// Get the package information from that series and pocket // Get the package information from that series and pocket
get(package, s, pocket, version, base_url, launchpad_vcs).await? get(package, s, pocket, version, base_url, launchpad_vcs).await?
} else { } else {
if let Some(cb) = progress { view.message(&format!(
cb( "Searching for package {} in {}...",
&format!( package,
"Searching for package {} in {}...", if base_url.is_none() {
package, resolved_dist
if base_url.is_none() { } else {
resolved_dist "ppa"
} else { }
"ppa" ));
}
),
"",
0,
0,
);
}
// Try to find the package in all series from that dist // Try to find the package in all series from that dist
find_package( find_package(
@@ -947,7 +915,7 @@ pub async fn lookup(
version, version,
base_url, base_url,
launchpad_vcs, launchpad_vcs,
progress, view,
) )
.await? .await?
}; };
@@ -1142,9 +1110,17 @@ Directory: pool/main/h/hello
#[tokio::test] #[tokio::test]
async fn test_find_package_fallback() { async fn test_find_package_fallback() {
// python2.7 is in bullseye but not above // python2.7 is in bullseye but not above
let info = find_package("python2.7", "debian", "", None, None, None, None) let info = find_package(
.await "python2.7",
.unwrap(); "debian",
"",
None,
None,
None,
&crate::report::Quiet,
)
.await
.unwrap();
assert_eq!(info.stanza.package, "python2.7"); assert_eq!(info.stanza.package, "python2.7");
assert_eq!(info.series, "bullseye") assert_eq!(info.series, "bullseye")
} }
@@ -1152,9 +1128,17 @@ Directory: pool/main/h/hello
#[tokio::test] #[tokio::test]
async fn test_find_package_devel() { async fn test_find_package_devel() {
// hello is in sid // hello is in sid
let info = find_package("hello", "debian", "", None, None, None, None) let info = find_package(
.await "hello",
.unwrap(); "debian",
"",
None,
None,
None,
&crate::report::Quiet,
)
.await
.unwrap();
assert_eq!(info.stanza.package, "hello"); assert_eq!(info.stanza.package, "hello");
assert_eq!(info.series, "sid") assert_eq!(info.series, "sid")
} }
+87 -84
View File
@@ -13,14 +13,14 @@ use log::debug;
use regex::Regex; use regex::Regex;
use crate::ProgressCallback; use crate::report::BuildView;
fn clone_repo( fn clone_repo(
url: &str, url: &str,
package: &str, package: &str,
branch: Option<&str>, branch: Option<&str>,
cwd: Option<&Path>, cwd: Option<&Path>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let target_path = if let Some(path) = cwd { let target_path = if let Some(path) = cwd {
path.join(package) path.join(package)
@@ -29,11 +29,11 @@ fn clone_repo(
}; };
let mut callbacks = git2::RemoteCallbacks::new(); 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 // Download progress
callbacks.transfer_progress(move |stats| { callbacks.transfer_progress(move |stats| {
(progress_cb)( view.progress(
"",
"Receiving objects...", "Receiving objects...",
stats.received_objects(), stats.received_objects(),
stats.total_objects(), stats.total_objects(),
@@ -42,7 +42,7 @@ fn clone_repo(
}); });
// Remote progress: messages 'Remote: compressing objects 10% (34/340)' // 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| { callbacks.sideband_progress(move |data| {
let msg = String::from_utf8_lossy(data); let msg = String::from_utf8_lossy(data);
let re = Regex::new(r"(.*):[ ]*([0-9]*)% \(([0-9]*)/([0-9]*)\)").unwrap(); let re = Regex::new(r"(.*):[ ]*([0-9]*)% \(([0-9]*)/([0-9]*)\)").unwrap();
@@ -61,7 +61,7 @@ fn clone_repo(
.parse::<usize>() .parse::<usize>()
.unwrap_or(0); .unwrap_or(0);
(progress_cb)("", msg.as_str(), objects, total); view.progress(msg.as_str(), objects, total);
} }
true true
@@ -232,7 +232,7 @@ struct ExtractedArchive {
fn extract_tar_archive<D, F>( fn extract_tar_archive<D, F>(
file_path: &Path, file_path: &Path,
dest: &Path, dest: &Path,
progress: ProgressCallback<'_>, view: &dyn BuildView,
decoder_factory: F, decoder_factory: F,
) -> Result<ExtractedArchive, Box<dyn Error>> ) -> Result<ExtractedArchive, Box<dyn Error>>
where where
@@ -373,9 +373,7 @@ where
current_entry += 1; current_entry += 1;
// Report progress // Report progress
if let Some(cb) = progress { view.progress("Extracting...", current_entry, total_entries);
cb("", "Extracting...", current_entry, total_entries);
}
} }
let wrapper = if multiple_roots { let wrapper = if multiple_roots {
@@ -389,16 +387,16 @@ where
fn extract_archive( fn extract_archive(
path: &Path, path: &Path,
dest: &Path, dest: &Path,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<ExtractedArchive, Box<dyn Error>> { ) -> Result<ExtractedArchive, Box<dyn Error>> {
let filename = path.file_name().unwrap().to_string_lossy(); let filename = path.file_name().unwrap().to_string_lossy();
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") { 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") { } 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") { } 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 { } else {
Err(format!("Unsupported archive format: {}", filename).into()) Err(format!("Unsupported archive format: {}", filename).into())
} }
@@ -439,7 +437,7 @@ async fn download_file_checksum(
checksum: &str, checksum: &str,
algo: crate::package_info::ChecksumAlgo, algo: crate::package_info::ChecksumAlgo,
target_dir: &Path, target_dir: &Path,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Archive mirrors and CDNs are busy enough that single attempts fail // Archive mirrors and CDNs are busy enough that single attempts fail
// spuriously (dropped connections, truncated bodies, index generations // spuriously (dropped connections, truncated bodies, index generations
@@ -448,7 +446,7 @@ async fn download_file_checksum(
const ATTEMPTS: u32 = 3; const ATTEMPTS: u32 = 3;
let mut last_error: Box<dyn Error> = String::new().into(); let mut last_error: Box<dyn Error> = String::new().into();
for attempt in 1..=ATTEMPTS { 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(()), Ok(()) => return Ok(()),
Err(e) => { Err(e) => {
log::warn!("download of '{url}' failed (attempt {attempt}/{ATTEMPTS}): {e}"); log::warn!("download of '{url}' failed (attempt {attempt}/{ATTEMPTS}): {e}");
@@ -468,7 +466,7 @@ async fn download_file_checksum_once(
checksum: &str, checksum: &str,
algo: crate::package_info::ChecksumAlgo, algo: crate::package_info::ChecksumAlgo,
target_dir: &Path, target_dir: &Path,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Download with the shared client (connect timeout). Large orig tarballs // Download with the shared client (connect timeout). Large orig tarballs
// can legitimately take longer than the client's default total timeout, // 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)?; file.write_all(&chunk)?;
buffer.extend_from_slice(&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); 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( async fn fetch_orig_tarball(
info: &PackageInfo, info: &PackageInfo,
cwd: Option<&Path>, cwd: Option<&Path>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let package_dir = if let Some(path) = cwd { let package_dir = if let Some(path) = cwd {
path.join(&info.stanza.package) path.join(&info.stanza.package)
@@ -630,7 +628,7 @@ async fn fetch_orig_tarball(
&orig_file.checksum, &orig_file.checksum,
orig_file.checksum_algo, orig_file.checksum_algo,
target_dir, target_dir,
progress, view,
) )
.await?; .await?;
} }
@@ -642,7 +640,7 @@ async fn fetch_orig_tarball(
async fn fetch_dsc_file( async fn fetch_dsc_file(
info: &PackageInfo, info: &PackageInfo,
cwd: Option<&Path>, cwd: Option<&Path>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let target_dir = cwd.unwrap_or_else(|| Path::new(".")); let target_dir = cwd.unwrap_or_else(|| Path::new("."));
@@ -662,7 +660,7 @@ async fn fetch_dsc_file(
&dsc_file.checksum, &dsc_file.checksum,
dsc_file.checksum_algo, dsc_file.checksum_algo,
target_dir, target_dir,
progress, view,
) )
.await?; .await?;
@@ -672,7 +670,7 @@ async fn fetch_dsc_file(
async fn fetch_archive_sources( async fn fetch_archive_sources(
info: &PackageInfo, info: &PackageInfo,
cwd: Option<&Path>, cwd: Option<&Path>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let package_dir = if let Some(path) = cwd { let package_dir = if let Some(path) = cwd {
path path
@@ -702,14 +700,7 @@ async fn fetch_archive_sources(
for file in &files { for file in &files {
let url = format!("{}/{}", info.archive_url, file.name); let url = format!("{}/{}", info.archive_url, file.name);
download_file_checksum( download_file_checksum(&url, &file.checksum, file.checksum_algo, package_dir, view).await?;
&url,
&file.checksum,
file.checksum_algo,
package_dir,
progress,
)
.await?;
// Extract all tar archives. Every tar of the source package matches // Extract all tar archives. Every tar of the source package matches
// (orig, debian tar, native tar, extra components); a tarball in an // (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 path = package_dir.join(&file.name);
let extract_dir = package_dir.join(&info.stanza.package); 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' // Special case: the debian tar does only contain 'debian'
if file.name.contains("debian.tar.") { if file.name.contains("debian.tar.") {
@@ -807,7 +798,7 @@ async fn fetch_archive_sources(
pub async fn pull( pub async fn pull(
package_info: &PackageInfo, package_info: &PackageInfo,
cwd: Option<&Path>, cwd: Option<&Path>,
progress: ProgressCallback<'_>, view: &dyn BuildView,
force_archive: bool, force_archive: bool,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let package = &package_info.stanza.package; let package = &package_info.stanza.package;
@@ -861,50 +852,37 @@ pub async fn pull(
None None
}; };
if let Some(cb) = progress { view.message(&format!(
cb( "Cloning {}{}...",
&format!( url,
"Cloning {}{}...", if let Some(b) = &branch_name {
url, format!(" (branch {})", b)
if let Some(b) = &branch_name { } else {
format!(" (branch {})", b) String::new()
} else { }
String::new() ));
}
),
"",
0,
0,
);
}
clone_repo( clone_repo(
url.as_str(), url.as_str(),
package, package,
branch_name.as_deref(), branch_name.as_deref(),
Some(&package_dir), Some(&package_dir),
progress, view,
)?; )?;
if !package_info.is_native() { if !package_info.is_native() {
if let Some(cb) = progress { view.message("Fetching orig tarball...");
cb("Fetching orig tarball...", "", 0, 0); fetch_orig_tarball(package_info, Some(&package_dir), view).await?;
}
fetch_orig_tarball(package_info, Some(&package_dir), progress).await?;
} else { } else {
debug!("Native package, skipping orig tarball fetch."); debug!("Native package, skipping orig tarball fetch.");
} }
if let Some(cb) = progress { view.message("Fetching dsc file...");
cb("Fetching dsc file...", "", 0, 0); fetch_dsc_file(package_info, Some(&package_dir), view).await?;
}
fetch_dsc_file(package_info, Some(&package_dir), progress).await?;
} else { } else {
// Fallback to archive fetching // Fallback to archive fetching
if let Some(cb) = progress { view.message("Downloading from archive...");
cb("Downloading from archive...", "", 0, 0); fetch_archive_sources(package_info, Some(&package_dir), view).await?;
}
fetch_archive_sources(package_info, Some(&package_dir), progress).await?;
} }
Ok(()) Ok(())
@@ -931,12 +909,26 @@ mod tests {
let cwd = temp_dir.path(); let cwd = temp_dir.path();
// Main 'pull' command: the one we want to test // 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(
.await package,
.unwrap(); None,
pull(&info, Some(cwd), None, archive.unwrap_or(false)) series,
.await "",
.unwrap(); dist,
None,
None,
&crate::report::Quiet,
)
.await
.unwrap();
pull(
&info,
Some(cwd),
&crate::report::Quiet,
archive.unwrap_or(false),
)
.await
.unwrap();
let package_dir = cwd.join(package); let package_dir = cwd.join(package);
assert!(package_dir.exists()); assert!(package_dir.exists());
@@ -1136,7 +1128,7 @@ mod tests {
.unwrap(); .unwrap();
let dest = temp_dir.path().join("dest"); 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 { let err = match result {
Ok(_) => panic!("extraction of a traversal archive should fail"), Ok(_) => panic!("extraction of a traversal archive should fail"),
@@ -1169,7 +1161,7 @@ mod tests {
.unwrap(); .unwrap();
let dest = temp_dir.path().join("dest"); 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 { let err = match result {
Ok(_) => panic!("extraction of an absolute-path archive should fail"), Ok(_) => panic!("extraction of an absolute-path archive should fail"),
@@ -1202,7 +1194,7 @@ mod tests {
.unwrap(); .unwrap();
let dest = temp_dir.path().join("dest"); 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!( assert_eq!(
std::fs::read_to_string(dest.join("hello-1.0/file.txt")).unwrap(), 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(); std::fs::write(&tar_path, tar_bytes).unwrap();
let dest = temp_dir.path().join("dest"); 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!( assert_eq!(
std::fs::read_to_string(dest.join("pkg-1.0/docs/v1/manual.txt")).unwrap(), 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(); std::fs::write(&tar_path, tar_bytes).unwrap();
let tree = temp_dir.path().join("tree"); 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(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
assert!(!tree.join("pax_global_header").exists()); assert!(!tree.join("pax_global_header").exists());
@@ -1297,7 +1290,7 @@ mod tests {
std::fs::write(&tar_path, tar_bytes).unwrap(); std::fs::write(&tar_path, tar_bytes).unwrap();
let dest = temp_dir.path().join("dest"); 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 { let err = match result {
Ok(_) => panic!("extraction of an escaping hard link should fail"), Ok(_) => panic!("extraction of an escaping hard link should fail"),
@@ -1332,7 +1325,8 @@ mod tests {
.unwrap(); .unwrap();
let tree = temp_dir.path().join("tree"); 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(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
assert_eq!( assert_eq!(
@@ -1366,7 +1360,8 @@ mod tests {
std::fs::write(&tar_path, tar_bytes).unwrap(); std::fs::write(&tar_path, tar_bytes).unwrap();
let tree = temp_dir.path().join("tree"); 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(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
let meta = std::fs::symlink_metadata(tree.join("README")).unwrap(); let meta = std::fs::symlink_metadata(tree.join("README")).unwrap();
@@ -1392,7 +1387,8 @@ mod tests {
.unwrap(); .unwrap();
let tree = temp_dir.path().join("tree"); 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!(extracted.wrapper.is_none());
assert!(tree.join("Makefile").exists()); assert!(tree.join("Makefile").exists());
@@ -1413,7 +1409,7 @@ mod tests {
let main = temp_dir.path().join("main.orig.tar"); let main = temp_dir.path().join("main.orig.tar");
std::fs::write(&main, build_tar(&[("dovecot-2.4.5/README", b"main")])).unwrap(); 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( unwrap_into(
&tree, &tree,
&extracted.wrapper.unwrap(), &extracted.wrapper.unwrap(),
@@ -1430,7 +1426,8 @@ mod tests {
]), ]),
) )
.unwrap(); .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( unwrap_into(
&tree, &tree,
&extracted.wrapper.unwrap(), &extracted.wrapper.unwrap(),
@@ -1472,7 +1469,8 @@ mod tests {
.unwrap(); .unwrap();
let tree = temp_dir.path().join("tree"); 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( unwrap_into(
&tree, &tree,
&extracted.wrapper.unwrap(), &extracted.wrapper.unwrap(),
@@ -1512,8 +1510,13 @@ mod tests {
std::fs::write(&tar_path, BZ2_TAR_WRAPPED).unwrap(); std::fs::write(&tar_path, BZ2_TAR_WRAPPED).unwrap();
let tree = temp_dir.path().join("tree"); let tree = temp_dir.path().join("tree");
let extracted = let extracted = extract_tar_archive(
extract_tar_archive(&tar_path, &tree, None, bzip2_rs::DecoderReader::new).unwrap(); &tar_path,
&tree,
&crate::report::Quiet,
bzip2_rs::DecoderReader::new,
)
.unwrap();
unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap(); unwrap_wrapper(&tree, &extracted.wrapper.unwrap()).unwrap();
assert_eq!( assert_eq!(
+6 -4
View File
@@ -68,10 +68,12 @@ pub struct BuildTarget<'a> {
/// Observer of a running build: target identification, phases, status /// Observer of a running build: target identification, phases, status
/// messages, progress and the final outcome. /// messages, progress and the final outcome.
/// ///
/// Implement this to observe the [`crate::build`] and [`crate::deb`] flows /// Implement this to observe the [`crate::build`], [`crate::deb`] and
/// from any frontend. All events arrive in order from the build thread; /// [`crate::pull`] flows from any frontend. All events arrive in order from
/// long-lived views are expected to be `Send + Sync` because builds may run /// the build thread; long-lived views are expected to be `Send + Sync`
/// inside async tasks. /// 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 { pub trait BuildView: Send + Sync {
/// The build target was identified; the events that follow belong to it /// The build target was identified; the events that follow belong to it
/// (including the earliest subprocess output, e.g. a chroot download). /// (including the earliest subprocess output, e.g. a chroot download).
+46 -24
View File
@@ -51,34 +51,56 @@ pub fn wait_spinner(msg: &str) -> ProgressBar {
pb pb
} }
/// Create a spinner-style progress bar attached to `multi`, returning the bar /// Terminal build view for the pull flow: one bar line attached to the
/// and a callback compatible with [`crate::ProgressCallback`] /// shared [`MultiProgress`], spinner-style while the work is unsized,
pub fn create_progress_bar( /// transfer-style once a byte or item total is known. The bar is created
multi: &MultiProgress, /// eagerly (an empty spinner, so the line exists from the first moment)
) -> (ProgressBar, impl Fn(&str, &str, usize, usize) + '_) { /// and released on drop.
let pb = multi.add(ProgressBar::new(0)); pub struct PullView {
pb.enable_steady_tick(Duration::from_millis(50)); multi: MultiProgress,
pb.set_style(spinner_style()); pb: ProgressBar,
}
let pb_clone = pb.clone(); impl PullView {
let callback = move |prefix: &str, msg: &str, progress: usize, total: usize| { /// Attach a pull view to `multi`
let pb = &pb_clone; pub fn new(multi: &MultiProgress) -> Self {
if progress != 0 && total != 0 { let pb = multi.add(ProgressBar::new(0));
pb.set_style(transfer_style()); pb.enable_steady_tick(Duration::from_millis(50));
pb.set_style(spinner_style());
Self {
multi: multi.clone(),
pb,
}
}
}
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 { } 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() { fn is_enabled(&self) -> bool {
pb.set_prefix(prefix.to_string()); 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`] /// Label of the interactive series selector, rendered by [`prompt::select`]