//! GNU Autotools build system (configure && make && make install) use crate::builder::BuildHelperContext; use crate::builder::state::{BuildStep, StateTracker}; use crate::cross::CrossConfig; use crate::fakeroot; use crate::package::PackageSpec; use crate::source::hooks; use anyhow::{Context, Result}; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; pub fn build( spec: &PackageSpec, src_dir: &Path, 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); let export_compiler_flags = export_compiler_flags && !flags.no_flags; let actual_src = resolve_actual_src(spec, src_dir)?; // Create destdir fs::create_dir_all(destdir)?; // 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 { flags.cc.clone() }; if export_compiler_flags && let Some(cflags_str) = env_vars .iter() .find(|(key, _)| key == "CFLAGS") .map(|(_, value)| value.clone()) && !cflags_str.trim().is_empty() { // Expand shell command substitutions like $($CC -print-resource-dir). let expanded = expand_shell_commands(&cflags_str, &cc)?; crate::builder::set_env_var(&mut env_vars, "CFLAGS", expanded); } let mut state = StateTracker::new_with_namespace( &actual_src, spec.build.flags.lib32_variant.then_some("lib32"), )?; // Run configure let build_dir = if let Some(dir) = &flags.build_dir { let bdir = actual_src.join(dir); fs::create_dir_all(&bdir)?; crate::log_info!(" Build directory: {}", bdir.display()); bdir } else { actual_src.clone() }; if !state.is_done(BuildStep::Configured) { crate::log_info!("Running configure..."); let configure_path = resolve_configure_path(spec, &actual_src); crate::log_info!(" Configure path: {}", configure_path.display()); let mut configure_cmd = Command::new(&configure_path); configure_cmd.current_dir(&build_dir); crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars); // Some projects use non-GNU configure scripts that reject --host/--build. // Probe support first and only add these options when advertised. 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); } let supports_host = configure_supports_option(help_text.as_deref(), "--host", &flags.configure_file); let supports_build = configure_supports_option(help_text.as_deref(), "--build", &flags.configure_file); let requested_host = if let Some(cc_cfg) = cross { Some(cc_cfg.host_triple().to_string()) } else if !flags.chost.is_empty() { Some(flags.chost.clone()) } else { None } .map(|host| { if flags.lib32_variant { lib32_host_triple(&host) } else { host } }); let requested_build = if cross.is_some() { CrossConfig::build_triple().ok() } else if !flags.cbuild.is_empty() { Some(flags.cbuild.clone()) } else { None }; if let Some(host) = requested_host { if supports_host { configure_cmd.arg(format!("--host={}", host)); } else { crate::log_info!(" configure does not support --host; skipping {}", host); } } if let Some(build) = requested_build { if supports_build { configure_cmd.arg(format!("--build={}", build)); } else { crate::log_info!(" configure does not support --build; skipping {}", build); } } for arg in &flags.configure { let expanded = expand_configure_arg(spec, arg, &env_vars); configure_cmd.arg(expanded); } for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools, flags)? { add_auto_configure_arg_if_supported(&mut configure_cmd, help_text.as_deref(), &arg); } let status = crate::interrupts::command_status(&mut configure_cmd) .with_context(|| format!("Failed to run configure in {}", build_dir.display()))?; if !status.success() { anyhow::bail!("configure failed with status: {}", status); } // Run post-configure hooks (after configure, before make) hooks::run_post_configure_commands(spec, &actual_src, destdir)?; state.mark_done(BuildStep::Configured)?; } else { crate::log_info!("Skipping configure (already done)"); } if !state.is_done(BuildStep::PostCompileDone) { // Run make 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 { crate::log_info!("Running {} in {}...", make_exec, make_dir.display()); 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 {} in {}", make_exec, make_dir.display()) })?; if !status.success() { anyhow::bail!( "{} failed with status: {} (dir: {})", make_exec, status, make_dir.display() ); } } if spec.should_skip_automatic_tests() { if flags.skip_tests { crate::log_info!("Skipping tests: disabled by build.flags.skip_tests"); } else { crate::log_info!( "Skipping tests: automatic tests are disabled for multilib builds" ); } } else { let test_dirs = resolve_make_dirs( &build_dir, &flags.make_test_dirs, "build.flags.make_test_dirs", )?; let mut ran_any_tests = false; let configured_test_targets = phase_targets(&flags.make_test_target, &flags.make_test_targets, None); for test_dir in test_dirs { let test_targets = if !configured_test_targets.is_empty() { configured_test_targets.clone() } else if make_exec_supports_make_assignments(make_exec) { maybe_find_autotools_test_target(&test_dir, false)? .map(|t| vec![t.to_string()]) .unwrap_or_default() } else { Vec::new() }; if !test_targets.is_empty() { crate::log_info!("Running {} in {}...", make_exec, test_dir.display()); let mut test_cmd = Command::new(make_exec); test_cmd.current_dir(&test_dir); add_make_variable_overrides_if_supported( &mut test_cmd, make_exec, &flags.make_test_vars, "test", )?; for test_target in &test_targets { test_cmd.arg(test_target); } crate::builder::prepare_tool_command(&mut test_cmd, &env_vars); let test_targets_display = test_targets.join(" "); let status = crate::interrupts::command_status(&mut test_cmd).with_context(|| { format!( "Failed to run {} {} in {}", make_exec, test_targets_display, test_dir.display() ) })?; if !status.success() { anyhow::bail!( "{} {} failed with status: {} (dir: {})", make_exec, test_targets_display, status, test_dir.display() ); } ran_any_tests = true; } } if !ran_any_tests { if flags.make_test_dirs.is_empty() { if !configured_test_targets.is_empty() { crate::log_info!("Skipping tests: no test directories to run"); } else if make_exec_supports_make_assignments(make_exec) { crate::log_info!("Skipping tests: no 'check' or 'test' target in Makefile"); } else { crate::log_info!( "Skipping tests: set build.flags.make_test_target when using build.flags.make_exec='{}'", make_exec ); } } else if !configured_test_targets.is_empty() { crate::log_info!( "Skipping tests: no test targets ran in build.flags.make_test_dirs" ); } else if make_exec_supports_make_assignments(make_exec) { crate::log_info!( "Skipping tests: no 'check' or 'test' target in build.flags.make_test_dirs" ); } else { crate::log_info!( "Skipping tests: set build.flags.make_test_target when using build.flags.make_exec='{}'", make_exec ); } } } // Run post-compile hooks (after make, before make install) hooks::run_post_compile_commands(spec, &actual_src, destdir)?; state.mark_done(BuildStep::PostCompileDone)?; } else { crate::log_info!("Skipping make and post-compile hooks (already done)"); } if !state.is_done(BuildStep::PostInstallDone) { // Run make install with internal fakeroot if not root crate::log_info!( "Running {} {}{}...", make_exec, phase_targets( &flags.make_install_target, &flags.make_install_targets, Some("install") ) .join(" "), if fakeroot::is_root() { "" } else { " (with internal fakeroot for build)" } ); let install_destdir = install_destdir_path(&build_dir, destdir, flags.lib32_variant); if flags.lib32_variant { if install_destdir.exists() { fs::remove_dir_all(&install_destdir).with_context(|| { format!( "Failed to clean temporary lib32 install dir: {}", install_destdir.display() ) })?; } fs::create_dir_all(&install_destdir).with_context(|| { format!( "Failed to create temporary lib32 install dir: {}", install_destdir.display() ) })?; } let install_dirs = resolve_make_dirs( &build_dir, &flags.make_install_dirs, "build.flags.make_install_dirs", )?; let install_targets = phase_targets( &flags.make_install_target, &flags.make_install_targets, Some("install"), ); for install_dir in install_dirs { let mut install_cmd = fakeroot::wrap_install_command(make_exec, &install_destdir); install_cmd.current_dir(&install_dir); if make_exec_supports_make_assignments(make_exec) && !has_make_variable_override(&flags.make_install_vars, "DESTDIR") { install_cmd.arg(format!("DESTDIR={}", install_destdir.to_string_lossy())); } add_make_variable_overrides_if_supported( &mut install_cmd, make_exec, &flags.make_install_vars, "install", )?; for install_target in &install_targets { install_cmd.arg(install_target); } let mut install_env = env_vars.clone(); install_env.push(( "DESTDIR".to_string(), install_destdir.to_string_lossy().into_owned(), )); crate::builder::prepare_tool_command(&mut install_cmd, &install_env); let status = crate::interrupts::command_status(&mut install_cmd).with_context(|| { format!( "Failed to run {} {} for {} in {}", make_exec, install_targets.join(" "), spec.package.name, install_dir.display() ) })?; if !status.success() { anyhow::bail!( "{} {} failed with status: {} (dir: {})", make_exec, install_targets.join(" "), status, install_dir.display() ); } } if flags.lib32_variant { crate::builder::stage_lib32_install_tree(&install_destdir, destdir)?; hooks::run_post_install_commands_in_dir(spec, &build_dir, destdir)?; } else { hooks::run_post_install_commands(spec, &actual_src, destdir)?; } state.mark_done(BuildStep::PostInstallDone)?; } else { crate::log_info!("Skipping make install and post-install hooks (already done)"); } 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); } for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools, flags)? { add_auto_configure_arg_if_supported(&mut configure_cmd, help_text.as_deref(), &arg); } 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())) } pub(crate) fn run_helper_configure( context: &BuildHelperContext, source_dir: Option<&Path>, build_dir: Option<&Path>, cross: Option<&CrossConfig>, env_vars: &[(String, String)], extra_args: &[String], ) -> Result<()> { let flags = context.build_flags(); let source_dir = source_dir .map(Path::to_path_buf) .unwrap_or(std::env::current_dir().context("Failed to determine current directory")?); let build_dir = build_dir.map(Path::to_path_buf).unwrap_or_else(|| { flags .build_dir .as_ref() .map(|dir| source_dir.join(dir)) .unwrap_or_else(|| source_dir.clone()) }); fs::create_dir_all(&build_dir) .with_context(|| format!("Failed to create build directory: {}", build_dir.display()))?; let mut helper_env = env_vars.to_vec(); let cc = if let Some(cc_cfg) = cross { cc_cfg.cc.clone() } else { helper_env .iter() .find(|(key, _)| key == "CC") .map(|(_, value)| value.clone()) .unwrap_or_else(|| flags.cc.clone()) }; if let Some(cflags_str) = helper_env .iter() .find(|(key, _)| key == "CFLAGS") .map(|(_, value)| value.clone()) .filter(|value| !value.trim().is_empty()) { let expanded = expand_shell_commands(&cflags_str, &cc)?; crate::builder::set_env_var(&mut helper_env, "CFLAGS", expanded); } let configure_path = helper_configure_path(context, &source_dir); let mut configure_cmd = Command::new(&configure_path); configure_cmd.current_dir(&build_dir); crate::builder::prepare_tool_command(&mut configure_cmd, &helper_env); let help_text = configure_help_text(&configure_path, &build_dir, &helper_env); 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); } let supports_host = configure_supports_option(help_text.as_deref(), "--host", &flags.configure_file); let supports_build = configure_supports_option(help_text.as_deref(), "--build", &flags.configure_file); let requested_host = if let Some(cc_cfg) = cross { Some(cc_cfg.host_triple().to_string()) } else if !flags.chost.is_empty() { Some(flags.chost.clone()) } else { None } .map(|host| { if flags.lib32_variant { lib32_host_triple(&host) } else { host } }); let requested_build = if cross.is_some() { CrossConfig::build_triple().ok() } else if !flags.cbuild.is_empty() { Some(flags.cbuild.clone()) } else { None }; if let Some(host) = requested_host { if supports_host { configure_cmd.arg(format!("--host={host}")); } else { crate::log_info!(" configure does not support --host; skipping {}", host); } } if let Some(build) = requested_build { if supports_build { configure_cmd.arg(format!("--build={build}")); } else { crate::log_info!(" configure does not support --build; skipping {}", build); } } for arg in &flags.configure { configure_cmd.arg(expand_with_envs(&context.expand_vars(arg), &helper_env)); } for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools, &flags)? { add_auto_configure_arg_if_supported(&mut configure_cmd, help_text.as_deref(), &arg); } for arg in extra_args { add_auto_configure_arg_if_supported( &mut configure_cmd, help_text.as_deref(), &expand_with_envs(&context.expand_vars(arg), &helper_env), ); } let status = crate::interrupts::command_status(&mut configure_cmd) .with_context(|| format!("Failed to run helper configure in {}", build_dir.display()))?; if !status.success() { anyhow::bail!("configure failed with status: {}", status); } Ok(()) } pub(crate) fn run_helper_install( context: &BuildHelperContext, build_dir: Option<&Path>, env_vars: &[(String, String)], extra_args: &[String], ) -> Result<()> { let flags = context.build_flags(); let source_dir = helper_source_dir(); let build_dir = build_dir.map(Path::to_path_buf).unwrap_or_else(|| { flags .build_dir .as_ref() .map(|dir| source_dir.join(dir)) .unwrap_or_else(|| source_dir.clone()) }); let destdir = std::env::var("DESTDIR").context("DESTDIR must be set for autotools_install")?; let make_exec = resolve_make_exec(&flags.make_exec); let install_dirs = resolve_make_dirs( &build_dir, &flags.make_install_dirs, "build.flags.make_install_dirs", )?; let install_targets = phase_targets( &flags.make_install_target, &flags.make_install_targets, Some("install"), ); for install_dir in install_dirs { let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir)); install_cmd.current_dir(&install_dir); if make_exec_supports_make_assignments(make_exec) && !has_make_variable_override(&flags.make_install_vars, "DESTDIR") { install_cmd.arg(format!("DESTDIR={destdir}")); } add_make_variable_overrides_if_supported( &mut install_cmd, make_exec, &flags.make_install_vars, "install", )?; for install_target in &install_targets { install_cmd.arg(install_target); } for arg in extra_args { install_cmd.arg(context.expand_vars(arg)); } let mut install_env = env_vars.to_vec(); crate::builder::set_env_var(&mut install_env, "DESTDIR", destdir.clone()); crate::builder::prepare_tool_command(&mut install_cmd, &install_env); let status = crate::interrupts::command_status(&mut install_cmd).with_context(|| { format!( "Failed to run helper {} {} in {}", make_exec, install_targets.join(" "), install_dir.display() ) })?; if !status.success() { anyhow::bail!( "{} {} failed with status: {} (dir: {})", make_exec, install_targets.join(" "), status, install_dir.display() ); } } Ok(()) } fn num_cpus() -> usize { std::thread::available_parallelism() .map(|n| n.get()) .unwrap_or(1) } pub(crate) fn resolve_actual_src(spec: &PackageSpec, src_dir: &Path) -> Result { let flags = &spec.build.flags; let source_subdir = spec.expand_vars(&flags.source_subdir); let actual_src = if source_subdir.is_empty() { src_dir.to_path_buf() } else { src_dir.join(&source_subdir) }; if !actual_src.exists() { anyhow::bail!( "Source directory not found: {} (source_subdir: {} -> {})", actual_src.display(), flags.source_subdir, source_subdir ); } Ok(actual_src) } fn resolve_configure_path(spec: &PackageSpec, actual_src: &Path) -> PathBuf { let configured = spec.expand_vars(&spec.build.flags.configure_file); let trimmed = configured.trim(); if trimmed.is_empty() { return actual_src.join("configure"); } let path = Path::new(trimmed); if path.is_absolute() { path.to_path_buf() } else { actual_src.join(path) } } fn helper_configure_path(context: &BuildHelperContext, actual_src: &Path) -> PathBuf { let flags = context.build_flags(); let configured = context.expand_vars(&flags.configure_file); let trimmed = configured.trim(); if trimmed.is_empty() { return actual_src.join("configure"); } let path = Path::new(trimmed); if path.is_absolute() { path.to_path_buf() } else { actual_src.join(path) } } fn configure_help_text( configure_path: &Path, build_dir: &Path, env_vars: &crate::builder::EnvVars, ) -> Option { let mut help_cmd = Command::new(configure_path); help_cmd.current_dir(build_dir); help_cmd.arg("--help"); crate::builder::prepare_command(&mut help_cmd, env_vars); let output = help_cmd.output().ok()?; let mut text = String::new(); text.push_str(&String::from_utf8_lossy(&output.stdout)); text.push_str(&String::from_utf8_lossy(&output.stderr)); if output.status.success() || looks_like_configure_help_text(&text) { return Some(text); } None } fn looks_like_configure_help_text(text: &str) -> bool { let trimmed = text.trim(); if trimmed.is_empty() { return false; } let lower = trimmed.to_ascii_lowercase(); lower.contains("usage:") || lower.contains("options:") || lower.contains("--help") || lower.contains("--prefix") } fn configure_help_supports_option(help_text: &str, option: &str) -> bool { configure_help_option_aliases(option) .iter() .any(|candidate| configure_help_mentions_option(help_text, candidate)) } fn configure_help_mentions_option(help_text: &str, option: &str) -> bool { help_text.match_indices(option).any(|(idx, _)| { let before = help_text[..idx].chars().next_back(); let after = help_text[idx + option.len()..].chars().next(); is_configure_option_boundary(before) && is_configure_option_boundary(after) }) } fn is_configure_option_boundary(ch: Option) -> bool { ch.is_none_or(|ch| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-') } fn configure_help_option_aliases(option: &str) -> Vec { let mut aliases = vec![option.to_string()]; if let Some(feature) = option.strip_prefix("--enable-") { aliases.push(format!("--disable-{feature}")); } if let Some(feature) = option.strip_prefix("--disable-") { aliases.push(format!("--enable-{feature}")); } if let Some(feature) = option.strip_prefix("--with-") { aliases.push(format!("--without-{feature}")); } if let Some(feature) = option.strip_prefix("--without-") { aliases.push(format!("--with-{feature}")); } aliases } fn configure_supports_option(help_text: Option<&str>, option: &str, configure_file: &str) -> bool { help_text .map(|text| configure_help_supports_option(text, option)) .unwrap_or(configure_file.trim().is_empty()) } fn has_configure_option_prefix(args: &[String], option: &str) -> bool { let with_eq = format!("{option}="); args.iter().any(|arg| { let trimmed = arg.trim(); trimmed == option || trimmed.starts_with(&with_eq) }) } fn configure_long_option(arg: &str) -> Option<&str> { let trimmed = arg.trim(); if !trimmed.starts_with("--") || trimmed == "--" { return None; } let end = trimmed .find(|ch: char| ch == '=' || ch.is_whitespace()) .unwrap_or(trimmed.len()); Some(&trimmed[..end]) } fn add_auto_configure_arg_if_supported( configure_cmd: &mut Command, help_text: Option<&str>, arg: &str, ) { if let Some(help_text) = help_text && let Some(option) = configure_long_option(arg) && !configure_help_supports_option(help_text, option) { let trimmed = arg.trim(); if trimmed == option { crate::log_info!(" configure does not support {}; skipping", option); } else { crate::log_info!( " configure does not support {}; skipping {}", option, trimmed ); } return; } configure_cmd.arg(arg); } fn default_configure_install_dirs( flags: &crate::package::BuildFlags, help_text: Option<&str>, ) -> Vec { let dirs = crate::builder::install_dirs(flags); let defaults = [ ("--bindir", dirs.bindir), ("--sbindir", dirs.sbindir), ("--libdir", dirs.libdir), ("--libexecdir", dirs.libexecdir), ("--sysconfdir", dirs.sysconfdir), ("--localstatedir", dirs.localstatedir), ("--sharedstatedir", dirs.sharedstatedir), ("--includedir", dirs.includedir), ("--datarootdir", dirs.datarootdir), ("--datadir", dirs.datadir), ("--mandir", dirs.mandir), ("--infodir", dirs.infodir), ]; defaults .iter() .filter(|(option, _)| { help_text.is_some_and(|text| configure_help_supports_option(text, option)) }) .filter(|(option, _)| !has_configure_option_prefix(&flags.configure, option)) .map(|(option, value)| format!("{option}={value}")) .collect() } fn install_destdir_path(build_dir: &Path, destdir: &Path, lib32_variant: bool) -> PathBuf { if lib32_variant { build_dir.join("destdir") } else { destdir.to_path_buf() } } fn lib32_host_triple(host: &str) -> String { host.replace("x86_64", "i686") } fn find_autotools_test_target(build_dir: &Path) -> Result> { for target in ["check", "test"] { if makefile_has_target(build_dir, target)? { return Ok(Some(target)); } } Ok(None) } fn maybe_find_autotools_test_target( build_dir: &Path, skip_tests: bool, ) -> Result> { if skip_tests { return Ok(None); } find_autotools_test_target(build_dir) } pub(crate) fn resolve_make_dirs( build_dir: &Path, dirs: &[String], field_name: &str, ) -> Result> { if dirs.is_empty() { return Ok(vec![build_dir.to_path_buf()]); } let canonical_build = fs::canonicalize(build_dir) .with_context(|| format!("Failed to resolve build directory {}", build_dir.display()))?; let mut out = Vec::with_capacity(dirs.len()); for raw in dirs { let trimmed = raw.trim(); if trimmed.is_empty() { anyhow::bail!("{} contains an empty directory entry", field_name); } let rel = Path::new(trimmed); if rel.is_absolute() { anyhow::bail!("{} entry '{}' must be a relative path", field_name, trimmed); } let candidate = build_dir.join(rel); let canonical_candidate = fs::canonicalize(&candidate).with_context(|| { format!( "{} entry '{}' does not exist in {}", field_name, trimmed, build_dir.display() ) })?; if !canonical_candidate.starts_with(&canonical_build) { anyhow::bail!( "{} entry '{}' resolves outside build directory", field_name, trimmed ); } if !canonical_candidate.is_dir() { anyhow::bail!("{} entry '{}' is not a directory", field_name, trimmed); } out.push(canonical_candidate); } Ok(out) } fn makefile_has_target(build_dir: &Path, target: &str) -> Result { for name in ["GNUmakefile", "Makefile", "makefile"] { let path = build_dir.join(name); if !path.exists() { continue; } let content = fs::read_to_string(&path) .with_context(|| format!("Failed to read makefile: {}", path.display()))?; if makefile_content_has_target(&content, target) { return Ok(true); } } Ok(false) } fn makefile_content_has_target(content: &str, target: &str) -> bool { for raw in content.lines() { let line = raw.trim_start(); if line.is_empty() || line.starts_with('#') || line.starts_with('\t') { continue; } if let Some(rest) = line.strip_prefix(".PHONY:") { if rest.split_whitespace().any(|t| t == target) { return true; } continue; } let Some(colon_pos) = line.find(':') else { continue; }; let rhs = &line[colon_pos + 1..]; if rhs.starts_with('=') { // Variable assignment (e.g. FOO:=bar), not a make target. continue; } let lhs = &line[..colon_pos]; if lhs.split_whitespace().any(|t| t == target) { return true; } } false } fn nonempty_trimmed(value: &str) -> Option<&str> { let trimmed = value.trim(); (!trimmed.is_empty()).then_some(trimmed) } fn helper_source_dir() -> PathBuf { std::env::var(crate::builder::DEPOT_BUILD_HELPER_SOURCE_DIR_ENV) .map(PathBuf::from) .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) } pub(crate) fn resolve_make_exec(configured: &str) -> &str { nonempty_trimmed(configured).unwrap_or("make") } pub(crate) fn phase_targets(single: &str, many: &[String], default: Option<&str>) -> Vec { let mut targets = Vec::new(); if let Some(target) = nonempty_trimmed(single) { targets.push(target.to_string()); } for target in many { if let Some(target) = nonempty_trimmed(target) { targets.push(target.to_string()); } } if targets.is_empty() && let Some(default_target) = default { targets.push(default_target.to_string()); } targets } pub(crate) fn make_exec_supports_make_assignments(make_exec: &str) -> bool { let Some(tool) = Path::new(make_exec) .file_name() .and_then(|name| name.to_str()) .map(|name| name.to_ascii_lowercase()) else { return false; }; tool == "make" || tool == "gmake" || tool == "bmake" || tool == "nmake" || tool.ends_with("-make") || tool.ends_with("make.exe") } pub(crate) fn add_make_variable_overrides_if_supported( cmd: &mut Command, make_exec: &str, vars: &[String], phase: &str, ) -> Result<()> { if make_exec_supports_make_assignments(make_exec) { return add_make_variable_overrides(cmd, vars, phase); } if !vars.is_empty() { let field_name = match phase { "build" => "build.flags.make_vars", "test" => "build.flags.make_test_vars", "install" => "build.flags.make_install_vars", _ => "build.flags.make_*_vars", }; anyhow::bail!( "{} is only supported with make-like executables; build.flags.make_exec='{}'", field_name, make_exec ); } Ok(()) } fn add_make_variable_overrides(cmd: &mut Command, vars: &[String], phase: &str) -> Result<()> { for raw in vars { let var = raw.trim(); if var.is_empty() { continue; } let Some((name, _value)) = var.split_once('=') else { anyhow::bail!( "Invalid make variable override '{}' for {} phase; expected NAME=VALUE", var, phase ); }; let name = name.trim(); if name.is_empty() || name.contains(char::is_whitespace) { anyhow::bail!( "Invalid make variable override '{}' for {} phase; expected NAME=VALUE", var, phase ); } cmd.arg(var); } Ok(()) } pub(crate) fn has_make_variable_override(vars: &[String], name: &str) -> bool { vars.iter().any(|raw| { let var = raw.trim(); let Some((lhs, _rhs)) = var.split_once('=') else { return false; }; lhs.trim() == name }) } /// Expand shell command substitutions like $($CC -print-resource-dir) in a string fn expand_shell_commands(input: &str, cc: &str) -> Result { let mut result = input.to_string(); // Find and expand $(...) patterns while let Some(start) = result.find("$(") { let rest = &result[start + 2..]; if let Some(end) = rest.find(')') { let cmd = &rest[..end]; // Replace $CC with actual compiler let cmd = cmd.replace("$CC", cc); // Execute the command via shell let output = Command::new("sh").arg("-c").arg(&cmd).output(); let replacement = match output { Ok(out) if out.status.success() => { String::from_utf8_lossy(&out.stdout).trim().to_string() } _ => { // Silently skip failed commands (e.g., gcc doesn't support -print-resource-dir) crate::log_warn!("Shell command '{}' failed, skipping", cmd); String::new() } }; result = format!("{}{}{}", &result[..start], replacement, &rest[end + 1..]); } else { break; // Malformed, no closing paren } } Ok(result) } fn expand_env_vars(input: &str) -> String { let mut result = input.to_string(); for (key, value) in std::env::vars() { result = result.replace(&format!("${}", key), &value); result = result.replace(&format!("${{{}}}", key), &value); } result } fn expand_with_envs(input: &str, envs: &[(String, String)]) -> String { let mut result = input.to_string(); for (k, v) in envs { result = result.replace(&format!("${}", k), v); result = result.replace(&format!("${{{}}}", k), v); } expand_env_vars(&result) } fn expand_configure_arg(spec: &PackageSpec, arg: &str, envs: &[(String, String)]) -> String { let with_spec_vars = spec.expand_vars(arg); expand_with_envs(&with_spec_vars, envs) } #[cfg(test)] mod tests { use super::*; use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec}; use std::path::PathBuf; use tempfile::tempdir; #[test] fn test_expand_shell_commands_simple() -> Result<()> { let out = expand_shell_commands("x $(echo foo) y", "gcc")?; assert_eq!(out, "x foo y"); Ok(()) } #[test] fn test_expand_shell_commands_replace_cc() -> Result<()> { // The command contains $CC which should be replaced with provided cc let out = expand_shell_commands("start $($CC -v >/dev/null; echo OK) end", "mycc")?; // Since the inner command echoes OK, after replacing $CC it should run and include OK assert!(out.contains("OK") || out.contains("")); Ok(()) } #[test] fn test_expand_with_envs_prefers_provided_envs() { let envs = vec![ ("CARCH".to_string(), "x86_64".to_string()), ("CHOST".to_string(), "x86_64-sfg-linux-gnu".to_string()), ]; let out = expand_with_envs("--with-gcc-arch=$CARCH --host=${CHOST}", &envs); assert!(out.contains("--with-gcc-arch=x86_64")); assert!(out.contains("--host=x86_64-sfg-linux-gnu")); } #[test] fn test_expand_configure_arg_expands_spec_and_env_vars() { 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, built_against: Vec::new(), 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![("CARCH".to_string(), "aarch64".to_string())]; let expanded = expand_configure_arg(&spec, "--program-prefix=$name-$version-$CARCH-", &envs); 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, built_against: Vec::new(), 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(); assert!(n >= 1); } #[test] fn test_configure_help_supports_host_build() { let help = "Usage: configure [OPTION]...\n --host=HOST cross host\n --build=BUILD"; assert!(configure_help_supports_option(help, "--host")); assert!(configure_help_supports_option(help, "--build")); assert!(!configure_help_supports_option(help, "--target")); } #[test] fn test_configure_help_supports_enable_disable_aliases() { let help = " --enable-static build static libraries\n --with-zlib=DIR"; assert!(configure_help_supports_option(help, "--disable-static")); assert!(configure_help_supports_option(help, "--without-zlib")); } #[test] fn test_configure_help_supports_option_requires_exact_match() { let help = "\ --host-cc=HOSTCC use host C compiler --build-suffix=SUFFIX library name suffix []"; assert!(!configure_help_supports_option(help, "--host")); assert!(!configure_help_supports_option(help, "--build")); } #[test] fn test_looks_like_configure_help_text_accepts_bootstrap_style_output() { let help = "\ Usage: ./bootstrap [...] Options: --help --prefix=PREFIX"; assert!(looks_like_configure_help_text(help)); } #[test] fn test_looks_like_configure_help_text_rejects_non_help_output() { assert!(!looks_like_configure_help_text("")); assert!(!looks_like_configure_help_text( "Unknown option: --disable-static" )); } #[test] fn test_configure_supports_option_defaults_by_configure_file_usage() { assert!(configure_supports_option(None, "--host", "")); assert!(!configure_supports_option( None, "--host", "build-aux/Configure" )); } #[test] fn test_default_configure_install_dirs_injects_expected_paths() { let flags = BuildFlags::default(); let help = "\ --bindir=DIR --sbindir=DIR --libdir=DIR --libexecdir=DIR --sysconfdir=DIR --localstatedir=DIR --sharedstatedir=DIR --includedir=DIR --datarootdir=DIR --datadir=DIR --mandir=DIR --infodir=DIR"; let args = default_configure_install_dirs(&flags, Some(help)); assert!(args.iter().any(|a| a == "--bindir=/usr/bin")); assert!(args.iter().any(|a| a == "--sbindir=/usr/bin")); assert!(args.iter().any(|a| a == "--libdir=/usr/lib")); assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib")); assert!(args.iter().any(|a| a == "--sysconfdir=/etc")); assert!(args.iter().any(|a| a == "--localstatedir=/var")); assert!(args.iter().any(|a| a == "--sharedstatedir=/var/lib")); assert!(args.iter().any(|a| a == "--includedir=/usr/include")); assert!(args.iter().any(|a| a == "--datarootdir=/usr/share")); assert!(args.iter().any(|a| a == "--datadir=/usr/share")); assert!(args.iter().any(|a| a == "--mandir=/usr/share/man")); assert!(args.iter().any(|a| a == "--infodir=/usr/share/info")); } #[test] fn test_default_configure_install_dirs_respects_explicit_user_overrides() { let flags = BuildFlags { configure: vec![ "--sbindir=/sbin".to_string(), "--libdir=/custom/lib".to_string(), "--datadir=/custom/share".to_string(), ], ..BuildFlags::default() }; let help = "--bindir=DIR --sbindir=DIR --libdir=DIR --datadir=DIR"; let args = default_configure_install_dirs(&flags, Some(help)); assert!(!args.iter().any(|a| a.starts_with("--sbindir="))); assert!(!args.iter().any(|a| a.starts_with("--libdir="))); assert!(!args.iter().any(|a| a.starts_with("--datadir="))); assert!(args.iter().any(|a| a == "--bindir=/usr/bin")); } #[test] fn test_default_configure_install_dirs_lib32_uses_lib32_dirs() { let help = "--libdir=DIR --libexecdir=DIR"; let flags = BuildFlags { lib32_variant: true, ..BuildFlags::default() }; let args = default_configure_install_dirs(&flags, Some(help)); assert!(args.iter().any(|a| a == "--libdir=/usr/lib32")); assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32")); } #[test] fn test_default_configure_install_dirs_skips_when_not_advertised() { let flags = BuildFlags::default(); let args = default_configure_install_dirs(&flags, Some("--prefix=PREFIX")); assert!(args.is_empty()); } #[test] fn test_configure_long_option_extracts_long_option_name() { assert_eq!( configure_long_option(" --disable-static "), Some("--disable-static") ); assert_eq!( configure_long_option("--with-zlib=/usr"), Some("--with-zlib") ); assert_eq!(configure_long_option("prefix=/usr"), None); assert_eq!(configure_long_option("-C"), None); } #[test] fn test_add_auto_configure_arg_if_supported_skips_unsupported_long_option() { let mut cmd = Command::new("configure"); add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "--disable-nls"); let args: Vec = cmd .get_args() .map(|arg| arg.to_string_lossy().to_string()) .collect(); assert!(args.is_empty()); } #[test] fn test_add_auto_configure_arg_if_supported_keeps_supported_alias_and_non_option_args() { let mut cmd = Command::new("configure"); add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "--disable-static"); add_auto_configure_arg_if_supported(&mut cmd, Some("--enable-static"), "srcdir"); let args: Vec = cmd .get_args() .map(|arg| arg.to_string_lossy().to_string()) .collect(); assert_eq!(args, vec!["--disable-static", "srcdir"]); } #[test] fn test_install_destdir_path_uses_build_dir_for_lib32() { let build_dir = Path::new("/tmp/build"); let destdir = Path::new("/tmp/pkg"); assert_eq!( install_destdir_path(build_dir, destdir, false), destdir.to_path_buf() ); assert_eq!( install_destdir_path(build_dir, destdir, true), build_dir.join("destdir") ); } #[test] fn test_makefile_content_has_target_detects_check_and_test() { let content = r#" .PHONY: all check all: @echo all check: @echo check "#; assert!(makefile_content_has_target(content, "check")); assert!(!makefile_content_has_target(content, "test")); } #[test] fn test_makefile_content_has_target_ignores_assignments() { let content = r#" TEST := value VAR:=$(shell echo hi) foo: bar @true "#; assert!(!makefile_content_has_target(content, "TEST")); assert!(!makefile_content_has_target(content, "VAR")); assert!(!makefile_content_has_target(content, "check")); } #[test] fn test_maybe_find_autotools_test_target_respects_skip_tests() -> Result<()> { let tmp = tempdir().unwrap(); std::fs::write(tmp.path().join("Makefile"), "check:\n\t@true\n").unwrap(); let skipped = maybe_find_autotools_test_target(tmp.path(), true)?; assert_eq!(skipped, None); let detected = maybe_find_autotools_test_target(tmp.path(), false)?; assert_eq!(detected, Some("check")); Ok(()) } #[test] fn test_resolve_make_dirs_defaults_to_build_dir() -> Result<()> { let tmp = tempdir().unwrap(); let dirs = resolve_make_dirs(tmp.path(), &[], "build.flags.make_dirs")?; assert_eq!(dirs, vec![tmp.path().to_path_buf()]); Ok(()) } #[test] fn test_resolve_make_dirs_resolves_multiple_relative_dirs() -> Result<()> { let tmp = tempdir().unwrap(); std::fs::create_dir_all(tmp.path().join("lib"))?; std::fs::create_dir_all(tmp.path().join("libelf"))?; let dirs = resolve_make_dirs( tmp.path(), &["lib".to_string(), "libelf".to_string()], "build.flags.make_dirs", )?; assert_eq!( dirs, vec![tmp.path().join("lib"), tmp.path().join("libelf")] ); Ok(()) } #[test] fn test_add_make_variable_overrides_accepts_valid_assignments() -> Result<()> { let mut cmd = Command::new("make"); add_make_variable_overrides( &mut cmd, &[ "CC=clang".to_string(), "V=1".to_string(), " CFLAGS=-O2 -pipe ".to_string(), ], "build", )?; let args: Vec = cmd .get_args() .map(|a| a.to_string_lossy().to_string()) .collect(); assert_eq!(args, vec!["CC=clang", "V=1", "CFLAGS=-O2 -pipe"]); Ok(()) } #[test] fn test_add_make_variable_overrides_rejects_invalid_assignment() { let mut cmd = Command::new("make"); let err = add_make_variable_overrides(&mut cmd, &["not-an-assignment".to_string()], "test") .expect_err("expected invalid assignment to fail"); assert!(err.to_string().contains("expected NAME=VALUE")); } #[test] fn test_has_make_variable_override_detects_destdir() { assert!(has_make_variable_override( &["DESTDIR=/tmp/pkg".to_string()], "DESTDIR" )); assert!(has_make_variable_override( &[" DESTDIR =/tmp/pkg ".to_string()], "DESTDIR" )); assert!(!has_make_variable_override( &["V=1".to_string(), "PREFIX=/usr".to_string()], "DESTDIR" )); } #[test] fn test_resolve_make_exec_defaults_and_trims() { assert_eq!(resolve_make_exec(""), "make"); assert_eq!(resolve_make_exec(" "), "make"); assert_eq!(resolve_make_exec(" ninja "), "ninja"); } #[test] fn test_make_exec_supports_make_assignments_detects_make_variants() { assert!(make_exec_supports_make_assignments("make")); assert!(make_exec_supports_make_assignments("/usr/bin/gmake")); assert!(!make_exec_supports_make_assignments("ninja")); } #[test] fn test_phase_targets_merges_singular_plural_and_default() { assert_eq!( phase_targets("bootstrap", &["stage1".into(), "stage2".into()], None), vec![ "bootstrap".to_string(), "stage1".to_string(), "stage2".to_string() ] ); assert_eq!( phase_targets("", &[], Some("install")), vec!["install".to_string()] ); } #[test] fn test_add_make_variable_overrides_if_supported_rejects_ninja_vars() { let mut cmd = Command::new("ninja"); let err = add_make_variable_overrides_if_supported( &mut cmd, "ninja", &["V=1".to_string()], "build", ) .expect_err("ninja should reject make variable override syntax"); assert!(err.to_string().contains("build.flags.make_vars")); } #[test] fn test_resolve_actual_src_expands_source_subdir_vars() { let tmp = tempdir().unwrap(); let src_root = tmp.path().join("srcroot"); let expanded = src_root.join("expect5.45.4").join("unix"); std::fs::create_dir_all(&expanded).unwrap(); let spec = PackageSpec { package: PackageInfo { name: "expect".into(), real_name: None, version: "5.45.4".into(), revision: 1, description: "d".into(), homepage: "h".into(), abi_breaking: false, built_against: Vec::new(), 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 { source_subdir: "$name$version/unix".into(), ..BuildFlags::default() }, }, dependencies: Dependencies::default(), package_alternatives: Default::default(), package_dependencies: Default::default(), spec_dir: PathBuf::from("."), }; let resolved = resolve_actual_src(&spec, &src_root).unwrap(); assert_eq!(resolved, expanded); } #[test] fn test_resolve_configure_path_defaults_to_source_configure() { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), abi_breaking: false, built_against: Vec::new(), 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 actual_src = PathBuf::from("/tmp/src"); let configure = resolve_configure_path(&spec, &actual_src); assert_eq!(configure, actual_src.join("configure")); } #[test] fn test_resolve_configure_path_uses_configure_file_and_expands_vars() { let flags = BuildFlags { configure_file: "build-aux/$name-configure".into(), ..BuildFlags::default() }; let spec = PackageSpec { package: PackageInfo { name: "foo".into(), real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), abi_breaking: false, built_against: Vec::new(), license: vec!["MIT".into()], }, packages: Vec::new(), alternatives: Default::default(), manual_sources: Vec::new(), source: Vec::new(), build: Build { build_type: BuildType::Autotools, flags, }, dependencies: Dependencies::default(), package_alternatives: Default::default(), package_dependencies: Default::default(), spec_dir: PathBuf::from("."), }; let actual_src = PathBuf::from("/tmp/src"); let configure = resolve_configure_path(&spec, &actual_src); assert_eq!(configure, actual_src.join("build-aux/foo-configure")); } }