deb: hand the staged tree to the build user selectively and in parallel
The handover chowned the whole build root with a serial 'chown -R'. Through the overlay staging every lower-layer file it touched is copy-up'd — the full data movement of the staged tree — which the profile of a kernel build showed as 15m53s in 'Preparing the build user', 19% of the whole run. The handover is now a 'find ! -uid | xargs -P chown' pipeline: only entries the build user does not own yet are chowned (staging and quilt re-application flip entries back to root; a resumed tree is already owned and the phase collapses to a metadata-only walk), and the copy-ups of independent files parallelize across the chroot's cores. On a 200k-file tree the handover drops from 9.9s to 0.9s. The ccache handover ran only when the build user was fresh, which is every build in practice (the chroot is per-run scratch); it now rides the same selective pipeline on every run, still best-effort — a failed handover costs cache hits, never the build.
This commit is contained in:
+66
-22
@@ -116,8 +116,7 @@ fn ensure_build_user(
|
||||
.output()
|
||||
};
|
||||
let probe = id_of(ctx, "-u").map_err(|e| format!("cannot look up {BUILD_USER}: {e}"))?;
|
||||
let fresh = !probe.status.success();
|
||||
if fresh {
|
||||
if !probe.status.success() {
|
||||
create_build_user(ctx)?;
|
||||
}
|
||||
|
||||
@@ -135,30 +134,53 @@ fn ensure_build_user(
|
||||
}
|
||||
}
|
||||
|
||||
// The shared ccache directory moves once, when the user is fresh: it
|
||||
// was root-owned by earlier root builds, and root keeps writing
|
||||
// through the new ownership anyway.
|
||||
if fresh && let Some(dir) = ccache_dir {
|
||||
let _ = ctx
|
||||
.command("chown")
|
||||
.arg("-R")
|
||||
.arg(format!("{}:{}", ids.0, ids.1))
|
||||
.arg(dir)
|
||||
.status();
|
||||
for dir in [Some(workroot), Some(Path::new("/tmp/pkh-home"))]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
hand_over(ctx, dir, &ids)?;
|
||||
}
|
||||
for dir in [workroot, Path::new("/tmp/pkh-home")] {
|
||||
// The shared cache degrades to per-run compiles when it cannot be
|
||||
// handed over: slower, never wrong.
|
||||
if let Some(dir) = ccache_dir {
|
||||
let _ = hand_over(ctx, Path::new(dir), &ids);
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// The shell pipeline handing `dir` to the build identity: chown only the
|
||||
/// entries it does not own yet, run in parallel.
|
||||
///
|
||||
/// Through the overlay staging a chown copy-ups every lower-layer file it
|
||||
/// touches — the full data movement of the staged tree. The serial
|
||||
/// `chown -R` of a kernel tree spends minutes on it; this pipeline pays
|
||||
/// the copy-up only for entries a root-side phase flipped back to root
|
||||
/// (staging, quilt re-application), and the copy-ups of independent files
|
||||
/// parallelize across the chroot's cores. On a resumed tree every entry
|
||||
/// is already owned and the phase collapses to a metadata-only walk.
|
||||
fn hand_over_script(dir: &str, uid: &str, gid: &str) -> String {
|
||||
format!(
|
||||
"n=$(nproc 2>/dev/null || echo 4); \
|
||||
find {} \\! -uid {uid} -print0 \
|
||||
| xargs -0 -r -P \"$n\" -n 512 chown -- {uid}:{gid}",
|
||||
shlex_quote(dir)
|
||||
)
|
||||
}
|
||||
|
||||
/// Run [`hand_over_script`] against `dir`, failing the build when the
|
||||
/// escalation cannot complete: a tree half-owned by root fails the rules
|
||||
/// on their first write into it.
|
||||
fn hand_over(ctx: &Arc<Context>, dir: &Path, ids: &(String, String)) -> Result<(), Box<dyn Error>> {
|
||||
let status = ctx
|
||||
.command("chown")
|
||||
.arg("-R")
|
||||
.arg(format!("{}:{}", ids.0, ids.1))
|
||||
.arg(dir)
|
||||
.command("sh")
|
||||
.arg("-c")
|
||||
.arg(hand_over_script(&dir.to_string_lossy(), &ids.0, &ids.1))
|
||||
.status()
|
||||
.map_err(|e| format!("cannot hand {} to {BUILD_USER}: {e}", dir.display()))?;
|
||||
if !status.success() {
|
||||
return Err(format!("cannot hand '{}' to '{BUILD_USER}'", dir.display()).into());
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create [`BUILD_USER`] in the build context. Minimal mmdebstrap chroots
|
||||
@@ -691,9 +713,10 @@ pub async fn build(
|
||||
// LTO/make -j process flood), and a rules chown that fails for the
|
||||
// build user is cosmetic — the .deb is assembled in the binary step's
|
||||
// own fakeroot session, where files unknown to its database are
|
||||
// recorded as root-owned anyway. Handing the tree over is a recursive
|
||||
// chown that can take minutes on a large carried tree, so it is a
|
||||
// phase of its own in the status bar and the profile.
|
||||
// recorded as root-owned anyway. Handing the tree over is a selective,
|
||||
// parallel chown (through the overlay staging each chown copy-ups the
|
||||
// file it touches), and it is a phase of its own in the status bar and
|
||||
// the profile.
|
||||
if build_user.is_some() {
|
||||
enter_phase(view, Phase::PreparingBuildUser, profiler);
|
||||
}
|
||||
@@ -1662,6 +1685,27 @@ mod tests {
|
||||
crate::debian::deps::parse_simple(s, true).unwrap()
|
||||
}
|
||||
|
||||
/// The handover pipeline chowns only entries the build user does not
|
||||
/// own (a resumed tree needs no re-chown of already-owned files), is
|
||||
/// parallel across the chroot's cores, and survives any spelling of
|
||||
/// the staged path.
|
||||
#[test]
|
||||
fn hand_over_script_selects_and_parallelizes() {
|
||||
let script = hand_over_script("/tmp/pkh-build/tree", "64500", "64500");
|
||||
assert!(
|
||||
script.contains("find '/tmp/pkh-build/tree' \\! -uid 64500 -print0"),
|
||||
"{script}"
|
||||
);
|
||||
assert!(script.contains("xargs -0 -r -P"), "{script}");
|
||||
assert!(script.contains("chown -- 64500:64500"), "{script}");
|
||||
|
||||
let quoted = hand_over_script("/tmp/some'dir", "1000", "1000");
|
||||
assert!(
|
||||
quoted.contains("find '/tmp/some'\\''dir'"),
|
||||
"embedded quotes must be shell-quoted: {quoted}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The build env carries the ccache wiring when it is enabled: the
|
||||
/// compiler shim first on PATH, the shared cache dir, content-keyed
|
||||
/// compiler checks and the size cap.
|
||||
|
||||
Reference in New Issue
Block a user