diff --git a/src/deb/local.rs b/src/deb/local.rs index c067c15..0869a11 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -276,26 +276,39 @@ pub async fn build( return Err("Could not install build-dependencies for the build".into()); } - // Install arch-independant build dependencies - log::debug!("Installing arch-independant build dependencies..."); - let status = cap( - ctx.command("apt-get") - .current_dir(package_dir_str) + // Install arch-independant build dependencies, only if the source declares + // any: without --arch-only this pass resolves the whole Build-Depends field + // too, which is redundant after the first pass and breaks cross builds. + let has_indep_deps = match ctx.read_file(&package_dir.join("debian/control")) { + Ok(control) => control + .lines() + .any(|l| l.to_ascii_lowercase().starts_with("build-depends-indep:")), + Err(e) => { + log::debug!("cannot read debian/control for indep build-deps: {}", e); + true + } + }; + if has_indep_deps { + log::debug!("Installing arch-independant build dependencies..."); + let mut cmd = ctx.command("apt-get"); + cmd.current_dir(package_dir_str) .envs(env.clone()) .arg("-y") - .arg("build-dep") - .arg("./"), - &sink, - ) - .status()?; + .arg("build-dep"); + if cross { + cmd.arg(format!("--host-architecture={arch}")); + } + cmd.arg("./"); + let status = cap(&mut cmd, &sink).status()?; - // If build-dep fails, we try to explain the failure using dose-debcheck - if !status.success() { - if let Some(u) = &ui { - u.suspend(); + // If build-dep fails, we try to explain the failure using dose-debcheck + if !status.success() { + if let Some(u) = &ui { + u.suspend(); + } + dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; + return Err("Could not install build-dependencies for the build".into()); } - dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; - return Err("Could not install build-dependencies for the build".into()); } // Run the build step diff --git a/src/deb/mod.rs b/src/deb/mod.rs index 43a7e58..b1a9a77 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -492,4 +492,123 @@ mod tests { async fn test_deb_gcc_debian_end_to_end() { test_build_end_to_end("gcc-15", "sid", None, None, false).await; } + + /// Create a synthetic source package that discriminates which architecture + /// is used to resolve Build-Depends-Indep during cross builds: + /// + /// - 'libdb-dev' is an arch:any package that is not Multi-Arch: same, so an + /// amd64 copy can only be installed by replacing the arm64 one + /// - the arch-specific binary links against libdb for the host + /// architecture, so the build only succeeds if the arm64 libdb-dev was + /// left in place by the arch-independant build-dep pass + fn create_indep_cross_test_source(parent: &Path) -> PathBuf { + let pkg_dir = parent.join("pkh-crosstest"); + std::fs::create_dir_all(pkg_dir.join("debian/source")).unwrap(); + + std::fs::write( + pkg_dir.join("debian/changelog"), + "pkh-crosstest (1.0) noble; urgency=medium\n\n \ + * Synthetic package exercising Build-Depends-Indep in cross builds.\n\n \ + -- pkh tests Tue, 15 Sep 2026 08:00:00 +0000\n", + ) + .unwrap(); + + std::fs::write( + pkg_dir.join("debian/control"), + "Source: pkh-crosstest\n\ + Section: devel\n\ + Priority: optional\n\ + Maintainer: pkh tests \n\ + Standards-Version: 4.7.4\n\ + Build-Depends: debhelper-compat (= 13), libdb-dev\n\ + Build-Depends-Indep: libdb-dev\n\ + Architecture: any all\n\ + \n\ + Package: pkh-crosstest\n\ + Architecture: any\n\ + Depends: ${misc:Depends}, ${shlibs:Depends}\n\ + Description: Cross-build regression package for build-dep resolution\n \ + Builds a host-architecture binary against libdb to detect a cross\n \ + build environment damaged by a wrongly-scoped build-dep pass.\n\ + \n\ + Package: pkh-crosstest-data\n\ + Architecture: all\n\ + Description: Cross-build regression package data (arch-indep)\n \ + Arch-indep binary so the indep build path is exercised.\n", + ) + .unwrap(); + + std::fs::write( + pkg_dir.join("debian/rules"), + "#!/usr/bin/make -f\n\ + %:\n\ + \tdh $@\n\ + \n\ + override_dh_auto_build:\n\ + \tprintf '#include \\nint main(void){DB *d; return db_create(&d, NULL, 0);}\\n' > main.c\n\ + \t$(DEB_HOST_GNU_TYPE)-gcc main.c -ldb -o pkh-crosstest\n", + ) + .unwrap(); + + use std::os::unix::fs::PermissionsExt; + let rules = pkg_dir.join("debian/rules"); + let mut perms = std::fs::metadata(&rules).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&rules, perms).unwrap(); + + std::fs::write(pkg_dir.join("debian/source/format"), "3.0 (native)\n").unwrap(); + + pkg_dir + } + + /// This ensures the arch-independant build-dep pass of a cross build + /// resolves dependencies for the host architecture, like dpkg-checkbuilddeps + /// does, instead of re-resolving the whole Build-Depends field for the + /// native architecture, which swaps host-arch -dev packages for native ones + /// and breaks the cross build environment. + #[tokio::test] + #[test_log::test] + #[cfg(target_arch = "x86_64")] + async fn test_deb_cross_indep_host_arch_end_to_end() { + let temp_dir = tempfile::tempdir().unwrap(); + let pkg_dir = create_indep_cross_test_source(temp_dir.path()); + + let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local)); + + crate::deb::build_binary_package( + Some("arm64"), + Some("noble"), + None, + Some(&pkg_dir), + true, + None, + None, + None, + Some(ctx), + None, + None, + ) + .await + .expect("Cannot cross-build package declaring Build-Depends-Indep"); + + // Both binary packages must have been produced, including the + // arch-independant one + let deb_files: Vec = std::fs::read_dir(temp_dir.path()) + .unwrap() + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect(); + assert!( + deb_files + .iter() + .any(|f| f.starts_with("pkh-crosstest_1.0_arm64.deb")), + "arch-specific .deb not produced, got: {deb_files:?}" + ); + assert!( + deb_files + .iter() + .any(|f| f.starts_with("pkh-crosstest-data_1.0_all.deb")), + "arch-independant .deb not produced, got: {deb_files:?}" + ); + } }