feat: Enhance package specification to support case-insensitive flags and add C++ compiler configuration

This commit is contained in:
2026-02-16 23:24:26 -06:00
parent 9a00608104
commit af0571c33b
2 changed files with 138 additions and 18 deletions
+117 -17
View File
@@ -17,19 +17,8 @@ pub fn build(
let flags = &spec.build.flags;
// Determine actual source directory (support source_subdir)
let actual_src = if !flags.source_subdir.is_empty() {
src_dir.join(&flags.source_subdir)
} else {
src_dir.to_path_buf()
};
let actual_src = resolve_actual_src(spec, src_dir)?;
if !actual_src.exists() {
anyhow::bail!(
"Source directory not found: {} (source_subdir: {})",
actual_src.display(),
flags.source_subdir
);
}
let build_dir = actual_src.join("build");
@@ -70,6 +59,10 @@ pub fn build(
}
env_vars.push(("CC", cc));
// Ensure CXX is exported for configure flags that reference $CXX
// prefer cross-config CXX when cross-compiling (handled earlier), otherwise use flags.cxx
env_vars.push(("CXX", flags.cxx.clone()));
// Export rootfs for build scripts
env_vars.push(("DEPOT_ROOTFS", flags.rootfs.clone()));
@@ -117,10 +110,11 @@ pub fn build(
cmake_cmd.arg(format!("-DCMAKE_TOOLCHAIN_FILE={}", tf.display()));
}
// Add custom configure flags from spec (supports cross-compilation overrides)
// Add custom configure flags from spec (supports cross-compilation overrides).
// Expand using env-vars we will supply to the child process first so patterns
// like `$CXX` (coming from flags.cxx) are substituted.
for flag in &flags.configure {
// Expand environment variables in the flag
let expanded = expand_env_vars(flag);
let expanded = expand_with_envs(flag, &env_vars);
cmake_cmd.arg(&expanded);
}
@@ -196,7 +190,7 @@ pub fn build(
/// Expand environment variables in a string (e.g., $DEPOT_SYSROOT)
fn expand_env_vars(input: &str) -> String {
let mut result = input.to_string();
// Simple expansion for $VAR and ${VAR} patterns
// Simple expansion for $VAR and ${VAR} patterns using process environment only
for (key, value) in std::env::vars() {
result = result.replace(&format!("${}", key), &value);
result = result.replace(&format!("${{{}}}", key), &value);
@@ -204,16 +198,73 @@ fn expand_env_vars(input: &str) -> String {
result
}
/// Expand using a provided set of env vars (used to expand flags before spawning child).
fn expand_with_envs(input: &str, envs: &Vec<(&str, 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 num_cpus() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
/// Resolve `source_subdir` with multiple fallbacks:
/// - empty -> use `src_dir`
/// - absolute path -> use if exists
/// - `src_dir/<sub>` -> use if exists
/// - `spec.spec_dir/<sub>` -> use if exists (supports `../llvm`)
/// - bare relative path (cwd)
fn resolve_actual_src(spec: &crate::package::PackageSpec, src_dir: &Path) -> anyhow::Result<std::path::PathBuf> {
let flags = &spec.build.flags;
if flags.source_subdir.is_empty() {
return Ok(src_dir.to_path_buf());
}
let candidate = std::path::Path::new(&flags.source_subdir);
// 1) absolute path -> use directly
if candidate.is_absolute() {
if candidate.exists() {
return Ok(candidate.to_path_buf());
}
anyhow::bail!("Source directory not found: {} (source_subdir: {})", candidate.display(), flags.source_subdir);
}
// 2) src_dir/<source_subdir>
let under_src = src_dir.join(&flags.source_subdir);
if under_src.exists() {
return Ok(under_src);
}
// 3) spec_dir/<source_subdir> (useful for ../llvm relative to spec)
let spec_path = spec.spec_dir.join(&flags.source_subdir);
if spec_path.exists() {
return Ok(spec_path);
}
// 4) bare relative path (relative to CWD)
if candidate.exists() {
return Ok(candidate.to_path_buf());
}
// fallback error
anyhow::bail!(
"Source directory not found: {} (tried src_dir, spec_dir, and absolute path)",
flags.source_subdir
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, PackageInfo, PackageSpec};
use tempfile::tempdir;
#[test]
fn test_expand_env_vars_replaces_vars() {
// Set a test env var
@@ -224,9 +275,58 @@ mod tests {
assert_eq!(out, "bar and bar");
}
#[test]
fn test_expand_with_envs_prefers_provided_envs() {
let envs = vec![("CXX", "my-cxx".to_string()), ("CC", "my-cc".to_string())];
let s = "-DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=${CXX} -DROOT=$HOME";
let out = expand_with_envs(s, &envs);
assert!(out.contains("my-cc"));
assert!(out.contains("my-cxx"));
// $HOME should be expanded from process env (may be present)
}
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn resolve_actual_src_prefers_srcdir_then_specdir_and_handles_absolute() {
let tmp = tempdir().unwrap();
let src_root = tmp.path().join("srcroot");
let spec_dir = tmp.path().join("specdir");
let external = tmp.path().join("external");
std::fs::create_dir_all(&src_root.join("sub")).unwrap();
// create directories for candidates
std::fs::create_dir_all(&spec_dir.join("../llvm")).unwrap();
std::fs::create_dir_all(&external).unwrap();
let spec = PackageSpec {
package: PackageInfo { name: "x".into(), version: "1.0".into(), revision: 1, description: "".into(), homepage: "".into(), license: "MIT".into() },
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build { build_type: BuildType::CMake, flags: BuildFlags { source_subdir: "sub".into(), ..BuildFlags::default() } },
dependencies: Default::default(),
spec_dir: spec_dir.clone(),
};
// case: relative path under src_dir
let p = resolve_actual_src(&spec, &src_root).unwrap();
assert!(p.ends_with("sub"));
// case: ../llvm should resolve relative to spec_dir
let mut spec2 = spec.clone();
spec2.build.flags.source_subdir = "../llvm".into();
let p2 = resolve_actual_src(&spec2, &src_root).unwrap();
assert!(p2.ends_with("llvm"));
// case: absolute path
let mut spec3 = spec.clone();
spec3.build.flags.source_subdir = external.to_string_lossy().into_owned();
let p3 = resolve_actual_src(&spec3, &src_root).unwrap();
assert_eq!(p3, external);
}
}
+21 -1
View File
@@ -103,7 +103,8 @@ impl PackageSpec {
fn apply_flags_table(&mut self, table: &toml::map::Map<String, toml::Value>) {
for (k, v) in table {
match k.as_str() {
// match case-insensitively for common keys (allow CXX/Cc etc.)
match k.to_lowercase().as_str() {
"cflags" => {
if let Some(arr) = v.as_array() {
self.build.flags.cflags = arr
@@ -131,6 +132,11 @@ impl PackageSpec {
self.build.flags.cc = s.to_string();
}
}
"cxx" => {
if let Some(s) = v.as_str() {
self.build.flags.cxx = s.to_string();
}
}
"ar" => {
if let Some(s) = v.as_str() {
self.build.flags.ar = s.to_string();
@@ -674,6 +680,9 @@ pub struct BuildFlags {
/// C compiler
#[serde(default = "default_cc")]
pub cc: String,
/// C++ compiler
#[serde(default = "default_cxx")]
pub cxx: String,
/// Archiver
#[serde(default = "default_ar")]
pub ar: String,
@@ -749,6 +758,7 @@ impl Default for BuildFlags {
ldflags: Vec::new(),
configure: Vec::new(),
cc: default_cc(),
cxx: default_cxx(),
ar: default_ar(),
libc: String::new(),
rootfs: default_rootfs(),
@@ -829,6 +839,16 @@ fn default_carch() -> String {
std::env::consts::ARCH.to_string()
}
fn default_cxx() -> String {
// Infer a sensible C++ compiler name from default_cc()
let cc = default_cc();
if cc.contains("clang") {
"clang++".to_string()
} else {
"g++".to_string()
}
}
/// Package dependencies
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct Dependencies {