From 3bc4d738c40cba0bcf6c14506c20fd8d3538be16 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Tue, 17 Mar 2026 19:29:04 -0500 Subject: [PATCH] feat: update depot version and enhance host build support - Bump the version of the "depot" package from 0.26.1 to 0.27.0 in Cargo.toml and Cargo.lock. - Modify meson.build, autotools.rs, cmake.rs, custom.rs, makefile.rs, and other builder files to include a new parameter for host build directory. - Implement logic to handle host builds across various build systems (Autotools, CMake, Meson). - Add tests to ensure that environment variables for host build directories are correctly expanded. - Introduce new flags in PackageSpec to control host build behavior. --- Cargo.lock | 2 +- Cargo.toml | 2 +- meson.build | 2 +- src/builder/autotools.rs | 147 +++++++++++++++++++++- src/builder/bin.rs | 3 +- src/builder/cmake.rs | 114 ++++++++++++++++- src/builder/custom.rs | 13 +- src/builder/makefile.rs | 5 +- src/builder/meson.rs | 90 +++++++++++++- src/builder/mod.rs | 259 ++++++++++++++++++++++++++++++++++++--- src/builder/perl.rs | 5 +- src/builder/python.rs | 1 + src/builder/rust.rs | 1 + src/commands.rs | 36 +++++- src/package/spec.rs | 18 +++ 15 files changed, 662 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f12b695..4b0e8d9 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.26.1" +version = "0.27.0" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index 281b1da..37f953d 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.26.1" +version = "0.27.0" edition = "2024" [lints.rust] diff --git a/meson.build b/meson.build index 25e291b..93a785e 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.26.1', + version: '0.27.0', meson_version: '>=0.60.0', default_options: ['buildtype=release'], ) diff --git a/src/builder/autotools.rs b/src/builder/autotools.rs index 81fa4a4..62be393 100755 --- a/src/builder/autotools.rs +++ b/src/builder/autotools.rs @@ -1,5 +1,6 @@ //! GNU Autotools build system (configure && make && make install) +use crate::builder::state::{BuildStep, StateTracker}; use crate::cross::CrossConfig; use crate::fakeroot; use crate::package::PackageSpec; @@ -15,6 +16,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; let make_exec = resolve_make_exec(&flags.make_exec); @@ -26,6 +28,13 @@ pub fn build( // Build environment variables let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags); + if let Some(host_dir) = host_build_dir { + crate::builder::set_env_var( + &mut env_vars, + crate::builder::DEPOT_BUILD_HOST_DIR_ENV, + host_dir.to_string_lossy().into_owned(), + ); + } let cc = if let Some(cc_cfg) = cross { cc_cfg.cc.clone() } else { @@ -44,7 +53,6 @@ pub fn build( crate::builder::set_env_var(&mut env_vars, "CFLAGS", expanded); } - use crate::builder::state::{BuildStep, StateTracker}; let mut state = StateTracker::new_with_namespace( &actual_src, spec.build.flags.lib32_variant.then_some("lib32"), @@ -385,6 +393,104 @@ pub fn build( Ok(()) } +pub(crate) fn ensure_host_build( + spec: &PackageSpec, + src_dir: &Path, + export_compiler_flags: bool, +) -> Result { + let host_spec = crate::builder::host_build_spec(spec); + let flags = &host_spec.build.flags; + let make_exec = resolve_make_exec(&flags.make_exec); + let export_compiler_flags = export_compiler_flags && !flags.no_flags; + let actual_src = resolve_actual_src(&host_spec, src_dir)?; + let build_dir = crate::builder::host_build_dir_for_source(&actual_src, flags); + fs::create_dir_all(&build_dir)?; + + let mut env_vars = + crate::builder::standard_build_env(&host_spec, None, true, export_compiler_flags); + if export_compiler_flags + && let Some(cflags_str) = env_vars + .iter() + .find(|(key, _)| key == "CFLAGS") + .map(|(_, value)| value.clone()) + && !cflags_str.trim().is_empty() + { + let expanded = expand_shell_commands(&cflags_str, &flags.cc)?; + crate::builder::set_env_var(&mut env_vars, "CFLAGS", expanded); + } + + let mut state = StateTracker::new_with_namespace(&actual_src, Some("host"))?; + + if !state.is_done(BuildStep::Configured) { + crate::log_info!( + "Running host-side configure build in {}...", + build_dir.display() + ); + let configure_path = resolve_configure_path(&host_spec, &actual_src); + let mut configure_cmd = Command::new(&configure_path); + configure_cmd.current_dir(&build_dir); + + crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars); + + let help_text = configure_help_text(&configure_path, &build_dir, &env_vars); + configure_cmd.arg(format!("--prefix={}", flags.prefix)); + for default_dir_arg in default_configure_install_dirs(flags, help_text.as_deref()) { + configure_cmd.arg(default_dir_arg); + } + for arg in &flags.configure { + let expanded = expand_configure_arg(&host_spec, arg, &env_vars); + configure_cmd.arg(expanded); + } + + let status = crate::interrupts::command_status(&mut configure_cmd) + .with_context(|| format!("Failed to run host configure in {}", build_dir.display()))?; + + if !status.success() { + anyhow::bail!("host configure failed with status: {}", status); + } + state.mark_done(BuildStep::Configured)?; + } + + if !state.is_done(BuildStep::PostCompileDone) { + let build_targets = phase_targets(&flags.make_target, &flags.make_targets, None); + let make_dirs = resolve_make_dirs(&build_dir, &flags.make_dirs, "build.flags.make_dirs")?; + for make_dir in make_dirs { + let mut make_cmd = Command::new(make_exec); + make_cmd.current_dir(&make_dir); + make_cmd.arg("-j").arg(num_cpus().to_string()); + add_make_variable_overrides_if_supported( + &mut make_cmd, + make_exec, + &flags.make_vars, + "build", + )?; + for target in &build_targets { + make_cmd.arg(target); + } + + crate::builder::prepare_tool_command(&mut make_cmd, &env_vars); + + let status = crate::interrupts::command_status(&mut make_cmd).with_context(|| { + format!("Failed to run host {} in {}", make_exec, make_dir.display()) + })?; + + if !status.success() { + anyhow::bail!( + "host {} failed with status: {} (dir: {})", + make_exec, + status, + make_dir.display() + ); + } + } + + state.mark_done(BuildStep::PostCompileDone)?; + } + + fs::canonicalize(&build_dir) + .with_context(|| format!("Failed to resolve host build dir: {}", build_dir.display())) +} + fn num_cpus() -> usize { std::thread::available_parallelism() .map(|n| n.get()) @@ -851,6 +957,45 @@ mod tests { assert_eq!(expanded, "--program-prefix=foo-1.2.3-aarch64-"); } + #[test] + fn test_expand_configure_arg_expands_host_build_dir_env() { + let spec = PackageSpec { + package: PackageInfo { + name: "foo".into(), + real_name: None, + version: "1.2.3".into(), + revision: 1, + description: "d".into(), + homepage: "h".into(), + abi_breaking: false, + license: vec!["MIT".into()], + }, + packages: Vec::new(), + alternatives: Default::default(), + manual_sources: Vec::new(), + source: Vec::new(), + build: Build { + build_type: BuildType::Autotools, + flags: BuildFlags::default(), + }, + dependencies: Dependencies::default(), + package_alternatives: Default::default(), + package_dependencies: Default::default(), + spec_dir: PathBuf::from("."), + }; + + let envs = vec![( + crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(), + "/tmp/build-host".to_string(), + )]; + let expanded = expand_configure_arg( + &spec, + "--with-build-tools=$DEPOT_BUILD_HOST_DIR/tools", + &envs, + ); + assert_eq!(expanded, "--with-build-tools=/tmp/build-host/tools"); + } + #[test] fn test_num_cpus_at_least_one() { let n = num_cpus(); diff --git a/src/builder/bin.rs b/src/builder/bin.rs index 8c3eede..6d3adf4 100644 --- a/src/builder/bin.rs +++ b/src/builder/bin.rs @@ -17,6 +17,7 @@ pub fn build( destdir: &Path, _cross: Option<&CrossConfig>, _export_compiler_flags: bool, + _host_build_dir: Option<&Path>, ) -> Result<()> { crate::log_info!( "Binary install: copying files from {} to {} (pkg type={})", @@ -114,7 +115,7 @@ mod tests { unix_fs::symlink(&target, src.join("usr/lib/libdummy.so.link"))?; let spec = mk_spec("bin-test", "1.0"); - build(&spec, src, dest, None, true)?; + build(&spec, src, dest, None, true, None)?; // Check copied file assert!(dest.join("usr/bin/hello").exists()); diff --git a/src/builder/cmake.rs b/src/builder/cmake.rs index 2e97194..2b4a619 100755 --- a/src/builder/cmake.rs +++ b/src/builder/cmake.rs @@ -1,11 +1,12 @@ //! CMake build system +use crate::builder::state::{BuildStep, StateTracker}; use crate::cross::CrossConfig; use crate::fakeroot; use crate::package::PackageSpec; use anyhow::{Context, Result}; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; pub fn build( @@ -14,6 +15,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; let make_exec_override = flags.make_exec.trim(); @@ -32,7 +34,14 @@ pub fn build( fs::create_dir_all(destdir)?; // Environment variables - let env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags); + let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags); + if let Some(host_dir) = host_build_dir { + crate::builder::set_env_var( + &mut env_vars, + crate::builder::DEPOT_BUILD_HOST_DIR_ENV, + host_dir.to_string_lossy().into_owned(), + ); + } // Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=) let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX") @@ -45,7 +54,6 @@ pub fn build( None }; - use crate::builder::state::{BuildStep, StateTracker}; let mut state = StateTracker::new_with_namespace( &actual_src, spec.build.flags.lib32_variant.then_some("lib32"), @@ -257,6 +265,96 @@ pub fn build( Ok(()) } +pub(crate) fn ensure_host_build( + spec: &PackageSpec, + src_dir: &Path, + export_compiler_flags: bool, +) -> Result { + let host_spec = crate::builder::host_build_spec(spec); + let flags = &host_spec.build.flags; + + let actual_src = resolve_actual_src(&host_spec, src_dir)?; + let build_dir = crate::builder::host_build_dir_for_source(&actual_src, flags); + + fs::create_dir_all(&build_dir)?; + + let env_vars = + crate::builder::standard_build_env(&host_spec, None, true, export_compiler_flags); + let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX") + .unwrap_or(flags.prefix.as_str()); + + let mut state = StateTracker::new_with_namespace(&actual_src, Some("host"))?; + + if !state.is_done(BuildStep::Configured) { + crate::log_info!( + "Running host-side cmake configure in {}...", + build_dir.display() + ); + let mut cmake_cmd = Command::new("cmake"); + cmake_cmd.current_dir(&build_dir); + cmake_cmd.arg("-S").arg(&actual_src); + cmake_cmd.arg("-B").arg(&build_dir); + cmake_cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix)); + cmake_cmd.arg("-DCMAKE_BUILD_TYPE=Release"); + for arg in cmake_install_dir_args(flags) { + cmake_cmd.arg(arg); + } + + let make_exec_override = flags.make_exec.trim(); + if !make_exec_override.is_empty() { + if !cmake_configure_flags_specify_generator(&flags.configure) + && let Some(generator) = cmake_generator_for_make_exec(make_exec_override) + { + cmake_cmd.arg("-G").arg(generator); + } + if !cmake_configure_flags_set_make_program(&flags.configure) { + cmake_cmd.arg(format!("-DCMAKE_MAKE_PROGRAM={make_exec_override}")); + } + } + + for flag in &flags.configure { + let expanded = expand_with_envs(flag, &env_vars); + cmake_cmd.arg(&expanded); + } + + crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars); + + let status = crate::interrupts::command_status(&mut cmake_cmd) + .context("Failed to run host cmake")?; + if !status.success() { + anyhow::bail!("host cmake configure failed"); + } + + state.mark_done(BuildStep::Configured)?; + } + + if !state.is_done(BuildStep::PostCompileDone) { + let build_targets = phase_targets(&flags.make_target, &flags.make_targets); + let mut build_cmd = Command::new("cmake"); + build_cmd.arg("--build").arg(&build_dir); + build_cmd.arg("-j").arg(num_cpus().to_string()); + if !build_targets.is_empty() { + build_cmd.arg("--target"); + for target in &build_targets { + build_cmd.arg(target); + } + } + + crate::builder::prepare_tool_command(&mut build_cmd, &env_vars); + + let status = crate::interrupts::command_status(&mut build_cmd) + .with_context(|| format!("Failed to run host cmake build for {}", spec.package.name))?; + if !status.success() { + anyhow::bail!("host cmake build failed"); + } + + state.mark_done(BuildStep::PostCompileDone)?; + } + + fs::canonicalize(&build_dir) + .with_context(|| format!("Failed to resolve host build dir: {}", build_dir.display())) +} + /// Expand environment variables in a string (e.g., $DEPOT_SYSROOT) fn expand_env_vars(input: &str) -> String { let mut result = input.to_string(); @@ -513,6 +611,16 @@ mod tests { // $HOME should be expanded from process env (may be present) } + #[test] + fn test_expand_with_envs_expands_host_build_dir() { + let envs = vec![( + crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(), + "/tmp/build-host".to_string(), + )]; + let out = expand_with_envs("-DTOOLS_DIR=$DEPOT_BUILD_HOST_DIR/bin", &envs); + assert_eq!(out, "-DTOOLS_DIR=/tmp/build-host/bin"); + } + #[test] fn test_num_cpus_at_least_one() { let n = num_cpus(); diff --git a/src/builder/custom.rs b/src/builder/custom.rs index d10e831..25db419 100755 --- a/src/builder/custom.rs +++ b/src/builder/custom.rs @@ -15,6 +15,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + _host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; let build_dir = if let Some(dir) = &flags.build_dir { @@ -330,7 +331,7 @@ mod tests { let spec = mk_spec("custom-no-build", "1.0"); - let res = build(&spec, tmp_src.path(), tmp_dest.path(), None, true); + let res = build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None); assert!(res.is_err()); Ok(()) } @@ -356,7 +357,7 @@ mod tests { spec.spec_dir = spec_dir.path().to_path_buf(); // src_dir is empty; build() should copy build.sh from spec_dir and run it (no-op) - build(&spec, tmp_src.path(), tmp_dest.path(), None, true)?; + build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None)?; // If we reached here, build() succeeded and build.sh was copied into src assert!(tmp_src.path().join("build.sh").exists()); Ok(()) @@ -404,7 +405,7 @@ depot_install_dev_pkg() { license: vec!["MIT".into()], }); - build(&spec, tmp_src.path(), tmp_dest.path(), None, true)?; + build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None)?; assert!(tmp_dest.path().join("usr/share/primary.txt").exists()); assert!( @@ -454,7 +455,7 @@ depot_install() { license: vec!["MIT".into()], }); - let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true) + let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None) .expect_err("missing per-output install handler should fail"); assert!(err.to_string().contains("Custom build script failed")); Ok(()) @@ -482,7 +483,7 @@ exit 0 } let spec = mk_spec("custom-fail-fast", "1.0"); - let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true) + let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None) .expect_err("non-function custom scripts should fail when a command fails"); assert!(err.to_string().contains("Custom build script failed")); Ok(()) @@ -539,7 +540,7 @@ exec "$@" let mut spec = mk_spec("custom-lib32", "1.0"); spec.build.flags.lib32_variant = true; - build(&spec, tmp_src.path(), tmp_dest.path(), None, true)?; + build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None)?; assert_eq!( std::fs::read_to_string(tmp_dest.path().join("usr/lib32/libfoo.so.1"))?, diff --git a/src/builder/makefile.rs b/src/builder/makefile.rs index e3baed7..64e17ad 100644 --- a/src/builder/makefile.rs +++ b/src/builder/makefile.rs @@ -12,6 +12,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + _host_build_dir: Option<&Path>, ) -> Result<()> { let mut state = StateTracker::new_with_namespace( src_dir, @@ -217,7 +218,7 @@ exec "$@" "cp built.txt $DESTDIR/usr/bin/installed.txt".into(), ]; - build(&spec, src_path, dest_path, None, true)?; + build(&spec, src_path, dest_path, None, true, None)?; // Verify build step let built_file = src_path.join("built.txt"); @@ -271,7 +272,7 @@ exec "$@" "printf 'bin' > \"$DESTDIR/usr/bin/foo\"".into(), ]; - build(&spec, src_path, dest_path, None, true)?; + build(&spec, src_path, dest_path, None, true, None)?; assert_eq!( fs::read_to_string(dest_path.join("usr/lib32/libfoo.so.1"))?, diff --git a/src/builder/meson.rs b/src/builder/meson.rs index 970826e..545ef9f 100755 --- a/src/builder/meson.rs +++ b/src/builder/meson.rs @@ -1,5 +1,6 @@ //! Meson build system +use crate::builder::state::{BuildStep, StateTracker}; use crate::cross::CrossConfig; use crate::fakeroot; use crate::package::PackageSpec; @@ -14,6 +15,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; @@ -27,7 +29,14 @@ pub fn build( fs::create_dir_all(destdir)?; // Environment variables - let env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags); + let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags); + if let Some(host_dir) = host_build_dir { + crate::builder::set_env_var( + &mut env_vars, + crate::builder::DEPOT_BUILD_HOST_DIR_ENV, + host_dir.to_string_lossy().into_owned(), + ); + } // Generate cross file if cross-compiling, or when the lib32 variant needs // Meson to treat the build as x86 instead of the native x86_64 host. @@ -39,7 +48,6 @@ pub fn build( None }; - use crate::builder::state::{BuildStep, StateTracker}; let mut state = StateTracker::new_with_namespace( &actual_src, spec.build.flags.lib32_variant.then_some("lib32"), @@ -188,6 +196,68 @@ pub fn build( Ok(()) } +pub(crate) fn ensure_host_build( + spec: &PackageSpec, + src_dir: &Path, + export_compiler_flags: bool, +) -> Result { + let host_spec = crate::builder::host_build_spec(spec); + let flags = &host_spec.build.flags; + + let actual_src = resolve_actual_src(&host_spec, src_dir)?; + let build_dir = crate::builder::host_build_dir_for_source(&actual_src, flags); + + fs::create_dir_all(&build_dir)?; + + let env_vars = + crate::builder::standard_build_env(&host_spec, None, true, export_compiler_flags); + let mut state = StateTracker::new_with_namespace(&actual_src, Some("host"))?; + + if !state.is_done(BuildStep::Configured) { + crate::log_info!( + "Running host-side meson setup in {}...", + build_dir.display() + ); + let mut meson_cmd = Command::new("meson"); + meson_cmd.current_dir(&actual_src); + meson_cmd.arg("setup"); + meson_cmd.arg(&build_dir); + + for arg in meson_setup_args(flags, None, &env_vars) { + meson_cmd.arg(arg); + } + + crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars); + + let status = crate::interrupts::command_status(&mut meson_cmd) + .context("Failed to run host meson setup")?; + if !status.success() { + anyhow::bail!("host meson setup failed"); + } + + state.mark_done(BuildStep::Configured)?; + } + + if !state.is_done(BuildStep::PostCompileDone) { + let mut ninja_cmd = Command::new("ninja"); + ninja_cmd.current_dir(&build_dir); + ninja_cmd.arg("-j").arg(num_cpus().to_string()); + + crate::builder::prepare_tool_command(&mut ninja_cmd, &env_vars); + + let status = crate::interrupts::command_status(&mut ninja_cmd) + .with_context(|| format!("Failed to run host ninja for {}", spec.package.name))?; + if !status.success() { + anyhow::bail!("host ninja build failed"); + } + + state.mark_done(BuildStep::PostCompileDone)?; + } + + fs::canonicalize(&build_dir) + .with_context(|| format!("Failed to resolve host build dir: {}", build_dir.display())) +} + fn num_cpus() -> usize { std::thread::available_parallelism() .map(|n| n.get()) @@ -485,6 +555,22 @@ mod tests { assert!(args.iter().any(|a| a == "--buildtype=release")); } + #[test] + fn test_meson_setup_args_expand_host_build_dir() { + let mut flags = BuildFlags::default(); + flags.configure = vec!["-Dtools_dir=$DEPOT_BUILD_HOST_DIR/bin".into()]; + + let args = meson_setup_args( + &flags, + None, + &[( + crate::builder::DEPOT_BUILD_HOST_DIR_ENV.to_string(), + "/tmp/build-host".to_string(), + )], + ); + assert!(args.iter().any(|a| a == "-Dtools_dir=/tmp/build-host/bin")); + } + #[test] fn test_meson_setup_args_include_install_dirs() { let args = meson_setup_args(&BuildFlags::default(), None, &[]); diff --git a/src/builder/mod.rs b/src/builder/mod.rs index b56dc99..e9566fa 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -21,6 +21,13 @@ use std::process::{Command, Stdio}; use walkdir::WalkDir; pub type EnvVars = Vec<(String, String)>; +pub(crate) const DEPOT_BUILD_HOST_DIR_ENV: &str = "DEPOT_BUILD_HOST_DIR"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetBuildKind { + Primary, + Lib32, +} #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct InstallDirs { @@ -55,6 +62,99 @@ fn default_libdir_for_variant(lib32_variant: bool) -> &'static str { } } +fn normalized_arch(arch: &str) -> &str { + match arch.trim() { + "amd64" => "x86_64", + "arm64" => "aarch64", + other => other, + } +} + +fn lib32_arch_for(arch: &str) -> String { + match normalized_arch(arch) { + "x86_64" => "i686".to_string(), + other => other.to_string(), + } +} + +pub(crate) fn host_arch() -> &'static str { + normalized_arch(std::env::consts::ARCH) +} + +pub(crate) fn effective_target_arch( + flags: &crate::package::BuildFlags, + cross: Option<&CrossConfig>, + kind: TargetBuildKind, +) -> String { + match kind { + TargetBuildKind::Lib32 => { + if let Some(cc_cfg) = cross { + return crate::cross::target_arch_from_triple(&crate::cross::lib32_target_triple( + cc_cfg.host_triple(), + )) + .to_string(); + } + if !flags.chost.trim().is_empty() { + return crate::cross::target_arch_from_triple(&crate::cross::lib32_target_triple( + flags.chost.trim(), + )) + .to_string(); + } + let base = if flags.carch.trim().is_empty() { + host_arch() + } else { + flags.carch.trim() + }; + lib32_arch_for(base) + } + TargetBuildKind::Primary => { + if let Some(cc_cfg) = cross { + return crate::cross::target_arch_from_triple(cc_cfg.host_triple()).to_string(); + } + if !flags.chost.trim().is_empty() { + return crate::cross::target_arch_from_triple(flags.chost.trim()).to_string(); + } + if !flags.carch.trim().is_empty() { + return flags.carch.trim().to_string(); + } + host_arch().to_string() + } + } +} + +fn target_arch_differs_from_host( + flags: &crate::package::BuildFlags, + cross: Option<&CrossConfig>, + kind: TargetBuildKind, +) -> bool { + normalized_arch(&effective_target_arch(flags, cross, kind)) != host_arch() +} + +pub(crate) fn default_host_build_dir_name(flags: &crate::package::BuildFlags) -> String { + match flags.build_dir.as_deref().map(str::trim) { + Some(dir) if !dir.is_empty() => format!("{}-host", dir), + _ => "build-host".to_string(), + } +} + +pub(crate) fn host_build_dir_for_source( + src_root: &Path, + flags: &crate::package::BuildFlags, +) -> PathBuf { + src_root.join(default_host_build_dir_name(flags)) +} + +pub(crate) fn host_build_spec(spec: &PackageSpec) -> PackageSpec { + let mut host_spec = spec.clone(); + host_spec.build.flags.lib32_variant = false; + host_spec.build.flags.chost.clear(); + host_spec.build.flags.cbuild.clear(); + host_spec.build.flags.carch = host_arch().to_string(); + host_spec.build.flags.host_build_dir = None; + host_spec.build.flags.build_dir = Some(default_host_build_dir_name(&spec.build.flags)); + host_spec +} + fn configured_install_dir(value: &str, default: &str) -> String { let trimmed = value.trim(); if trimmed.is_empty() { @@ -357,8 +457,14 @@ pub fn standard_build_env( if !flags.cbuild.is_empty() { set_env_var(&mut env_vars, "CBUILD", flags.cbuild.clone()); } - if !flags.carch.is_empty() { - set_env_var(&mut env_vars, "CARCH", flags.carch.clone()); + let target_kind = if flags.lib32_variant { + TargetBuildKind::Lib32 + } else { + TargetBuildKind::Primary + }; + let effective_carch = effective_target_arch(flags, cross, target_kind); + if !effective_carch.is_empty() { + set_env_var(&mut env_vars, "CARCH", effective_carch); } if !flags.prefix.is_empty() { set_env_var(&mut env_vars, "PREFIX", flags.prefix.clone()); @@ -422,6 +528,34 @@ pub fn standard_build_env( env_vars } +pub fn ensure_host_build( + spec: &PackageSpec, + src_dir: &Path, + cross: Option<&CrossConfig>, + export_compiler_flags: bool, + kind: TargetBuildKind, +) -> Result> { + if !spec.build.flags.host_build + || !target_arch_differs_from_host(&spec.build.flags, cross, kind) + { + return Ok(None); + } + + let host_dir = match spec.build.build_type { + BuildType::Autotools => autotools::ensure_host_build(spec, src_dir, export_compiler_flags)?, + BuildType::CMake => cmake::ensure_host_build(spec, src_dir, export_compiler_flags)?, + BuildType::Meson => meson::ensure_host_build(spec, src_dir, export_compiler_flags)?, + other => { + anyhow::bail!( + "build.flags.host_build is currently supported only for autotools/cmake/meson (got {:?})", + other + ); + } + }; + + Ok(Some(host_dir)) +} + /// Prepare a Command with a hermetic environment and some essential variables preserved. pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) { cmd.env_clear(); @@ -498,6 +632,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + host_build_dir: Option<&Path>, ) -> Result<()> { if let Some(cc) = cross { crate::log_info!( @@ -515,25 +650,84 @@ pub fn build( } match spec.build.build_type { - BuildType::Autotools => { - autotools::build(spec, src_dir, destdir, cross, export_compiler_flags) - } - BuildType::CMake => cmake::build(spec, src_dir, destdir, cross, export_compiler_flags), - BuildType::Meson => meson::build(spec, src_dir, destdir, cross, export_compiler_flags), - BuildType::Perl => perl::build(spec, src_dir, destdir, cross, export_compiler_flags), - BuildType::Custom => custom::build(spec, src_dir, destdir, cross, export_compiler_flags), - BuildType::Python => python::build(spec, src_dir, destdir, cross, export_compiler_flags), - BuildType::Rust => rust::build(spec, src_dir, destdir, cross, export_compiler_flags), - BuildType::Bin => bin::build(spec, src_dir, destdir, cross, export_compiler_flags), + BuildType::Autotools => autotools::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::CMake => cmake::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::Meson => meson::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::Perl => perl::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::Custom => custom::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::Python => python::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::Rust => rust::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), + BuildType::Bin => bin::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), BuildType::Meta => { // Metapackages are metadata-only; create an empty staging root and let // packaging/installation metadata carry dependencies. std::fs::create_dir_all(destdir)?; Ok(()) } - BuildType::Makefile => { - makefile::build(spec, src_dir, destdir, cross, export_compiler_flags) - } + BuildType::Makefile => makefile::build( + spec, + src_dir, + destdir, + cross, + export_compiler_flags, + host_build_dir, + ), } } #[cfg(test)] @@ -675,6 +869,41 @@ mod tests { assert!(env.iter().any(|(k, v)| k == "CPP" && v == "clang-cpp")); } + #[test] + fn test_standard_build_env_exports_effective_carch_for_cross_and_lib32() { + let spec = mk_spec(Vec::new(), Vec::new()); + let cross = CrossConfig { + prefix: "aarch64-linux-gnu".into(), + cc: "aarch64-linux-gnu-gcc".into(), + cxx: "aarch64-linux-gnu-g++".into(), + ar: "aarch64-linux-gnu-ar".into(), + ranlib: "aarch64-linux-gnu-ranlib".into(), + strip: "aarch64-linux-gnu-strip".into(), + ld: "aarch64-linux-gnu-ld".into(), + nm: "aarch64-linux-gnu-nm".into(), + objcopy: "aarch64-linux-gnu-objcopy".into(), + objdump: "aarch64-linux-gnu-objdump".into(), + readelf: "aarch64-linux-gnu-readelf".into(), + }; + + let cross_env = standard_build_env(&spec, Some(&cross), true, true); + assert!( + cross_env + .iter() + .any(|(k, v)| k == "CARCH" && v == "aarch64"), + "expected cross builds to export target CARCH" + ); + + let mut lib32_spec = spec.clone(); + lib32_spec.build.flags.lib32_variant = true; + lib32_spec.build.flags.carch = "x86_64".into(); + let lib32_env = standard_build_env(&lib32_spec, None, true, true); + assert!( + lib32_env.iter().any(|(k, v)| k == "CARCH" && v == "i686"), + "expected lib32 builds to export i686 CARCH" + ); + } + #[test] fn test_standard_build_env_respects_export_compiler_flags_toggle() { let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]); diff --git a/src/builder/perl.rs b/src/builder/perl.rs index d20a20d..981eced 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -17,6 +17,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + _host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; if flags.build_dir.is_some() { @@ -459,7 +460,7 @@ exec "$@" spec.build.flags.make_exec = tools_path.join("make").to_string_lossy().into_owned(); spec.build.flags.configure = vec!["CCFLAGS=-fPIC".into()]; - let build_result = build(&spec, src_path, dest_path, None, true); + let build_result = build(&spec, src_path, dest_path, None, true, None); build_result?; @@ -490,7 +491,7 @@ exec "$@" let mut spec = mk_spec("perl-test", "1.0"); spec.build.flags.build_dir = Some("build".into()); - let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true) + let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None) .expect_err("perl build should reject build_dir"); assert!( err.to_string() diff --git a/src/builder/python.rs b/src/builder/python.rs index 3d63740..e7c556c 100644 --- a/src/builder/python.rs +++ b/src/builder/python.rs @@ -32,6 +32,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + _host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; let actual_src = resolve_actual_src(spec, src_dir)?; diff --git a/src/builder/rust.rs b/src/builder/rust.rs index 8bedfa8..f6a2fd7 100755 --- a/src/builder/rust.rs +++ b/src/builder/rust.rs @@ -14,6 +14,7 @@ pub fn build( destdir: &Path, cross: Option<&CrossConfig>, export_compiler_flags: bool, + _host_build_dir: Option<&Path>, ) -> Result<()> { let flags = &spec.build.flags; diff --git a/src/commands.rs b/src/commands.rs index 9b40cde..d660cc5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1182,9 +1182,19 @@ fn build_lib32_companion_package( } crate::log_info!("Running separate lib32 build pass..."); + let host_build_dir = builder::ensure_host_build( + pkg_spec, + src_dir, + cross_config, + export_compiler_flags, + builder::TargetBuildKind::Lib32, + )?; let mut lib32_input = pkg_spec.clone(); lib32_input.build.flags.build_32 = true; - let lib32_build_spec = make_lib32_build_spec(&lib32_input); + let mut lib32_build_spec = make_lib32_build_spec(&lib32_input); + if let Some(host_dir) = host_build_dir.as_ref() { + lib32_build_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned()); + } let lib32_pkg_spec = make_lib32_package_spec(pkg_spec); let lib32_destdir = config .build_dir @@ -1208,6 +1218,7 @@ fn build_lib32_companion_package( &lib32_destdir, cross_config, export_compiler_flags, + host_build_dir.as_deref(), )?; let lib32_src = lib32_destdir.join("usr/lib32"); @@ -4545,6 +4556,16 @@ fn run_direct_install_request( // 1-2. Fetch + extract sources (supports archives and git URL#rev) let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; built_src_dir = Some(src_dir.clone()); + let host_build_dir = builder::ensure_host_build( + &pkg_spec, + &src_dir, + cross_config.as_ref(), + !options.no_flags, + builder::TargetBuildKind::Primary, + )?; + if let Some(host_dir) = host_build_dir.as_ref() { + pkg_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned()); + } // 3. Build let destdir = config @@ -4559,6 +4580,7 @@ fn run_direct_install_request( &destdir, cross_config.as_ref(), !options.no_flags, + host_build_dir.as_deref(), )?; // 3.1 Copy license files into staged tree @@ -5108,6 +5130,17 @@ pub fn run(cli: Cli) -> Result<()> { .as_ref() .map(|p| cross::CrossConfig::from_prefix(p)) .transpose()?; + let host_build_dir = builder::ensure_host_build( + &pkg_spec, + &src_dir, + cross_config.as_ref(), + !no_flags, + builder::TargetBuildKind::Primary, + )?; + if let Some(host_dir) = host_build_dir.as_ref() { + pkg_spec.build.flags.host_build_dir = + Some(host_dir.to_string_lossy().into_owned()); + } if !lib32_only { builder::build( &pkg_spec, @@ -5115,6 +5148,7 @@ pub fn run(cli: Cli) -> Result<()> { &destdir, cross_config.as_ref(), !no_flags, + host_build_dir.as_deref(), )?; if let Some(watcher) = interrupt_watcher.as_ref() { watcher.check()?; diff --git a/src/package/spec.rs b/src/package/spec.rs index f499972..c4993ba 100755 --- a/src/package/spec.rs +++ b/src/package/spec.rs @@ -865,6 +865,11 @@ impl PackageSpec { self.build.flags.lib32_only = b; } } + "host_build" | "host-build" => { + if let Some(b) = toml_value_as_boolish(v) { + self.build.flags.host_build = b; + } + } "configure_lib32" | "configure-lib32" => { if let Some(arr) = v.as_array() { self.build.flags.configure_lib32 = arr @@ -4126,6 +4131,14 @@ pub struct BuildFlags { deserialize_with = "deserialize_boolish" )] pub lib32_only: bool, + /// Perform an additional native host-side helper build when the active target arch differs. + #[serde( + default, + alias = "host-build", + alias = "host_build", + deserialize_with = "deserialize_boolish" + )] + pub host_build: bool, #[serde(default)] pub configure: Vec, /// PEP 517 config settings for Python builds (each entry is `KEY=VALUE` or `KEY`). @@ -4403,6 +4416,9 @@ pub struct BuildFlags { /// Internal runtime marker used to adjust builder behavior for the lib32 variant. #[serde(skip)] pub lib32_variant: bool, + /// Internal runtime marker containing the absolute path to the native host helper build dir. + #[serde(skip)] + pub host_build_dir: Option, } impl Default for BuildFlags { @@ -4432,6 +4448,7 @@ impl Default for BuildFlags { skip_tests: false, build_32: false, lib32_only: false, + host_build: false, configure: Vec::new(), config_settings: Vec::new(), configure_lib32: Vec::new(), @@ -4491,6 +4508,7 @@ impl Default for BuildFlags { build_dir: None, binary_type: String::new(), lib32_variant: false, + host_build_dir: None, } } }