use anyhow::{Context, Result};
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
fn find_configs_in_dir(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
for name in &["config", "config.toml"] {
let p = dir.join(name);
if p.is_file() {
out.push(p);
}
}
out
}
pub fn discover_cargo_config_files(start_dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let mut seen = HashSet::new();
let global_dir = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cargo")));
if let Some(gdir) = global_dir {
for p in find_configs_in_dir(&gdir) {
let canon = p.canonicalize().unwrap_or_else(|_| p.clone());
if seen.insert(canon) {
files.push(p);
}
}
}
let mut ancestor_chain = Vec::new();
let mut curr = Some(start_dir);
while let Some(d) = curr {
ancestor_chain.push(d);
curr = d.parent();
}
ancestor_chain.reverse();
for dir in ancestor_chain {
let cargo_dir = dir.join(".cargo");
for p in find_configs_in_dir(&cargo_dir) {
let canon = p.canonicalize().unwrap_or_else(|_| p.clone());
if seen.insert(canon) {
files.push(p);
}
}
}
files
}
pub fn load_cargo_config_rustflags(
start_dir: &Path,
target_triple: Option<&str>,
) -> Result<Vec<String>> {
let host_triple = super::compiler_resolve::resolve_host_triple();
let effective_target = target_triple.unwrap_or(host_triple);
let files = discover_cargo_config_files(start_dir);
let mut all_target_flags = Vec::new();
let mut all_build_flags = Vec::new();
for config_path in files {
let content = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read cargo config at {:?}", config_path))?;
let val: toml::Value = toml::from_str(&content)
.with_context(|| format!("Failed to parse cargo config at {:?}", config_path))?;
let extracted = extract_rustflags_from_toml(&val, effective_target, &config_path)?;
if let Some(tf) = extracted.target {
all_target_flags.extend(tf);
}
if let Some(bf) = extracted.build {
all_build_flags.extend(bf);
}
}
let upper_triple = effective_target.replace('-', "_").to_uppercase();
let target_env_key = format!("CARGO_TARGET_{}_RUSTFLAGS", upper_triple);
if let Ok(flags_str) = std::env::var(&target_env_key) {
all_target_flags.extend(flags_str.split_whitespace().map(|s| s.to_string()));
}
if !all_target_flags.is_empty() {
return Ok(all_target_flags);
}
if let Ok(flags_str) = std::env::var("CARGO_BUILD_RUSTFLAGS") {
all_build_flags.extend(flags_str.split_whitespace().map(|s| s.to_string()));
}
if !all_build_flags.is_empty() {
return Ok(all_build_flags);
}
Ok(Vec::new())
}
#[derive(Default, Debug, PartialEq, Eq)]
struct ExtractedRustflags {
target: Option<Vec<String>>,
build: Option<Vec<String>>,
}
fn extract_rustflags_from_toml(
val: &toml::Value,
effective_target: &str,
config_path: &Path,
) -> Result<ExtractedRustflags> {
let mut extracted = ExtractedRustflags::default();
if let Some(targets) = val.get("target").and_then(|t| t.as_table()) {
for (key, target_val) in targets {
if key.starts_with("cfg(") || key.contains("cfg(") {
if let Some(target_table) = target_val.as_table() {
if target_table.contains_key("rustflags") {
anyhow::bail!(
"Unsupported cargo config: target.'cfg(...)' rustflags is not yet supported in {}",
config_path.display()
);
}
}
}
}
if let Some(target_val) = targets.get(effective_target) {
if let Some(rf) = target_val.get("rustflags") {
extracted.target = parse_flags_value(rf);
}
}
}
if let Some(build_tbl) = val.get("build").and_then(|b| b.as_table()) {
if let Some(rf) = build_tbl.get("rustflags") {
extracted.build = parse_flags_value(rf);
}
}
Ok(extracted)
}
fn parse_flags_value(val: &toml::Value) -> Option<Vec<String>> {
match val {
toml::Value::String(s) => Some(s.split_whitespace().map(|x| x.to_string()).collect()),
toml::Value::Array(arr) => {
let mut res = Vec::new();
for item in arr {
if let toml::Value::String(s) = item {
res.push(s.clone());
}
}
Some(res)
}
_ => None,
}
}
pub fn resolve_config_start_dir_with_opt(
base_dir: &Path,
explicit_start_dir: Option<&Path>,
) -> PathBuf {
if let Some(explicit) = explicit_start_dir {
return explicit.to_path_buf();
}
if let Ok(cwd) = std::env::current_dir() {
if cwd.starts_with(base_dir) || base_dir.starts_with(&cwd) {
return cwd;
}
}
base_dir.to_path_buf()
}
pub fn resolve_config_start_dir(base_dir: &Path) -> PathBuf {
resolve_config_start_dir_with_opt(base_dir, None)
}
pub fn load_cargo_config_build_target(start_dir: &Path) -> Result<Option<String>> {
let files = discover_cargo_config_files(start_dir);
for config_path in files.into_iter().rev() {
let content = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read cargo config at {:?}", config_path))?;
let val: toml::Value = toml::from_str(&content)
.with_context(|| format!("Failed to parse cargo config at {:?}", config_path))?;
if let Some(build_tbl) = val.get("build").and_then(|b| b.as_table()) {
if let Some(target_val) = build_tbl.get("target") {
match target_val {
toml::Value::String(s) => {
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(Some(trimmed.to_string()));
}
}
toml::Value::Array(arr) => {
if let Some(toml::Value::String(first)) = arr.first() {
let trimmed = first.trim();
if !trimmed.is_empty() {
return Ok(Some(trimmed.to_string()));
}
}
}
_ => {}
}
}
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_build_rustflags_string_and_array() {
let content_str = r#"
[build]
rustflags = "-C opt-level=3 --cfg custom_mark"
"#;
let val: toml::Value = toml::from_str(content_str).unwrap();
let extracted =
extract_rustflags_from_toml(&val, "x86_64-unknown-linux-gnu", Path::new("dummy.toml"))
.unwrap();
assert_eq!(
extracted.build.unwrap(),
vec!["-C", "opt-level=3", "--cfg", "custom_mark"]
);
let content_arr = r#"
[build]
rustflags = ["-C", "opt-level=2", "--cfg", "mark_array"]
"#;
let val2: toml::Value = toml::from_str(content_arr).unwrap();
let extracted2 =
extract_rustflags_from_toml(&val2, "x86_64-unknown-linux-gnu", Path::new("dummy.toml"))
.unwrap();
assert_eq!(
extracted2.build.unwrap(),
vec!["-C", "opt-level=2", "--cfg", "mark_array"]
);
}
#[test]
fn test_extract_target_triple_rustflags_precedence() {
let content = r#"
[build]
rustflags = ["--cfg", "build_flag"]
[target.x86_64-unknown-linux-gnu]
rustflags = ["--cfg", "linux_flag"]
"#;
let val: toml::Value = toml::from_str(content).unwrap();
let extracted =
extract_rustflags_from_toml(&val, "x86_64-unknown-linux-gnu", Path::new("dummy.toml"))
.unwrap();
assert_eq!(extracted.target.unwrap(), vec!["--cfg", "linux_flag"]);
assert_eq!(extracted.build.unwrap(), vec!["--cfg", "build_flag"]);
}
#[test]
fn test_extract_target_cfg_fails_closed() {
let content = r#"
[target.'cfg(target_os = "linux")']
rustflags = ["--cfg", "linux_cfg"]
"#;
let val: toml::Value = toml::from_str(content).unwrap();
let res =
extract_rustflags_from_toml(&val, "x86_64-unknown-linux-gnu", Path::new("custom.toml"));
assert!(res.is_err());
assert!(res
.unwrap_err()
.to_string()
.contains("target.'cfg(...)' rustflags is not yet supported"));
}
}