deb: run the rootless build step under fakeroot, sharing the state

The rules run as the unprivileged build user, so an ownership change
they make during the build (a sysroot chown) failed per file with
EPERM, spamming the build view and log and silently not happening.

Wrap the build step in fakeroot like the binary step already is, and
share one session state between the two — the way dpkg-buildpackage
-rfakeroot wraps every rules target: the chown succeeds faked and
carries into the .deb assembly. With the context's own user the
build step stays bare. The state file lives in the staged build
root, outside the package tree, and is removed before each build so
an interrupted previous attempt cannot seed the session.
This commit is contained in:
2026-10-02 17:36:05 +02:00
parent 6967813fa0
commit 5f36a54e7c
+44 -7
View File
@@ -250,6 +250,13 @@ fn rules_command<'a>(
}
}
/// The fakeroot state file shared by the rules steps of a rootless
/// build, in the staged build root: outside the package tree, so it
/// pollutes neither the sources nor the collected artifacts.
fn fakeroot_state_file(build_root: &str) -> PathBuf {
Path::new(build_root).join(".pkh-fakeroot.state")
}
#[allow(clippy::too_many_arguments)]
pub async fn build(
package: &str,
@@ -670,11 +677,10 @@ pub async fn build(
.unwrap_or_else(|| "binary-targets".to_string());
env.insert("DEB_RULES_REQUIRES_ROOT".to_string(), rules_requires_root);
// Rootless builds: the rules (and the fakeroot-wrapped binary step)
// run as an unprivileged user owning the staged tree. Everything
// before this point - dependency installation, quilt patch
// application, staging cleanup - legitimately needs the context's
// root and stays with it.
// Rootless builds: the rules (fakeroot-wrapped, see below) run as an
// unprivileged user owning the staged tree. Everything before this
// point - dependency installation, quilt patch application, staging
// cleanup - legitimately needs the context's root and stays with it.
let rule_ids = match build_user {
Some(_) => {
let workroot = Path::new(package_dir_str)
@@ -687,12 +693,34 @@ pub async fn build(
None => None,
};
// Rootless: the rules run under fakeroot with one session state
// shared between the build and binary steps, the way
// dpkg-buildpackage -rfakeroot wraps every rules target. An
// ownership change made during the build (a sysroot chown) then
// succeeds — faked — and carries into the .deb, instead of failing
// per file with EPERM spam. With the context's own user the build
// step runs bare; the binary step keeps its unconditional wrap.
let fakeroot_state = fakeroot_state_file(build_root);
if rule_ids.is_some() {
// A state left by an interrupted previous attempt must not
// seed this session.
let _ = ctx.command("rm").arg("-f").arg(&fakeroot_state).status();
}
let mut rules = match rule_ids.as_ref() {
Some(_) => {
let mut cmd = rules_command(&ctx, rule_ids.as_ref(), "fakeroot");
cmd.arg("-s").arg(&fakeroot_state).arg("debian/rules");
cmd
}
None => rules_command(&ctx, None, "debian/rules"),
};
// Run the build step
log::debug!("Building (debian/rules build) package...");
enter_phase(view, Phase::Building, profiler);
let started = Instant::now();
let status = cap(
rules_command(&ctx, rule_ids.as_ref(), "debian/rules")
rules
.current_dir(package_dir_str)
.envs(env.clone())
.arg("build"),
@@ -721,8 +749,17 @@ pub async fn build(
// Run the 'binary' step to produce deb
enter_phase(view, Phase::ProducingBinaries, profiler);
let started = Instant::now();
let mut rules = rules_command(&ctx, rule_ids.as_ref(), "fakeroot");
if rule_ids.is_some() {
// Pick up the ownership changes the build step recorded.
rules
.arg("-i")
.arg(&fakeroot_state)
.arg("-s")
.arg(&fakeroot_state);
}
let status = cap(
rules_command(&ctx, rule_ids.as_ref(), "fakeroot")
rules
.current_dir(package_dir_str)
.envs(env.clone())
.arg("debian/rules")