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()
|
.output()
|
||||||
};
|
};
|
||||||
let probe = id_of(ctx, "-u").map_err(|e| format!("cannot look up {BUILD_USER}: {e}"))?;
|
let probe = id_of(ctx, "-u").map_err(|e| format!("cannot look up {BUILD_USER}: {e}"))?;
|
||||||
let fresh = !probe.status.success();
|
if !probe.status.success() {
|
||||||
if fresh {
|
|
||||||
create_build_user(ctx)?;
|
create_build_user(ctx)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,30 +134,53 @@ fn ensure_build_user(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The shared ccache directory moves once, when the user is fresh: it
|
for dir in [Some(workroot), Some(Path::new("/tmp/pkh-home"))]
|
||||||
// was root-owned by earlier root builds, and root keeps writing
|
.into_iter()
|
||||||
// through the new ownership anyway.
|
.flatten()
|
||||||
if fresh && let Some(dir) = ccache_dir {
|
{
|
||||||
let _ = ctx
|
hand_over(ctx, dir, &ids)?;
|
||||||
.command("chown")
|
|
||||||
.arg("-R")
|
|
||||||
.arg(format!("{}:{}", ids.0, ids.1))
|
|
||||||
.arg(dir)
|
|
||||||
.status();
|
|
||||||
}
|
}
|
||||||
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
|
let status = ctx
|
||||||
.command("chown")
|
.command("sh")
|
||||||
.arg("-R")
|
.arg("-c")
|
||||||
.arg(format!("{}:{}", ids.0, ids.1))
|
.arg(hand_over_script(&dir.to_string_lossy(), &ids.0, &ids.1))
|
||||||
.arg(dir)
|
|
||||||
.status()
|
.status()
|
||||||
.map_err(|e| format!("cannot hand {} to {BUILD_USER}: {e}", dir.display()))?;
|
.map_err(|e| format!("cannot hand {} to {BUILD_USER}: {e}", dir.display()))?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(format!("cannot hand '{}' to '{BUILD_USER}'", dir.display()).into());
|
return Err(format!("cannot hand '{}' to '{BUILD_USER}'", dir.display()).into());
|
||||||
}
|
}
|
||||||
}
|
Ok(())
|
||||||
Ok(ids)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create [`BUILD_USER`] in the build context. Minimal mmdebstrap chroots
|
/// 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
|
// 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
|
// build user is cosmetic — the .deb is assembled in the binary step's
|
||||||
// own fakeroot session, where files unknown to its database are
|
// own fakeroot session, where files unknown to its database are
|
||||||
// recorded as root-owned anyway. Handing the tree over is a recursive
|
// recorded as root-owned anyway. Handing the tree over is a selective,
|
||||||
// chown that can take minutes on a large carried tree, so it is a
|
// parallel chown (through the overlay staging each chown copy-ups the
|
||||||
// phase of its own in the status bar and the profile.
|
// file it touches), and it is a phase of its own in the status bar and
|
||||||
|
// the profile.
|
||||||
if build_user.is_some() {
|
if build_user.is_some() {
|
||||||
enter_phase(view, Phase::PreparingBuildUser, profiler);
|
enter_phase(view, Phase::PreparingBuildUser, profiler);
|
||||||
}
|
}
|
||||||
@@ -1662,6 +1685,27 @@ mod tests {
|
|||||||
crate::debian::deps::parse_simple(s, true).unwrap()
|
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
|
/// The build env carries the ccache wiring when it is enabled: the
|
||||||
/// compiler shim first on PATH, the shared cache dir, content-keyed
|
/// compiler shim first on PATH, the shared cache dir, content-keyed
|
||||||
/// compiler checks and the size cap.
|
/// compiler checks and the size cap.
|
||||||
|
|||||||
Reference in New Issue
Block a user