diff --git a/src/deb/local.rs b/src/deb/local.rs index 13007dd..c5a5ec8 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -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")