use std::process::Command;
use std::{env, fs};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_gamma_engine::cfg::{CfgSet, Verdict};
use toml::{Table, Value};
use crate::HashMap;
const PROFILE_DEPTH: usize = 16;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Build {
pub target: Option<String>,
pub cfgs: Vec<String>,
pub codegen: Vec<String>,
pub debug_assertions: Option<bool>,
pub undecided: Vec<String>,
pub several_targets: bool,
}
impl Build {
#[must_use]
pub fn resolve(root: &Utf8Path, profile: Option<&str>, extra: &[String]) -> Self {
Self::resolve_in(root, profile, extra, &Environment::ambient())
}
fn resolve_in(root: &Utf8Path, profile: Option<&str>, extra: &[String], environment: &Environment) -> Self {
let config = CargoConfig::load(root, environment);
let (target, several_targets) = target(extra, environment, &config);
let (flags, undecided) = rustflags(environment, &config, target.as_deref());
let profile = named_profile(extra).or_else(|| profile.map(ToOwned::to_owned));
Self {
debug_assertions: assertions(&flags).or_else(|| profile_assertions(root, &config, profile.as_deref())),
cfgs: valued(&flags, "--cfg"),
codegen: codegen(&flags),
target,
undecided,
several_targets,
}
}
#[cfg_attr(
not(any(test, feature = "internals")),
expect(
dead_code,
reason = "the list exists to be compared against, not to be read: its only consumer is \
the invariant test that fails when the resolution learns to read an input \
nobody added here, so it is live only where that test can see it"
)
)]
pub const INPUTS: &'static [&'static str] = &[
"CARGO_BUILD_TARGET",
"CARGO_ENCODED_RUSTFLAGS",
"RUSTFLAGS",
"CARGO_BUILD_RUSTFLAGS",
"CARGO_TARGET_<triple>_RUSTFLAGS",
"CARGO_HOME",
"cargo config files",
"build.target",
"build.rustflags",
"target.*.rustflags",
"profile.*",
];
#[must_use]
pub fn requested_targets(extra: &[String], configured: Option<&str>) -> Vec<String> {
let mut named = valued(extra, "--target");
if named.is_empty() {
named = configured.map(ToOwned::to_owned).into_iter().collect();
}
named.sort();
named.dedup();
named
}
#[must_use]
pub fn settings(root: &Utf8Path) -> Vec<String> {
Self::settings_in(root, &Environment::ambient())
}
fn settings_in(root: &Utf8Path, environment: &Environment) -> Vec<String> {
let config = CargoConfig::load(root, environment);
let mut parts = Vec::new();
parts.extend(config.sources.iter().cloned());
if let Some(target) = config.string(&["build", "target"]) {
parts.push(format!("build.target={target}"));
}
for flag in config.strings(&["build", "rustflags"]) {
parts.push(format!("build.rustflags={flag}"));
}
for table in config.keys(&["target"]) {
for flag in config.strings(&["target", &table, "rustflags"]) {
parts.push(format!("target.{table}.rustflags={flag}"));
}
}
let manifest = read_table(&root.join("Cargo.toml"));
for table in config.tables.iter().chain(manifest.as_ref()) {
if let Some(profiles) = table.get("profile") {
parts.push(format!("profile={profiles}"));
}
}
parts
}
pub(super) fn probe_args(&self) -> Vec<String> {
let mut args = vec!["--print".to_owned(), "cfg".to_owned()];
if let Some(target) = self.target.as_ref() {
args.push("--target".to_owned());
args.push(target.clone());
}
if let Some(on) = self.debug_assertions {
args.push("-C".to_owned());
args.push(format!("debug-assertions={}", if on { "on" } else { "off" }));
}
for option in &self.codegen {
args.push("-C".to_owned());
args.push(option.clone());
}
for predicate in &self.cfgs {
args.push("--cfg".to_owned());
args.push(predicate.clone());
}
args
}
}
#[derive(Clone, Debug, Default)]
struct Environment {
target: Option<String>,
encoded_rustflags: Option<String>,
rustflags: Option<String>,
build_rustflags: Option<String>,
target_rustflags: HashMap<String, String>,
cargo_home: Option<Utf8PathBuf>,
}
impl Environment {
fn target_flags(&self, triple: &str) -> Option<&str> {
self.target_rustflags.get(&variable_component(triple)).map(String::as_str)
}
fn ambient() -> Self {
Self::read(
|name| env::var(name),
env::vars_os().filter_map(|(name, _value)| name.into_string().ok()),
)
}
fn read(mut get: impl FnMut(&str) -> Result<String, env::VarError>, names: impl IntoIterator<Item = String>) -> Self {
let home = get("CARGO_HOME").ok().map(Utf8PathBuf::from).or_else(|| {
get("HOME")
.or_else(|_absent| get("USERPROFILE"))
.ok()
.map(|home| Utf8PathBuf::from(home).join(".cargo"))
});
let target = get("CARGO_BUILD_TARGET").ok();
let encoded_rustflags = get("CARGO_ENCODED_RUSTFLAGS").ok();
let rustflags = get("RUSTFLAGS").ok();
let build_rustflags = get("CARGO_BUILD_RUSTFLAGS").ok();
let target_rustflags = names
.into_iter()
.filter_map(|name| {
let component = name.strip_prefix("CARGO_TARGET_")?.strip_suffix("_RUSTFLAGS")?.to_owned();
Some((component, get(&name).ok()?))
})
.collect();
Self {
target,
encoded_rustflags,
rustflags,
build_rustflags,
target_rustflags,
cargo_home: home,
}
}
}
#[derive(Debug, Default)]
struct CargoConfig {
tables: Vec<Table>,
sources: Vec<String>,
}
impl CargoConfig {
fn load(dir: &Utf8Path, environment: &Environment) -> Self {
let mut config = Self::default();
let mut at = Some(dir);
while let Some(directory) = at {
for name in ["config.toml", "config"] {
let path = directory.join(".cargo").join(name);
if config.include(&path) {
break;
}
}
at = directory.parent();
}
if let Some(home) = environment.cargo_home.as_ref()
&& !cargo_home_was_walked(dir, home)
{
for name in ["config.toml", "config"] {
let path = home.join(name);
if config.include(&path) {
break;
}
}
}
config
}
fn include(&mut self, path: &Utf8Path) -> bool {
let bytes = match fs::read(path.as_std_path()) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
Err(error) => {
self.sources.push(format!("cargo-config:{path}:unreadable:{:?}", error.kind()));
return true;
}
};
self.sources.push(format!("cargo-config:{path}:{}", blake3::hash(&bytes).to_hex()));
if let Ok(text) = core::str::from_utf8(&bytes)
&& let Ok(table) = toml::from_str(text)
{
self.tables.push(table);
}
true
}
fn string(&self, path: &[&str]) -> Option<&str> {
self.tables.iter().find_map(|table| lookup(table, path)?.as_str())
}
fn strings(&self, path: &[&str]) -> Vec<String> {
self.tables
.iter()
.filter_map(|table| lookup(table, path))
.flat_map(|value| match value {
Value::String(one) => vec![one.clone()],
Value::Array(many) => many.iter().filter_map(|entry| entry.as_str().map(ToOwned::to_owned)).collect(),
_other => Vec::new(),
})
.collect()
}
fn keys(&self, path: &[&str]) -> Vec<String> {
let mut names: Vec<String> = self
.tables
.iter()
.filter_map(|table| lookup(table, path)?.as_table())
.flat_map(|table| table.keys().cloned())
.collect();
names.sort();
names.dedup();
names
}
}
fn cargo_home_was_walked(dir: &Utf8Path, home: &Utf8Path) -> bool {
home.file_name() == Some(".cargo") && home.parent().is_some_and(|parent| dir.starts_with(parent))
}
fn read_table(path: &Utf8Path) -> Option<Table> {
let text = fs::read_to_string(path.as_std_path()).ok()?;
toml::from_str(&text).ok()
}
fn lookup<'table>(table: &'table Table, path: &[&str]) -> Option<&'table Value> {
let (last, leading) = path.split_last()?;
let mut at = table;
for name in leading {
at = at.get(*name)?.as_table()?;
}
at.get(*last)
}
fn target(extra: &[String], environment: &Environment, config: &CargoConfig) -> (Option<String>, bool) {
let mut named = Build::requested_targets(extra, environment.target.as_deref());
if named.is_empty() {
named = config.strings(&["build", "target"]);
}
named.sort();
named.dedup();
match named.len() {
0 => (None, false),
1 => (named.pop(), false),
_several => (None, true),
}
}
fn rustflags(environment: &Environment, config: &CargoConfig, target: Option<&str>) -> (Vec<String>, Vec<String>) {
let tables = target_tables(config, target, environment);
let joined: Vec<String> = tables
.iter()
.filter(|(applies, _flags)| *applies == Verdict::Yes)
.flat_map(|(_applies, flags)| flags.iter().cloned())
.collect();
let ambient = environment
.encoded_rustflags
.as_ref()
.map(|encoded| {
encoded
.split('\u{1f}')
.filter(|flag| !flag.is_empty())
.map(ToOwned::to_owned)
.collect()
})
.or_else(|| environment.rustflags.as_ref().map(|flags| split(flags)));
let reading_tables = ambient.is_none() && !joined.is_empty();
let chosen = ambient
.or_else(|| reading_tables.then(|| joined.clone()))
.or_else(|| environment.build_rustflags.as_ref().map(|flags| split(flags)))
.or_else(|| {
let flags = config.strings(&["build", "rustflags"]);
(!flags.is_empty()).then_some(flags)
})
.unwrap_or_default();
let mut undecided: Vec<String> = Vec::new();
for (applies, flags) in &tables {
if reading_tables && *applies == Verdict::Yes {
continue;
}
undecided.extend(decided_names(flags));
}
undecided.sort();
undecided.dedup();
(chosen, undecided)
}
fn target_tables(config: &CargoConfig, target: Option<&str>, environment: &Environment) -> Vec<(Verdict, Vec<String>)> {
let names = config.keys(&["target"]);
let asked_about = !names.is_empty() || !environment.target_rustflags.is_empty();
let triple = || target.or_else(|| host_triple(config, asked_about));
let mut cfgs: Option<Option<CfgSet>> = None;
let mut tables: Vec<(Verdict, Vec<String>)> = names
.iter()
.map(|name| {
let applies = predicate_of(name).map_or_else(
|| triple().map_or(Verdict::Unknown, |triple| Verdict::from(triple == name)),
|predicate| {
cfgs.get_or_insert_with(|| probe_cfgs(target))
.as_ref()
.map_or(Verdict::Unknown, |cfgs| cfgs.decide_str(predicate))
},
);
let flags = predicate_of(name)
.is_none()
.then(|| environment.target_flags(name).map(split))
.flatten()
.unwrap_or_else(|| config.strings(&["target", name, "rustflags"]));
(applies, flags)
})
.collect();
if let Some(triple) = triple()
&& !names.iter().any(|name| name == triple)
&& let Some(flags) = environment.target_flags(triple)
{
tables.push((Verdict::Yes, split(flags)));
}
tables
}
fn variable_component(triple: &str) -> String {
triple.to_uppercase().replace(['-', '.'], "_")
}
fn predicate_of(table: &str) -> Option<&str> {
table.strip_prefix("cfg(")?.strip_suffix(')')
}
fn probe_cfgs(target: Option<&str>) -> Option<CfgSet> {
let program = env::var("RUSTC").unwrap_or_else(|_absent| "rustc".to_owned());
let mut command = Command::new(program);
let _builder = command.arg("--print").arg("cfg");
if let Some(triple) = target {
let _builder = command.arg("--target").arg(triple);
}
let output = command.output().ok().filter(|output| output.status.success())?;
Some(CfgSet::parse(&String::from_utf8_lossy(&output.stdout)))
}
fn decided_names(flags: &[String]) -> Vec<String> {
let mut names: Vec<String> = valued(flags, "--cfg").into_iter().map(|predicate| name_of(&predicate)).collect();
for option in options(flags) {
if option
.strip_prefix("debug-assertions")
.is_some_and(|rest| rest.is_empty() || rest.starts_with('='))
{
names.push("debug_assertions".to_owned());
} else if option.starts_with("panic=") {
names.push("panic".to_owned());
} else if option.starts_with("target-feature=") {
names.push("target_feature".to_owned());
}
}
names
}
fn host_triple(config: &CargoConfig, asked_about: bool) -> Option<&'static str> {
use std::sync::OnceLock;
static HOST: OnceLock<Option<String>> = OnceLock::new();
if !asked_about && config.keys(&["target"]).is_empty() {
return None;
}
HOST.get_or_init(|| {
let program = env::var("RUSTC").unwrap_or_else(|_absent| "rustc".to_owned());
let output = Command::new(program).arg("-vV").output().ok()?;
let printed = String::from_utf8_lossy(&output.stdout).into_owned();
parse_host(&printed)
})
.as_deref()
}
fn parse_host(printed: &str) -> Option<String> {
printed
.lines()
.find_map(|line| line.strip_prefix("host: "))
.map(|triple| triple.trim().to_owned())
}
fn split(flags: &str) -> Vec<String> {
flags.split_whitespace().map(ToOwned::to_owned).collect()
}
fn valued(args: &[String], flag: &str) -> Vec<String> {
let mut found = Vec::new();
let mut expecting = false;
for argument in args {
if expecting {
found.push(argument.clone());
expecting = false;
continue;
}
if argument == flag {
expecting = true;
} else if let Some(value) = argument.strip_prefix(flag).and_then(|rest| rest.strip_prefix('=')) {
found.push(value.to_owned());
}
}
found
}
fn name_of(predicate: &str) -> String {
predicate.split_once('=').map_or(predicate, |(name, _value)| name).trim().to_owned()
}
fn codegen(flags: &[String]) -> Vec<String> {
options(flags)
.filter(|option| option.starts_with("target-feature=") || option.starts_with("panic="))
.map(ToOwned::to_owned)
.collect()
}
fn assertions(flags: &[String]) -> Option<bool> {
options(flags)
.filter_map(|option| option.strip_prefix("debug-assertions"))
.filter_map(|rest| match rest {
"" => Some(Some(true)),
_other => rest.strip_prefix('=').map(|value| match value.trim() {
"y" | "yes" | "on" | "true" => Some(true),
"n" | "no" | "off" | "false" => Some(false),
_unrecognised => None,
}),
})
.last()
.flatten()
}
fn options(flags: &[String]) -> impl Iterator<Item = &str> {
let mut expecting = false;
flags.iter().filter_map(move |flag| {
if expecting {
expecting = false;
return Some(flag.as_str());
}
if flag == "-C" || flag == "--codegen" {
expecting = true;
return None;
}
flag.strip_prefix("-C").or_else(|| flag.strip_prefix("--codegen="))
})
}
fn named_profile(extra: &[String]) -> Option<String> {
if extra.iter().any(|argument| argument == "--release" || argument == "-r") {
return Some("release".to_owned());
}
valued(extra, "--profile").pop()
}
fn profile_assertions(root: &Utf8Path, config: &CargoConfig, profile: Option<&str>) -> Option<bool> {
let manifest = read_table(&root.join("Cargo.toml"));
let declared = |name: &str, key: &str| {
config.string(&["profile", name, key]).map(ToOwned::to_owned).or_else(|| {
manifest
.as_ref()
.and_then(|table| lookup(table, &["profile", name, key]))?
.as_str()
.map(ToOwned::to_owned)
})
};
let switched = |name: &str| {
["debug-assertions", "debug_assertions"]
.into_iter()
.find_map(|key| lookup_bool(config, manifest.as_ref(), name, key))
};
let mut current = profile.unwrap_or("test").to_owned();
for _hop in 0..PROFILE_DEPTH {
if let Some(on) = switched(¤t) {
return Some(on);
}
match current.as_str() {
"dev" => return Some(true),
"release" => return Some(false),
_custom => {}
}
let parent = declared(¤t, "inherits").or_else(|| match current.as_str() {
"test" => Some("dev".to_owned()),
"bench" => Some("release".to_owned()),
_custom => None,
})?;
current = parent;
}
None
}
fn lookup_bool(config: &CargoConfig, manifest: Option<&Table>, profile: &str, key: &str) -> Option<bool> {
let path = ["profile", profile, key];
config
.tables
.iter()
.find_map(|table| lookup(table, &path)?.as_bool())
.or_else(|| lookup(manifest?, &path)?.as_bool())
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use tempfile::TempDir;
use super::*;
fn tree(files: &[(&str, &str)]) -> (TempDir, Utf8PathBuf) {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
for (relative, contents) in files {
let path = root.join(relative);
fs::create_dir_all(path.parent().expect("every fixture path has a parent").as_std_path()).expect("directories");
fs::write(path.as_std_path(), contents).expect("the fixture file is written");
}
(directory, root)
}
fn empty(root: &Utf8Path) -> Environment {
Environment {
cargo_home: Some(root.join("nowhere")),
..Environment::default()
}
}
#[test]
fn environment_names_and_home_fallbacks_are_read_exactly() {
let values = std::collections::HashMap::from([
("HOME", "home"),
("USERPROFILE", "profile"),
("CARGO_BUILD_TARGET", "target"),
("CARGO_ENCODED_RUSTFLAGS", "encoded"),
("RUSTFLAGS", "plain"),
("CARGO_BUILD_RUSTFLAGS", "build"),
]);
let mut asked = Vec::new();
let environment = Environment::read(
|name| {
asked.push(name.to_owned());
values.get(name).map(|value| (*value).to_owned()).ok_or(env::VarError::NotPresent)
},
[],
);
assert_eq!(
asked,
[
"CARGO_HOME",
"HOME",
"CARGO_BUILD_TARGET",
"CARGO_ENCODED_RUSTFLAGS",
"RUSTFLAGS",
"CARGO_BUILD_RUSTFLAGS",
]
);
assert_eq!(environment.cargo_home.as_deref(), Some(Utf8Path::new("home/.cargo")));
assert_eq!(environment.target.as_deref(), Some("target"));
assert_eq!(environment.encoded_rustflags.as_deref(), Some("encoded"));
assert_eq!(environment.rustflags.as_deref(), Some("plain"));
assert_eq!(environment.build_rustflags.as_deref(), Some("build"));
let profile = Environment::read(
|name| {
(name == "USERPROFILE")
.then(|| "profile".to_owned())
.ok_or(env::VarError::NotPresent)
},
[],
);
assert_eq!(profile.cargo_home.as_deref(), Some(Utf8Path::new("profile/.cargo")));
}
fn resolve(root: &Utf8Path, profile: Option<&str>, extra: &[&str]) -> Build {
let extra: Vec<String> = extra.iter().map(|argument| (*argument).to_owned()).collect();
Build::resolve_in(root, profile, &extra, &empty(root))
}
#[test]
fn the_settings_name_every_file_borne_input_the_resolution_reads() {
let (_directory, root) = tree(&[
(
".cargo/config.toml",
"[build]\ntarget = \"wasm32-unknown-unknown\"\nrustflags = [\"--cfg\", \"loom\"]\n\n[target.'cfg(unix)']\nrustflags = [\"-Cdebug-assertions=on\"]\n\n[profile.dev]\ndebug-assertions = false\n",
),
("Cargo.toml", "[profile.mutants]\ninherits = \"release\"\ndebug-assertions = true\n"),
]);
let settings = Build::settings_in(&root, &empty(&root));
assert!(settings.contains(&"build.target=wasm32-unknown-unknown".to_owned()), "{settings:?}");
assert!(settings.contains(&"build.rustflags=--cfg".to_owned()), "{settings:?}");
assert!(settings.contains(&"build.rustflags=loom".to_owned()), "{settings:?}");
assert!(
settings.contains(&"target.cfg(unix).rustflags=-Cdebug-assertions=on".to_owned()),
"{settings:?}"
);
assert_eq!(
settings.iter().filter(|part| part.starts_with("profile=")).count(),
2,
"both the configuration's profiles and the manifest's are read: {settings:?}"
);
}
#[test]
fn an_unconfigured_workspace_holds_no_settings() {
let (_directory, root) = tree(&[]);
assert!(Build::settings_in(&root, &empty(&root)).is_empty());
}
#[test]
fn an_unprojected_cargo_setting_still_moves_the_record_input() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[env]\nSUBJECT_MODE = \"one\"\n")]);
let before = Build::settings_in(&root, &empty(&root));
fs::write(root.join(".cargo/config.toml").as_std_path(), "[env]\nSUBJECT_MODE = \"two\"\n").expect("configuration is writable");
assert_ne!(before, Build::settings_in(&root, &empty(&root)));
}
#[test]
fn a_user_cargo_setting_still_moves_the_record_input() {
let (_workspace, root) = tree(&[]);
let (_home_directory, home) = tree(&[("config.toml", "[env]\nSUBJECT_MODE = \"one\"\n")]);
let environment = Environment {
cargo_home: Some(home.clone()),
..Environment::default()
};
let before = Build::settings_in(&root, &environment);
fs::write(home.join("config.toml").as_std_path(), "[env]\nSUBJECT_MODE = \"two\"\n").expect("configuration is writable");
assert_ne!(before, Build::settings_in(&root, &environment));
}
#[test]
fn the_requested_targets_are_the_ones_the_command_line_and_the_environment_ask_for() {
let extra = ["--target=wasm32-unknown-unknown".to_owned()];
let both = [
"--target=wasm32-unknown-unknown".to_owned(),
"--target".to_owned(),
"aarch64-apple-darwin".to_owned(),
];
assert_eq!(Build::requested_targets(&[], None), Vec::<String>::new());
assert_eq!(
Build::requested_targets(&[], Some("x86_64-unknown-linux-musl")),
["x86_64-unknown-linux-musl"]
);
assert_eq!(
Build::requested_targets(&extra, Some("x86_64-unknown-linux-musl")),
["wasm32-unknown-unknown"],
"a passthrough target outranks the variable, as it does for cargo"
);
assert_eq!(
Build::requested_targets(&both, None),
["aarch64-apple-darwin", "wasm32-unknown-unknown"]
);
}
#[test]
fn a_passthrough_target_is_read_however_it_is_written() {
let (_directory, root) = tree(&[]);
assert_eq!(
resolve(&root, None, &["--target", "wasm32-unknown-unknown"]).target.as_deref(),
Some("wasm32-unknown-unknown")
);
assert_eq!(
resolve(&root, None, &["--target=wasm32-unknown-unknown"]).target.as_deref(),
Some("wasm32-unknown-unknown")
);
}
#[test]
fn a_target_directory_is_not_a_target() {
let (_directory, root) = tree(&[]);
let build = resolve(&root, None, &["--target-dir=elsewhere"]);
assert_eq!(build.target, None);
assert!(!build.several_targets);
}
#[test]
fn public_resolution_reads_the_workspace_configuration() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\ntarget=\"configured-publicly\"\n")]);
assert_eq!(Build::resolve(&root, None, &[]).target.as_deref(), Some("configured-publicly"));
}
#[test]
fn the_cargo_configuration_supplies_the_target() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\ntarget = \"aarch64-apple-darwin\"\n")]);
assert_eq!(resolve(&root, None, &[]).target.as_deref(), Some("aarch64-apple-darwin"));
}
#[test]
fn a_passthrough_target_outranks_the_configured_one() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\ntarget = \"aarch64-apple-darwin\"\n")]);
assert_eq!(
resolve(&root, None, &["--target", "wasm32-unknown-unknown"]).target.as_deref(),
Some("wasm32-unknown-unknown")
);
}
#[test]
fn a_configuration_file_above_the_workspace_is_read() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\ntarget = \"aarch64-apple-darwin\"\n")]);
let inner = root.join("member");
fs::create_dir_all(inner.as_std_path()).expect("a member directory");
assert_eq!(
Build::resolve_in(&inner, None, &[], &empty(&root)).target.as_deref(),
Some("aarch64-apple-darwin")
);
}
#[test]
fn configuration_search_obeys_cargo_precedence_and_reads_every_level() {
let (_directory, root) = tree(&[
(
".cargo/config.toml",
"[build]\ntarget = \"near\"\nrustflags = [\"--cfg\", \"near\"]\n",
),
(
".cargo/config",
"[build]\ntarget = \"legacy-near\"\nrustflags = [\"--cfg\", \"legacy-near\"]\n",
),
("member/.cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"member\"]\n"),
("member/.cargo/config", "[build]\nrustflags = [\"--cfg\", \"legacy-member\"]\n"),
]);
let config = CargoConfig::load(&root.join("member"), &empty(&root));
assert_eq!(config.string(&["build", "target"]), Some("near"));
assert_eq!(config.strings(&["build", "rustflags"]), vec!["--cfg", "member", "--cfg", "near"]);
}
#[test]
fn legacy_configuration_names_are_used_when_toml_is_absent() {
let (_directory, root) = tree(&[
("workspace/.cargo/config", "[build]\ntarget=\"legacy-workspace\"\n"),
("home/config", "[build]\nrustflags=[\"legacy-home\"]\n"),
]);
let workspace = root.join("workspace");
let environment = Environment {
cargo_home: Some(root.join("home")),
..Environment::default()
};
let config = CargoConfig::load(&workspace, &environment);
assert_eq!(config.string(&["build", "target"]), Some("legacy-workspace"));
assert_eq!(config.strings(&["build", "rustflags"]), vec!["legacy-home"]);
}
#[test]
fn user_configuration_is_read_only_when_it_is_outside_the_workspace() {
let (_directory, root) = tree(&[
("workspace/.cargo/config.toml", "[build]\nrustflags = [\"workspace\"]\n"),
("home/config.toml", "[build]\nrustflags = [\"home\"]\n"),
("home/config", "[build]\nrustflags = [\"legacy-home\"]\n"),
]);
let workspace = root.join("workspace");
let outside = Environment {
cargo_home: Some(root.join("home")),
..Environment::default()
};
let inside = Environment {
cargo_home: Some(workspace.join(".cargo")),
..Environment::default()
};
assert_eq!(
CargoConfig::load(&workspace, &outside).strings(&["build", "rustflags"]),
vec!["workspace", "home"]
);
assert_eq!(
CargoConfig::load(&workspace, &inside).strings(&["build", "rustflags"]),
vec!["workspace"]
);
}
#[test]
fn descendant_non_dot_cargo_home_reaches_resolution_and_record_settings_once() {
let (_directory, root) = tree(&[("cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"from_descendant_home\"]\n")]);
let environment = Environment {
cargo_home: Some(root.join("cargo")),
..Environment::default()
};
let build = Build::resolve_in(&root, None, &[], &environment);
let settings = Build::settings_in(&root, &environment);
assert_eq!(build.cfgs, ["from_descendant_home"]);
assert_eq!(
settings.iter().filter(|part| part.starts_with("cargo-config:")).count(),
1,
"the configuration digest must contain the descendant home exactly once: {settings:?}"
);
assert!(
settings.contains(&"build.rustflags=from_descendant_home".to_owned()),
"{settings:?}"
);
}
#[test]
fn a_deeply_nested_cargo_cfg_key_is_left_undecided_before_parsing() {
let predicate = format!("{}unix{}", "all(".repeat(100), ")".repeat(100));
let config = format!("[target.'cfg({predicate})']\nrustflags = [\"--cfg\", \"deep_guard\"]\n");
let (_directory, root) = tree(&[(".cargo/config.toml", &config)]);
let build = resolve(&root, None, &[]);
assert_eq!(build.cfgs, Vec::<String>::new());
assert_eq!(build.undecided, ["deep_guard"]);
}
#[test]
fn table_keys_are_sorted_and_deduplicated() {
let (_directory, root) = tree(&[
(".cargo/config.toml", "[target.z]\nrustflags=[]\n[target.a]\nrustflags=[]\n"),
("member/.cargo/config.toml", "[target.z]\nrustflags=[]\n[target.m]\nrustflags=[]\n"),
]);
let config = CargoConfig::load(&root.join("member"), &empty(&root));
assert_eq!(config.keys(&["target"]), vec!["a", "m", "z"]);
assert!(config.keys(&["missing"]).is_empty());
}
#[test]
fn several_targets_are_reported_rather_than_picked_between() {
let (_directory, root) = tree(&[]);
let build = resolve(
&root,
None,
&["--target", "wasm32-unknown-unknown", "--target", "aarch64-apple-darwin"],
);
assert!(build.several_targets);
assert_eq!(build.target, None);
let repeated = resolve(
&root,
None,
&["--target", "wasm32-unknown-unknown", "--target", "wasm32-unknown-unknown"],
);
assert!(!repeated.several_targets);
assert_eq!(repeated.target.as_deref(), Some("wasm32-unknown-unknown"));
}
#[test]
fn target_sources_fall_back_in_order_and_are_normalised() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\ntarget = [\"z\", \"a\", \"z\"]\n")]);
let config = CargoConfig::load(&root, &empty(&root));
let environment = Environment {
target: Some("environment".to_owned()),
..empty(&root)
};
assert_eq!(target(&[], &environment, &config), (Some("environment".to_owned()), false));
assert_eq!(target(&[], &empty(&root), &config), (None, true));
}
#[test]
fn configured_rustflags_carry_their_custom_predicates() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[build]\nrustflags = [\"--cfg\", \"loom\", \"-C\", \"target-feature=+avx2\", \"-C\", \"panic=abort\", \"-C\", \"opt-level=3\"]\n",
)]);
let build = resolve(&root, None, &[]);
assert_eq!(build.cfgs, vec!["loom".to_owned()]);
assert_eq!(build.codegen, vec!["target-feature=+avx2".to_owned(), "panic=abort".to_owned()]);
}
#[test]
fn a_target_table_supplies_the_flags_for_its_own_triple() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[build]\nrustflags = [\"--cfg\", \"everywhere\"]\n\n[target.wasm32-unknown-unknown]\nrustflags = [\"--cfg\", \"woven\"]\n",
)]);
let build = resolve(&root, None, &["--target", "wasm32-unknown-unknown"]);
assert_eq!(build.cfgs, vec!["woven".to_owned()]);
}
#[test]
fn the_variable_spelling_of_a_target_table_is_read_and_beats_the_file() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[build]\nrustflags = [\"--cfg\", \"everywhere\"]\n\n[target.wasm32-unknown-unknown]\nrustflags = [\"--cfg\", \"filed\"]\n",
)]);
let environment = Environment {
target_rustflags: core::iter::once(("WASM32_UNKNOWN_UNKNOWN".to_owned(), "--cfg live".to_owned())).collect(),
..empty(&root)
};
let extra = ["--target".to_owned(), "wasm32-unknown-unknown".to_owned()];
let build = Build::resolve_in(&root, None, &extra, &environment);
assert_eq!(build.cfgs, vec!["live".to_owned()]);
}
#[test]
fn a_target_variable_applies_with_no_table_to_hang_it_on() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"everywhere\"]\n")]);
let environment = Environment {
target_rustflags: core::iter::once(("WASM32_UNKNOWN_UNKNOWN".to_owned(), "--cfg live".to_owned())).collect(),
..empty(&root)
};
let extra = ["--target".to_owned(), "wasm32-unknown-unknown".to_owned()];
assert_eq!(Build::resolve_in(&root, None, &extra, &environment).cfgs, vec!["live".to_owned()]);
let elsewhere = Environment {
target_rustflags: core::iter::once(("AARCH64_APPLE_DARWIN".to_owned(), "--cfg live".to_owned())).collect(),
..empty(&root)
};
assert_eq!(
Build::resolve_in(&root, None, &extra, &elsewhere).cfgs,
vec!["everywhere".to_owned()]
);
}
#[test]
fn a_triple_is_spelled_into_a_variable_name_the_way_cargo_spells_it() {
assert_eq!(variable_component("x86_64-unknown-linux-gnu"), "X86_64_UNKNOWN_LINUX_GNU");
assert_eq!(variable_component("thumbv8m.main-none-eabi"), "THUMBV8M_MAIN_NONE_EABI");
}
#[test]
fn the_target_specific_variables_are_read_by_enumeration() {
let environment = Environment::read(
|name| match name {
"CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS" => Ok("--cfg live".to_owned()),
_other => Err(env::VarError::NotPresent),
},
[
"CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS".to_owned(),
"CARGO_TARGET_DIR".to_owned(),
"RUSTFLAGS".to_owned(),
],
);
assert_eq!(environment.target_flags("wasm32-unknown-unknown"), Some("--cfg live"));
assert_eq!(environment.target_flags("x86_64-unknown-linux-gnu"), None);
assert_eq!(environment.target_rustflags.len(), 1, "a variable that is not one was read");
}
#[test]
fn a_predicate_gated_table_leaves_its_names_unanswerable() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[build]\nrustflags = [\"--cfg\", \"everywhere\"]\n\n[target.'cfg(target_os = \"redox\")']\nrustflags = [\"--cfg\", \"sometimes\"]\n",
)]);
let build = resolve(&root, None, &[]);
assert_eq!(build.cfgs, vec!["everywhere".to_owned()]);
assert_eq!(build.undecided, vec!["sometimes".to_owned()]);
}
#[test]
fn a_valued_predicate_in_a_gated_table_leaves_its_bare_name_unanswerable() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[target.'cfg(target_os = \"redox\")']\nrustflags = [\"--cfg\", \"flavor=\\\"strawberry\\\"\"]\n",
)]);
let build = resolve(&root, None, &[]);
assert_eq!(build.undecided, vec!["flavor".to_owned()]);
}
#[test]
fn a_matching_predicate_table_is_joined_with_the_triples_own() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[target.wasm32-unknown-unknown]\nrustflags = [\"--cfg\", \"triple\"]\n\n\
[target.'cfg(target_family = \"wasm\")']\nrustflags = [\"--cfg\", \"family\"]\n\n\
[target.'cfg(target_os = \"linux\")']\nrustflags = [\"--cfg\", \"elsewhere\"]\n",
)]);
let build = resolve(&root, None, &["--target", "wasm32-unknown-unknown"]);
assert!(build.cfgs.contains(&"triple".to_owned()), "{:?}", build.cfgs);
assert!(build.cfgs.contains(&"family".to_owned()), "{:?}", build.cfgs);
assert!(!build.cfgs.contains(&"elsewhere".to_owned()), "{:?}", build.cfgs);
assert_eq!(build.undecided, vec!["elsewhere".to_owned()]);
assert!(!build.undecided.contains(&"family".to_owned()));
}
#[test]
fn a_table_this_build_does_not_read_leaves_its_codegen_predicates_unanswerable() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[target.wasm32-unknown-unknown]\nrustflags = [\"-C\", \"debug-assertions=off\", \"-C\", \"panic=abort\", \"-C\", \"target-feature=+simd128\"]\n",
)]);
let build = resolve(&root, None, &[]);
assert_eq!(
build.undecided,
vec!["debug_assertions".to_owned(), "panic".to_owned(), "target_feature".to_owned()]
);
}
#[test]
fn the_build_rustflags_variable_ranks_below_the_target_tables() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[target.wasm32-unknown-unknown]\nrustflags = [\"--cfg\", \"tabled\"]\n",
)]);
let environment = Environment {
target: Some("wasm32-unknown-unknown".to_owned()),
build_rustflags: Some("--cfg from_the_variable".to_owned()),
..empty(&root)
};
let build = Build::resolve_in(&root, None, &[], &environment);
assert_eq!(build.cfgs, vec!["tabled".to_owned()]);
}
#[test]
fn the_build_rustflags_variable_outranks_the_configured_build_rustflags() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"configured\"]\n")]);
let environment = Environment {
build_rustflags: Some("--cfg from_the_variable".to_owned()),
..empty(&root)
};
let build = Build::resolve_in(&root, None, &[], &environment);
assert_eq!(build.cfgs, vec!["from_the_variable".to_owned()]);
let outranked = Build::resolve_in(
&root,
None,
&[],
&Environment {
rustflags: Some("--cfg plain".to_owned()),
build_rustflags: Some("--cfg from_the_variable".to_owned()),
..empty(&root)
},
);
assert_eq!(outranked.cfgs, vec!["plain".to_owned()]);
}
#[test]
fn the_environment_outranks_the_configuration_files() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"configured\"]\n")]);
let environment = Environment {
rustflags: Some("--cfg loom -C debug-assertions=off".to_owned()),
..empty(&root)
};
let build = Build::resolve_in(&root, None, &[], &environment);
assert_eq!(build.cfgs, vec!["loom".to_owned()]);
assert_eq!(build.debug_assertions, Some(false), "the flags say so outright");
}
#[test]
fn cargo_build_rustflags_are_used_when_the_other_variables_are_absent() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"configured\"]\n")]);
let environment = Environment {
build_rustflags: Some("--cfg environment".to_owned()),
..empty(&root)
};
assert_eq!(Build::resolve_in(&root, None, &[], &environment).cfgs, vec!["environment"]);
}
#[test]
fn target_table_uncertainty_is_sorted_deduplicated_and_excludes_the_chosen_flags() {
let (_directory, root) = tree(&[(
".cargo/config.toml",
"[target.chosen]\nrustflags=[\"--cfg\", \"same\"]\n\
[target.z]\nrustflags=[\"--cfg\", \"maybe\", \"--cfg\", \"maybe\"]\n\
[target.a]\nrustflags=[\"--cfg\", \"alpha\"]\n",
)]);
let build = resolve(&root, None, &["--target", "chosen"]);
assert_eq!(build.cfgs, vec!["same"]);
assert_eq!(build.undecided, vec!["alpha", "maybe"]);
}
#[test]
fn encoded_flags_are_split_on_the_unit_separator() {
let (_directory, root) = tree(&[]);
let environment = Environment {
encoded_rustflags: Some("--cfg\u{1f}flavor=\"two words\"\u{1f}".to_owned()),
rustflags: Some("--cfg ignored".to_owned()),
..empty(&root)
};
let build = Build::resolve_in(&root, None, &[], &environment);
assert_eq!(build.cfgs, vec!["flavor=\"two words\"".to_owned()]);
}
#[test]
fn flag_value_parsing_requires_exact_spellings_and_keeps_order() {
let args = [
"--cfg",
"one",
"--cfg=two",
"--cfg-dir=wrong",
"--cfg",
"three",
"--cfg",
"--cfg=four",
]
.map(ToOwned::to_owned);
assert_eq!(valued(&args, "--cfg"), vec!["one", "two", "three", "--cfg=four"]);
}
#[test]
fn codegen_options_accept_every_rustc_spelling_but_only_cfg_relevant_values() {
let flags = [
"-C",
"target-feature=+avx2",
"--codegen",
"panic=abort",
"-Cdebug-assertions",
"--codegen=target-feature=-sse",
"-C",
"opt-level=3",
]
.map(ToOwned::to_owned);
assert_eq!(codegen(&flags), vec!["target-feature=+avx2", "panic=abort", "target-feature=-sse"]);
assert_eq!(assertions(&flags), Some(true));
}
#[test]
fn debug_assertion_flags_use_the_last_recognised_exact_value() {
for enabled in ["y", "yes", "on", "true"] {
assert_eq!(assertions(&["-C".to_owned(), format!("debug-assertions={enabled}")]), Some(true));
}
for disabled in ["n", "no", "off", "false"] {
assert_eq!(
assertions(&["--codegen".to_owned(), format!("debug-assertions={disabled}")]),
Some(false)
);
}
assert_eq!(
assertions(&["-Cdebug-assertions=true", "-Cdebug-assertions=perhaps", "-Cdebug-assertions=false"].map(ToOwned::to_owned)),
Some(false)
);
assert_eq!(assertions(&["-Cdebug-assertions=perhaps".to_owned()]), None);
assert_eq!(assertions(&["-Cdebug-assertions-extra=true".to_owned()]), None);
assert_eq!(assertions(&["-C".to_owned()]), None);
assert!(options(&["-C".to_owned()]).next().is_none());
}
#[test]
fn release_shorthand_must_match_exactly() {
assert_eq!(named_profile(&["-r".to_owned()]).as_deref(), Some("release"));
assert_eq!(named_profile(&["--release".to_owned()]).as_deref(), Some("release"));
assert_eq!(named_profile(&["-really-not-release".to_owned()]), None);
}
#[test]
fn host_output_parsing_requires_the_host_field() {
assert_eq!(
parse_host("rustc 1.0\nhost: x86_64-example\nrelease: 1.0\n").as_deref(),
Some("x86_64-example")
);
assert_eq!(parse_host("rustc 1.0\ntarget: x86_64-example\n"), None);
}
#[test]
fn host_lookup_is_skipped_without_target_tables_and_answers_with_them() {
let with_target = CargoConfig {
tables: vec![toml::from_str("[target.host]\nrustflags=[]\n").expect("the fixture parses")],
sources: Vec::new(),
};
let host = host_triple(&with_target, false).expect("the compiler that built the tests reports its host");
assert!(!host.is_empty());
assert_ne!(host, "xyzzy");
assert_eq!(host_triple(&CargoConfig::default(), false), None);
}
#[test]
fn the_default_profile_has_debug_assertions_on() {
let (_directory, root) = tree(&[]);
assert_eq!(resolve(&root, None, &[]).debug_assertions, Some(true));
}
#[test]
fn the_release_profile_has_them_off() {
let (_directory, root) = tree(&[]);
assert_eq!(resolve(&root, Some("release"), &[]).debug_assertions, Some(false));
assert_eq!(resolve(&root, None, &["--release"]).debug_assertions, Some(false));
assert_eq!(resolve(&root, None, &["--profile", "bench"]).debug_assertions, Some(false));
}
#[test]
fn a_passthrough_profile_outranks_the_configured_one() {
let (_directory, root) = tree(&[]);
assert_eq!(resolve(&root, Some("dev"), &["--profile=release"]).debug_assertions, Some(false));
}
#[test]
fn a_custom_profile_follows_what_it_inherits() {
let (_directory, root) = tree(&[(
"Cargo.toml",
"[workspace]\n\n[profile.mutants]\ninherits = \"release\"\n\n[profile.loud]\ninherits = \"dev\"\n",
)]);
assert_eq!(resolve(&root, Some("mutants"), &[]).debug_assertions, Some(false));
assert_eq!(resolve(&root, Some("loud"), &[]).debug_assertions, Some(true));
}
#[test]
fn a_profile_that_switches_them_on_is_believed_over_what_it_inherits() {
let (_directory, root) = tree(&[(
"Cargo.toml",
"[workspace]\n\n[profile.mutants]\ninherits = \"release\"\ndebug-assertions = true\n",
)]);
assert_eq!(resolve(&root, Some("mutants"), &[]).debug_assertions, Some(true));
}
#[test]
fn the_underscore_spelling_of_debug_assertions_is_accepted() {
let (_directory, root) = tree(&[(
"Cargo.toml",
"[workspace]\n\n[profile.mutants]\ninherits = \"release\"\ndebug_assertions = true\n",
)]);
assert_eq!(resolve(&root, Some("mutants"), &[]).debug_assertions, Some(true));
}
#[test]
fn a_configured_profile_overrides_the_manifest() {
let (_directory, root) = tree(&[
("Cargo.toml", "[workspace]\n\n[profile.release]\ndebug-assertions = true\n"),
(".cargo/config.toml", "[profile.release]\ndebug-assertions = false\n"),
]);
assert_eq!(resolve(&root, Some("release"), &[]).debug_assertions, Some(false));
}
#[test]
fn a_profile_that_cannot_be_followed_is_left_unanswered() {
let (_directory, root) = tree(&[("Cargo.toml", "[workspace]\n")]);
assert_eq!(resolve(&root, Some("nowhere"), &[]).debug_assertions, None);
}
#[test]
fn a_missing_parent_is_not_replaced_with_the_empty_profile() {
let (_directory, root) = tree(&[(
"Cargo.toml",
"[workspace]\n\n[profile.custom]\n\n[profile.\"\"]\ndebug-assertions = true\n",
)]);
assert_eq!(resolve(&root, Some("custom"), &[]).debug_assertions, None);
}
#[test]
fn a_cyclic_profile_chain_terminates() {
let (_directory, root) = tree(&[(
"Cargo.toml",
"[workspace]\n\n[profile.a]\ninherits = \"b\"\n\n[profile.b]\ninherits = \"a\"\n",
)]);
assert_eq!(resolve(&root, Some("a"), &[]).debug_assertions, None);
}
fn profile_chain(length: usize, switched_at: usize) -> String {
use core::fmt::Write as _;
let mut manifest = String::from("[workspace]\n");
for index in 0..=length {
let _ = writeln!(manifest, "\n[profile.p{index}]");
if index < length {
let _ = writeln!(manifest, "inherits = \"p{}\"", index + 1);
}
if index == switched_at {
manifest.push_str("debug-assertions = true\n");
}
}
manifest
}
#[test]
fn profile_chain_iteration_starts_at_zero_and_ends_before_the_limit() {
for (switched_at, expected) in [(0, Some(true)), (PROFILE_DEPTH - 1, Some(true)), (PROFILE_DEPTH, None)] {
let manifest = profile_chain(PROFILE_DEPTH, switched_at);
let (_directory, root) = tree(&[("Cargo.toml", &manifest)]);
assert_eq!(resolve(&root, Some("p0"), &[]).debug_assertions, expected);
}
}
#[test]
fn an_unparsable_configuration_file_is_ignored() {
let (_directory, root) = tree(&[(".cargo/config.toml", "[build\ntarget =\n")]);
assert_eq!(resolve(&root, None, &[]).target, None);
}
#[test]
fn the_probe_command_line_carries_every_setting() {
let build = Build {
target: Some("wasm32-unknown-unknown".to_owned()),
cfgs: vec!["loom".to_owned()],
codegen: vec!["target-feature=+atomics".to_owned()],
debug_assertions: Some(false),
undecided: Vec::new(),
several_targets: false,
};
assert_eq!(
build.probe_args(),
vec![
"--print",
"cfg",
"--target",
"wasm32-unknown-unknown",
"-C",
"debug-assertions=off",
"-C",
"target-feature=+atomics",
"--cfg",
"loom",
]
);
assert!(
!Build::default()
.probe_args()
.iter()
.any(|argument| argument.starts_with("debug-assertions"))
);
}
}