deb,report,ui: report the staged-tree copy while a record forks

Forking an incremental base copies its whole staged tree — bounded,
and without reflinks a matter of minutes — but the copy emitted no
build events: the live view showed the target line with nothing below
it, and an interrupt there printed raw cp errors with no hint of what
was running (the fork happens before the first bootstrap phase owns
the view).

Announce the copy through a new BuildView::byte_progress port. The
fork sizes the source up front (the copy's total), re-measures the
growing destination from a scoped thread while cp runs (cp has no
progress output to tap), and reports the completed copy when it
returns; unmeasurable or empty sources copy unreported. The terminal
view renders it like the other determinate bars, with byte-unit
counters.
This commit is contained in:
2026-10-03 01:28:30 +02:00
parent 83a853d259
commit f6f2b882a8
4 changed files with 227 additions and 16 deletions
+47 -1
View File
@@ -296,6 +296,10 @@ impl BuildView for RecordLogView<'_> {
self.inner.progress(label, pos, total);
}
fn byte_progress(&self, label: &str, pos: u64, total: u64) {
self.inner.byte_progress(label, pos, total);
}
fn sink(&self) -> Option<Arc<dyn crate::context::LineSink>> {
Some(self.log.clone())
}
@@ -506,6 +510,7 @@ async fn build_binary_package_impl(
series,
chroot_arch,
mirror.as_deref(),
view,
) {
Ok((record, keep)) => {
keep_upper = keep;
@@ -795,9 +800,32 @@ async fn build_binary_package_impl(
result
}
/// The staged-tree copy of a fork is bounded (the base's whole tree)
/// and, without reflinks, takes minutes: announce it on the view, then
/// feed byte progress once the fork's watcher has measured the total.
/// Callers hand [`session::CopyProgress`] the box through `as_deref()`.
fn copy_report<'a>(
view: Option<&'a dyn BuildView>,
base_id: &str,
) -> Option<Box<dyn Fn(u64, u64) + Sync + 'a>> {
view.map(|view| {
let label = format!("Copying the staged tree of record {base_id}");
view.message(&label);
Box::new(move |pos: u64, total: u64| {
let size = crate::utils::size::format_bytes;
view.byte_progress(
&format!("{label} ({}/{})", size(pos), size(total)),
pos,
total,
)
}) as Box<dyn Fn(u64, u64) + Sync + 'a>
})
}
/// Resolve the record a build runs in, per the `--incremental` request,
/// against the real sessions root and the chroot tarball the bootstrap
/// would use today. See [`resolve_record_in`].
#[allow(clippy::too_many_arguments)]
fn resolve_record(
incremental: &Incremental,
host_tree: &Path,
@@ -806,6 +834,7 @@ fn resolve_record(
series: &str,
chroot_arch: Option<&str>,
mirror: Option<&str>,
view: &dyn BuildView,
) -> Result<(session::Session, bool), Box<dyn Error>> {
resolve_record_in(
&session::sessions_root(),
@@ -814,11 +843,14 @@ fn resolve_record(
host_tree,
identity,
version,
Some(view),
)
}
/// The testable core of [`resolve_record`] over an explicit sessions root
/// and cached-tarball digest (the fork gate's reference).
/// and cached-tarball digest (the fork gate's reference). `view` is the
/// live build view when there is one; the fork reports its staged-tree
/// copy through it.
///
/// Every local build runs in its own record: [`Incremental::Off`] gets a
/// brand-new one, [`Incremental::Auto`] and [`Incremental::Id`] fork the
@@ -833,6 +865,7 @@ fn resolve_record_in(
host_tree: &Path,
identity: &session::SessionIdentity,
version: &str,
view: Option<&dyn BuildView>,
) -> Result<(session::Session, bool), Box<dyn Error>> {
match incremental {
Incremental::Off => Ok((
@@ -854,6 +887,7 @@ fn resolve_record_in(
} else {
"no successful record among the last usable ones; the newest record"
};
let report = copy_report(view, &base.manifest().id);
match session::fork_record_in(
records_root,
&base,
@@ -861,6 +895,7 @@ fn resolve_record_in(
identity,
version,
cache_tarball_sha256,
report.as_deref(),
) {
Ok(record) => {
log::info!(
@@ -901,6 +936,7 @@ fn resolve_record_in(
"Base of the incremental build: record {} (pinned by id)",
base.manifest().id
);
let report = copy_report(view, &base.manifest().id);
Ok((
session::fork_record_in(
records_root,
@@ -909,6 +945,7 @@ fn resolve_record_in(
identity,
version,
cache_tarball_sha256,
report.as_deref(),
)?,
true,
))
@@ -1767,6 +1804,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect("a plain run always resolves to a fresh record");
assert!(!keep_upper, "a plain run drops its staged tree");
@@ -1798,6 +1836,7 @@ mod tests {
&identity,
"1.0",
&cache_sha(&tarball),
None,
)
.unwrap();
let b_id = b.manifest().id;
@@ -1811,6 +1850,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect("the auto selection must fork a candidate record");
assert!(keep_upper);
@@ -1836,6 +1876,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect("no candidates must degrade to a fresh build");
assert!(keep_upper, "the first record becomes a base");
@@ -1877,6 +1918,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect("the record carrying a staged tree is still the base");
assert_eq!(record.manifest().base.as_deref(), Some(carrier_id.as_str()));
@@ -1907,6 +1949,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect("a pinned failed record is a valid base");
assert!(keep_upper);
@@ -1940,6 +1983,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect_err("records of other trees must not be forked by id");
assert!(
@@ -1981,6 +2025,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect_err("an ambiguous prefix must be refused");
assert!(
@@ -2029,6 +2074,7 @@ mod tests {
&host_tree,
&identity,
"1.0",
None,
)
.expect_err("a foreign-identity record must not be forked");
assert!(