use std::collections::BTreeMap;
use std::env;
use anyhow::{Context, Result};
use cargo_config2::Flags;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CliConfig {
build_rustflags: Option<Flags>,
target_rustflags: BTreeMap<String, Flags>,
}
impl CliConfig {
pub fn parse(config_args: &[String]) -> Result<Self> {
let mut parsed = Self::default();
for arg in config_args {
let config: cargo_config2::de::Config = if arg.ends_with(".toml") {
cargo_config2::de::Config::load_file(arg)?
} else {
toml::from_str(arg)
.with_context(|| format!("failed to parse --config argument `{arg}`"))?
};
if let Some(flags) = &config.build.rustflags {
append_de_flags(parsed.build_rustflags.get_or_insert_default(), flags);
}
for (key, target_config) in &config.target {
if key.starts_with("cfg(") {
continue;
}
if let Some(flags) = &target_config.rustflags {
append_de_flags(
parsed.target_rustflags.entry(key.clone()).or_default(),
flags,
);
}
}
}
Ok(parsed)
}
pub fn rustflags(
&self,
cargo_config: &cargo_config2::Config,
rust_target: &str,
) -> Result<Option<Flags>> {
let resolved = cargo_config.rustflags(rust_target)?;
if env::var_os("CARGO_ENCODED_RUSTFLAGS").is_some() || env::var_os("RUSTFLAGS").is_some() {
return Ok(resolved);
}
Ok(self.overlay(rust_target, resolved, cargo_config.build.rustflags.clone()))
}
fn overlay(
&self,
rust_target: &str,
resolved: Option<Flags>,
build_flags: Option<Flags>,
) -> Option<Flags> {
let from_build_tier = resolved == build_flags;
if let Some(cli_target) = self.target_rustflags.get(rust_target) {
let mut flags = if from_build_tier {
Flags::default()
} else {
resolved.unwrap_or_default()
};
flags.flags.extend(cli_target.flags.iter().cloned());
Some(flags)
} else if let Some(cli_build) = &self.build_rustflags {
if from_build_tier {
let mut flags = resolved.unwrap_or_default();
flags.flags.extend(cli_build.flags.iter().cloned());
Some(flags)
} else {
resolved
}
} else {
resolved
}
}
}
fn append_de_flags(flags: &mut Flags, de_flags: &cargo_config2::de::Flags) {
flags
.flags
.extend(de_flags.flags.iter().map(|value| value.val.clone()));
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
const TARGET: &str = "x86_64-unknown-linux-gnu";
fn flags(s: &str) -> Flags {
Flags::from_space_separated(s)
}
#[test]
fn test_parse_target_rustflags_array() {
let config = CliConfig::parse(&[format!(
"target.{TARGET}.rustflags=['-C','target-cpu=x86-64-v4']"
)])
.unwrap();
assert_eq!(
config.target_rustflags[TARGET].flags,
vec!["-C", "target-cpu=x86-64-v4"]
);
assert!(config.build_rustflags.is_none());
}
#[test]
fn test_parse_target_rustflags_string() {
let config = CliConfig::parse(&[format!(
"target.{TARGET}.rustflags='-C target-cpu=x86-64-v4'"
)])
.unwrap();
assert_eq!(
config.target_rustflags[TARGET].flags,
vec!["-C", "target-cpu=x86-64-v4"]
);
}
#[test]
fn test_parse_build_rustflags() {
let config =
CliConfig::parse(&["build.rustflags=['-Ctarget-cpu=neoverse-n1']".to_string()])
.unwrap();
assert_eq!(
config.build_rustflags.unwrap().flags,
vec!["-Ctarget-cpu=neoverse-n1"]
);
}
#[test]
fn test_parse_multiple_args_join_left_to_right() {
let config = CliConfig::parse(&[
format!("target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v2']"),
format!("target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v4']"),
])
.unwrap();
assert_eq!(
config.target_rustflags[TARGET].flags,
vec!["-Ctarget-cpu=x86-64-v2", "-Ctarget-cpu=x86-64-v4"]
);
}
#[test]
fn test_parse_config_file() {
let mut file = tempfile::Builder::new().suffix(".toml").tempfile().unwrap();
writeln!(
file,
"[target.{TARGET}]\nrustflags = ['-C', 'target-cpu=x86-64-v3']"
)
.unwrap();
let config = CliConfig::parse(&[file.path().to_str().unwrap().to_string()]).unwrap();
assert_eq!(
config.target_rustflags[TARGET].flags,
vec!["-C", "target-cpu=x86-64-v3"]
);
}
#[test]
fn test_parse_ignores_unrelated_and_cfg_keys() {
let config = CliConfig::parse(&[
"net.git-fetch-with-cli=true".to_string(),
"profile.release.lto=true".to_string(),
"target.'cfg(target_arch = \"x86_64\")'.rustflags=['-Ctarget-cpu=x86-64-v4']"
.to_string(),
])
.unwrap();
assert_eq!(config, CliConfig::default());
}
#[test]
fn test_overlay_cli_target_replaces_build_tier() {
let config = CliConfig::parse(&[format!(
"target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v4']"
)])
.unwrap();
let result = config.overlay(
TARGET,
Some(flags("-Ctarget-cpu=x86-64-v2")),
Some(flags("-Ctarget-cpu=x86-64-v2")),
);
assert_eq!(result.unwrap().flags, vec!["-Ctarget-cpu=x86-64-v4"]);
}
#[test]
fn test_overlay_cli_target_joins_file_target_tier() {
let config = CliConfig::parse(&[format!(
"target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v4']"
)])
.unwrap();
let result = config.overlay(TARGET, Some(flags("-Ctarget-cpu=x86-64-v2")), None);
assert_eq!(
result.unwrap().flags,
vec!["-Ctarget-cpu=x86-64-v2", "-Ctarget-cpu=x86-64-v4"]
);
}
#[test]
fn test_overlay_cli_target_other_triple_is_ignored() {
let config = CliConfig::parse(&[
"target.aarch64-unknown-linux-gnu.rustflags=['-Ctarget-cpu=neoverse-n1']".to_string(),
])
.unwrap();
let result = config.overlay(TARGET, None, None);
assert_eq!(result, None);
}
#[test]
fn test_overlay_cli_build_joins_build_tier() {
let config =
CliConfig::parse(&["build.rustflags=['-Ctarget-cpu=x86-64-v4']".to_string()]).unwrap();
let result = config.overlay(
TARGET,
Some(flags("-Ctarget-cpu=x86-64-v2")),
Some(flags("-Ctarget-cpu=x86-64-v2")),
);
assert_eq!(
result.unwrap().flags,
vec!["-Ctarget-cpu=x86-64-v2", "-Ctarget-cpu=x86-64-v4"]
);
}
#[test]
fn test_overlay_cli_build_ignored_when_target_tier_present() {
let config =
CliConfig::parse(&["build.rustflags=['-Ctarget-cpu=x86-64-v4']".to_string()]).unwrap();
let result = config.overlay(TARGET, Some(flags("-Ctarget-cpu=x86-64-v2")), None);
assert_eq!(result.unwrap().flags, vec!["-Ctarget-cpu=x86-64-v2"]);
}
#[test]
fn test_overlay_no_cli_flags_keeps_resolved() {
let config = CliConfig::default();
let result = config.overlay(TARGET, Some(flags("-Ctarget-cpu=x86-64-v2")), None);
assert_eq!(result.unwrap().flags, vec!["-Ctarget-cpu=x86-64-v2"]);
}
}