use std::collections::HashMap;
use anyhow::{Context as _, Result};
use crate::config::{BuildConfig, BuildIgnore, BuildOverride, BuilderKind, CrateConfig};
use crate::context::Context;
use crate::env::parse_env_entries;
use crate::env_expand::expand_env;
use crate::env_source::EnvSource;
use crate::log::StageLogger;
use crate::target::map_target;
use crate::template::TemplateVars;
pub const BUILD_TARGET_VARS: &[&str] = &[
"Target",
"Os",
"Arch",
"Arm64",
"Arm",
"Amd64",
"Mips",
"I386",
"ArtifactExt",
"ArtifactID",
];
pub fn seed_build_target_vars(vars: &mut TemplateVars, target: &str, os: &str, build_id: &str) {
vars.set("Target", target);
vars.set("Os", os);
vars.set("Arch", &map_target(target).1);
crate::archive_name::seed_variant_vars(vars, target, None);
vars.set("ArtifactExt", if os == "windows" { ".exe" } else { "" });
vars.set("ArtifactID", build_id);
}
pub fn clear_build_target_vars(vars: &mut TemplateVars) {
for k in BUILD_TARGET_VARS {
vars.set(k, "");
}
}
pub fn is_target_ignored(target: &str, ignores: &[BuildIgnore]) -> bool {
if ignores.is_empty() {
return false;
}
let (os, arch) = map_target(target);
ignores.iter().any(|ig| ig.os == os && ig.arch == arch)
}
pub fn try_compile_glob(
key: &str,
label: &str,
log: &StageLogger,
strict: bool,
) -> Result<Option<glob::Pattern>> {
match glob::Pattern::new(key) {
Ok(pat) => Ok(Some(pat)),
Err(e) => {
if strict {
anyhow::bail!(
"build: invalid glob pattern in {} '{}': {} (strict mode)",
label,
key,
e
);
}
log.warn(&format!(
"invalid glob pattern in {} '{}': {}",
label, key, e
));
Ok(None)
}
}
}
pub fn resolve_target_env(
env: Option<&HashMap<String, HashMap<String, String>>>,
target: &str,
log: &StageLogger,
strict: bool,
) -> Result<Option<HashMap<String, String>>> {
let Some(env) = env else { return Ok(None) };
if env.is_empty() {
return Ok(None);
}
let mut sorted_keys: Vec<&String> = env.keys().collect();
sorted_keys.sort();
let mut merged: HashMap<String, String> = HashMap::new();
let mut matched_any = false;
for key in sorted_keys {
let Some(pat) = try_compile_glob(key, "build.env key", log, strict)? else {
continue;
};
if pat.matches(target)
&& let Some(vals) = env.get(key)
{
matched_any = true;
for (k, v) in vals {
merged.insert(k.clone(), v.clone());
}
}
}
Ok(if matched_any { Some(merged) } else { None })
}
pub fn find_matching_override<'a>(
target: &str,
overrides: &'a [BuildOverride],
log: &StageLogger,
strict: bool,
) -> Result<Option<&'a BuildOverride>> {
for ov in overrides {
for pat_str in &ov.targets {
let Some(pat) = try_compile_glob(pat_str, "build override target", log, strict)? else {
continue;
};
if pat.matches(target) {
return Ok(Some(ov));
}
}
}
Ok(None)
}
pub fn amd64_variant_from_rustflags(rustflags: &str) -> Option<String> {
let tokens: Vec<&str> = rustflags.split_whitespace().collect();
let mut i = 0;
while i < tokens.len() {
let cpu = if let Some(val) = tokens[i].strip_prefix("-Ctarget-cpu=") {
Some(val)
} else if let Some(val) = tokens[i]
.strip_prefix("--codegen=")
.and_then(|opt| opt.strip_prefix("target-cpu="))
{
Some(val)
} else if (tokens[i] == "-C" || tokens[i] == "--codegen")
&& i + 1 < tokens.len()
&& let Some(val) = tokens[i + 1].strip_prefix("target-cpu=")
{
i += 1;
Some(val)
} else {
None
};
if let Some(cpu) = cpu
&& let Some(level) = cpu.strip_prefix("x86-64-")
{
return Some(level.to_string());
}
i += 1;
}
None
}
pub fn cargo_target_rustflags_key(target: &str) -> String {
let base = target.split('.').next().unwrap_or(target);
format!(
"CARGO_TARGET_{}_RUSTFLAGS",
base.to_uppercase().replace('-', "_")
)
}
fn effective_rustflags(
target: &str,
env: &HashMap<String, String>,
process: &dyn EnvSource,
) -> Option<String> {
let lookup = |key: &str| env.get(key).cloned().or_else(|| process.var(key));
if let Some(encoded) = lookup("CARGO_ENCODED_RUSTFLAGS") {
return Some(encoded.replace('\u{1f}', " "));
}
if let Some(flags) = lookup("RUSTFLAGS") {
return Some(flags);
}
lookup(&cargo_target_rustflags_key(target))
}
pub fn amd64_variant_from_env(
target: &str,
env: &HashMap<String, String>,
process: &dyn EnvSource,
) -> Option<String> {
if !target.starts_with("x86_64") {
return None;
}
effective_rustflags(target, env, process).and_then(|flags| amd64_variant_from_rustflags(&flags))
}
pub fn declared_amd64_variant(build: &BuildConfig, target: &str) -> Option<String> {
if !target.starts_with("x86_64") {
return None;
}
build.amd64_variant.map(|l| l.as_str().to_string())
}
pub fn build_amd64_variant(
build: &BuildConfig,
target: &str,
env: &HashMap<String, String>,
process: &dyn EnvSource,
log: &StageLogger,
) -> Option<String> {
let declared = declared_amd64_variant(build, target);
let detected = amd64_variant_from_env(target, env, process);
if let (Some(d), Some(det)) = (declared.as_deref(), detected.as_deref())
&& d != det
{
log.warn(&format!(
"build '{}' declares amd64_variant \"{d}\" but its resolved env for {target} \
detects x86-64 level \"{det}\"; the declaration names the artifacts — update \
`amd64_variant:` if the tuning level changed",
build
.id
.as_deref()
.or(build.binary.as_deref())
.unwrap_or("<unnamed>"),
));
}
declared.or(detected)
}
pub fn prebuilt_amd64_variant(build: &BuildConfig, target: &str) -> Option<String> {
if target.split('-').next() == Some("x86_64") {
Some(build.amd64_variant.map_or("v1", |l| l.as_str()).to_string())
} else {
None
}
}
pub fn config_time_amd64_variant(
crate_cfg: &CrateConfig,
target: &str,
default_targets: &[String],
ctx: &mut Context,
) -> Result<Option<String>> {
if !target.starts_with("x86_64") {
return Ok(None);
}
let Some(builds) = crate::build_plan::planned_builds(crate_cfg) else {
return Ok(None);
};
let default_builds = ctx.config.defaults.as_ref().and_then(|d| d.builds.as_ref());
let default_ignores: Vec<BuildIgnore> = default_builds
.and_then(|b| b.ignore.clone())
.unwrap_or_default();
let default_overrides: Vec<BuildOverride> = default_builds
.and_then(|b| b.overrides.clone())
.unwrap_or_default();
let log = ctx.logger("build");
let pass_matches = |pass: usize, b: &BuildConfig| -> bool {
let prebuilt = matches!(b.builder, Some(BuilderKind::Prebuilt));
match pass {
0 => prebuilt,
1 => !prebuilt && b.copy_from.is_none(),
_ => !prebuilt && b.copy_from.is_some(),
}
};
for pass in 0..3 {
for build in &builds {
if !pass_matches(pass, build) {
continue;
}
if !crate::build_plan::build_produces(crate_cfg, build) {
continue;
}
if build_skipped(build, ctx)? {
continue;
}
let targets: &[String] = if pass == 0 {
build.targets.as_deref().unwrap_or(&[])
} else {
build.targets.as_deref().unwrap_or(default_targets)
};
if !targets.iter().any(|t| t == target) {
continue;
}
let ignores = build.ignore.as_deref().unwrap_or(&default_ignores);
if is_target_ignored(target, ignores) {
continue;
}
if pass == 0 {
return Ok(prebuilt_amd64_variant(build, target));
}
return build_env_amd64_variant(build, target, &default_overrides, &log, ctx);
}
}
Ok(None)
}
fn build_skipped(build: &BuildConfig, ctx: &Context) -> Result<bool> {
match build.skip.as_ref() {
Some(s) => s
.try_evaluates_to_true(|tmpl| ctx.render_template(tmpl))
.with_context(|| {
format!(
"build: render skip template for build '{}'",
build.id.as_deref().unwrap_or("<unnamed>")
)
}),
None => Ok(false),
}
}
fn build_env_amd64_variant(
build: &BuildConfig,
target: &str,
default_overrides: &[BuildOverride],
log: &StageLogger,
ctx: &mut Context,
) -> Result<Option<String>> {
if let Some(declared) = declared_amd64_variant(build, target) {
return Ok(Some(declared));
}
let strict = ctx.is_strict();
let raw = resolve_target_env(build.env.as_ref(), target, log, strict)?;
if build.copy_from.is_some() {
let empty = HashMap::new();
return Ok(amd64_variant_from_env(
target,
raw.as_ref().unwrap_or(&empty),
ctx.env_source(),
));
}
let saved_vars: Vec<(&str, Option<String>)> = BUILD_TARGET_VARS
.iter()
.map(|k| (*k, ctx.template_vars().get(k).cloned()))
.collect();
let (os, _) = map_target(target);
seed_build_target_vars(
ctx.template_vars_mut(),
target,
&os,
build.id.as_deref().unwrap_or(""),
);
let projected = (|| -> Result<HashMap<String, String>> {
let mut rendered: HashMap<String, String> = HashMap::new();
let mut touched: Vec<(String, Option<String>)> = Vec::new();
if let Some(raw) = &raw {
let mut keys: Vec<&String> = raw.keys().collect();
keys.sort();
for k in keys {
let v = &raw[k];
let val = ctx.render_template(v).unwrap_or_else(|_| v.clone());
let expanded = expand_env(&val);
touched.push((k.clone(), ctx.template_vars().all_env().get(k).cloned()));
ctx.template_vars_mut().set_env(k, &expanded);
rendered.insert(k.clone(), expanded);
}
}
let overrides = build.overrides.as_deref().unwrap_or(default_overrides);
let override_merge = (|| -> Result<()> {
if let Some(ov) = find_matching_override(target, overrides, log, strict)?
&& let Some(ov_env) = &ov.env
{
for (k, v) in parse_env_entries(ov_env)? {
let val = ctx.render_template(&v).unwrap_or_else(|_| v.clone());
rendered.insert(k, expand_env(&val));
}
}
Ok(())
})();
for (k, prior) in touched {
match prior {
Some(p) => ctx.template_vars_mut().set_env(&k, &p),
None => {
ctx.template_vars_mut().unset_env(&k);
}
}
}
override_merge?;
Ok(rendered)
})();
for (k, prior) in saved_vars {
match prior {
Some(p) => ctx.template_vars_mut().set(k, &p),
None => {
ctx.template_vars_mut().unset(k);
}
}
}
let mut rendered = projected?;
if build.reproducible.unwrap_or(false)
&& rendered
.get("RUSTFLAGS")
.map(|s| s.trim().is_empty())
.unwrap_or(true)
{
rendered.insert(
"RUSTFLAGS".to_string(),
ctx.env_source().var("RUSTFLAGS").unwrap_or_default(),
);
}
Ok(amd64_variant_from_env(target, &rendered, ctx.env_source()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Amd64Variant, Config, Defaults};
use crate::context::{Context, ContextOptions};
use crate::env_source::MapEnvSource;
const LINUX_AMD64: &str = "x86_64-unknown-linux-gnu";
fn ctx() -> Context {
let config = Config {
project_name: "myapp".to_string(),
..Default::default()
};
let mut ctx = Context::new(config, ContextOptions::default());
ctx.set_env_source(MapEnvSource::new());
ctx.template_vars_mut().set("Version", "1.0.0");
ctx.template_vars_mut().set("ProjectName", "myapp");
ctx
}
fn no_process_env() -> MapEnvSource {
MapEnvSource::new()
}
fn crate_with_build(build: BuildConfig) -> CrateConfig {
CrateConfig {
name: "myapp".to_string(),
builds: Some(vec![build]),
..Default::default()
}
}
fn tuned_build(target_key: &str, rustflags: &str) -> BuildConfig {
let mut env = HashMap::new();
env.insert(
target_key.to_string(),
HashMap::from([("RUSTFLAGS".to_string(), rustflags.to_string())]),
);
BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
env: Some(env),
..Default::default()
}
}
#[test]
fn detects_variant_from_static_env() {
let krate = crate_with_build(tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3"));
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3")
);
}
#[test]
fn detects_variant_via_glob_env_key() {
let krate = crate_with_build(tuned_build("x86_64-*", "-C target-cpu=x86-64-v2"));
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v2")
);
}
#[test]
fn non_x86_64_targets_never_derive_a_variant() {
let mut build = tuned_build("*", "-Ctarget-cpu=x86-64-v3");
build.targets = Some(vec!["aarch64-unknown-linux-gnu".to_string()]);
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, "aarch64-unknown-linux-gnu", &[], &mut c).unwrap(),
None
);
}
#[test]
fn untuned_env_derives_no_variant() {
let build = BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
..Default::default()
};
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap(),
None
);
}
#[test]
fn templated_env_renders_before_detection() {
let mut c = ctx();
c.template_vars_mut().set("TuneLevel", "3");
let krate = crate_with_build(tuned_build(
LINUX_AMD64,
"-Ctarget-cpu=x86-64-v{{ TuneLevel }}",
));
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3")
);
}
#[test]
fn unrenderable_env_falls_back_to_raw_string_like_the_planner() {
let krate = crate_with_build(tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v2 {{ Broken"));
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v2"),
"raw fallback must preserve a literal target-cpu token"
);
let krate = crate_with_build(tuned_build(LINUX_AMD64, "{{ BuildTimeOnlyVar }}"));
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap(),
None,
"a build-time-only tuning value derives no variant (loud backstop remains)"
);
}
#[test]
fn override_env_participates_in_detection() {
let build = BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
overrides: Some(vec![BuildOverride {
targets: vec!["x86_64-*".to_string()],
env: Some(vec!["RUSTFLAGS=-Ctarget-cpu=x86-64-v4".to_string()]),
flags: None,
features: None,
}]),
..Default::default()
};
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v4")
);
}
#[test]
fn defaults_builds_overrides_apply_when_build_has_none() {
let build = BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
..Default::default()
};
let krate = crate_with_build(build);
let mut c = ctx();
c.config.defaults = Some(Defaults {
builds: Some(BuildConfig {
overrides: Some(vec![BuildOverride {
targets: vec![LINUX_AMD64.to_string()],
env: Some(vec!["RUSTFLAGS=-Ctarget-cpu=x86-64-v3".to_string()]),
flags: None,
features: None,
}]),
..Default::default()
}),
..Default::default()
});
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3")
);
}
#[test]
fn inherited_process_rustflags_reach_detection() {
let mut build = BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
reproducible: Some(true),
..Default::default()
};
let krate = crate_with_build(build.clone());
let mut c = ctx();
c.set_env_source(MapEnvSource::new().with("RUSTFLAGS", "-Ctarget-cpu=x86-64-v3"));
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3")
);
build.reproducible = None;
let krate = crate_with_build(build);
let mut c = ctx();
c.set_env_source(MapEnvSource::new().with("RUSTFLAGS", "-Ctarget-cpu=x86-64-v3"));
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3"),
"the inherited RUSTFLAGS reaches cargo regardless of reproducible:"
);
}
#[test]
fn reproducible_merge_shadows_config_target_rustflags_without_process_flags() {
let mut build = tuned_build_key(
LINUX_AMD64,
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS",
"-Ctarget-cpu=x86-64-v3",
);
build.reproducible = Some(true);
let krate = crate_with_build(build.clone());
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap(),
None,
"the reproducible RUSTFLAGS merge shadows the target-specific var"
);
build.reproducible = None;
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3"),
"a plain build's target-specific var stays cargo's source"
);
}
#[test]
fn process_rustflags_suppress_config_target_rustflags() {
let env = HashMap::from([(
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v3".to_string(),
)]);
let ci = MapEnvSource::new().with("RUSTFLAGS", "-Dwarnings");
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &env, &ci),
None,
"a process RUSTFLAGS shadows the config target-specific var"
);
let krate = crate_with_build(tuned_build_key(
LINUX_AMD64,
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS",
"-Ctarget-cpu=x86-64-v3",
));
let mut c = ctx();
c.set_env_source(MapEnvSource::new().with("RUSTFLAGS", "-Dwarnings"));
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap(),
None,
"the projection must stamp the baseline the binary actually is"
);
}
#[test]
fn process_encoded_rustflags_shadow_config_rustflags() {
let env = HashMap::from([(
"RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v2".to_string(),
)]);
let process =
MapEnvSource::new().with("CARGO_ENCODED_RUSTFLAGS", "-C\u{1f}target-cpu=x86-64-v4");
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &env, &process).as_deref(),
Some("v4"),
"a process CARGO_ENCODED_RUSTFLAGS outranks the config RUSTFLAGS"
);
}
#[test]
fn config_key_beats_process_key_on_the_same_tier() {
let env = HashMap::from([(
"RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v2".to_string(),
)]);
let process = MapEnvSource::new().with("RUSTFLAGS", "-Ctarget-cpu=x86-64-v3");
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &env, &process).as_deref(),
Some("v2"),
"the .envs() overlay replaces the inherited value for the same key"
);
}
#[test]
fn declared_vs_detected_mismatch_warns_and_match_stays_silent() {
use crate::log::{StageLogger, Verbosity};
let mut build = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v4");
build.amd64_variant = Some(Amd64Variant::V3);
let env = HashMap::from([(
"RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v4".to_string(),
)]);
let (log, cap) = StageLogger::with_capture("build", Verbosity::Quiet);
assert_eq!(
build_amd64_variant(&build, LINUX_AMD64, &env, &no_process_env(), &log).as_deref(),
Some("v3"),
"the declaration still wins"
);
let warns = cap.warn_messages();
assert_eq!(warns.len(), 1, "exactly one mismatch warning: {warns:?}");
assert!(
warns[0].contains("\"v3\"") && warns[0].contains("\"v4\""),
"warning names both levels: {}",
warns[0]
);
build.amd64_variant = Some(Amd64Variant::V4);
let (log, cap) = StageLogger::with_capture("build", Verbosity::Quiet);
assert_eq!(
build_amd64_variant(&build, LINUX_AMD64, &env, &no_process_env(), &log).as_deref(),
Some("v4")
);
assert_eq!(cap.warn_count(), 0, "a matching declaration must not warn");
build.amd64_variant = Some(Amd64Variant::V3);
let empty = HashMap::new();
let (log, cap) = StageLogger::with_capture("build", Verbosity::Quiet);
assert_eq!(
build_amd64_variant(&build, LINUX_AMD64, &empty, &no_process_env(), &log).as_deref(),
Some("v3")
);
assert_eq!(
cap.warn_count(),
0,
"declaring an undetectable level is the feature, not a mismatch"
);
}
#[test]
fn skipped_and_target_ignored_builds_do_not_decide_the_variant() {
use crate::config::StringOrBool;
let mut skipped = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
skipped.skip = Some(StringOrBool::Bool(true));
let live = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v2");
let krate = CrateConfig {
name: "myapp".to_string(),
builds: Some(vec![skipped, live]),
..Default::default()
};
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v2")
);
let mut ignored = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
ignored.ignore = Some(vec![BuildIgnore {
os: "linux".to_string(),
arch: "amd64".to_string(),
}]);
let krate = crate_with_build(ignored);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap(),
None
);
}
#[test]
fn detects_long_form_codegen_flags() {
assert_eq!(
amd64_variant_from_rustflags("--codegen target-cpu=x86-64-v3").as_deref(),
Some("v3")
);
assert_eq!(
amd64_variant_from_rustflags("--codegen=target-cpu=x86-64-v2").as_deref(),
Some("v2")
);
assert_eq!(
amd64_variant_from_rustflags("--codegen opt-level=3").as_deref(),
None
);
}
#[test]
fn detects_cargo_target_rustflags_env_var() {
let env = HashMap::from([(
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v3".to_string(),
)]);
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &env, &no_process_env()).as_deref(),
Some("v3")
);
assert_eq!(
amd64_variant_from_env("x86_64-apple-darwin", &env, &no_process_env()),
None
);
assert_eq!(
amd64_variant_from_env("x86_64-unknown-linux-gnu.2.17", &env, &no_process_env())
.as_deref(),
Some("v3")
);
}
#[test]
fn rustflags_shadows_cargo_target_rustflags() {
let both = HashMap::from([
(
"RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v2".to_string(),
),
(
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v3".to_string(),
),
]);
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &both, &no_process_env()).as_deref(),
Some("v2"),
"RUSTFLAGS must win when both are present"
);
let empty_shadows = HashMap::from([
("RUSTFLAGS".to_string(), "".to_string()),
(
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v3".to_string(),
),
]);
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &empty_shadows, &no_process_env()),
None,
"a set-but-empty RUSTFLAGS still shadows the target-specific var"
);
let encoded_wins = HashMap::from([
(
"CARGO_ENCODED_RUSTFLAGS".to_string(),
"-C\u{1f}target-cpu=x86-64-v4".to_string(),
),
(
"RUSTFLAGS".to_string(),
"-Ctarget-cpu=x86-64-v2".to_string(),
),
]);
assert_eq!(
amd64_variant_from_env(LINUX_AMD64, &encoded_wins, &no_process_env()).as_deref(),
Some("v4"),
"CARGO_ENCODED_RUSTFLAGS outranks RUSTFLAGS"
);
}
#[test]
fn cargo_target_env_var_flows_through_the_projection() {
let krate = crate_with_build(tuned_build_key(
LINUX_AMD64,
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS",
"-Ctarget-cpu=x86-64-v2",
));
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v2")
);
}
fn tuned_build_key(target_key: &str, env_key: &str, value: &str) -> BuildConfig {
let mut env = HashMap::new();
env.insert(
target_key.to_string(),
HashMap::from([(env_key.to_string(), value.to_string())]),
);
BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
env: Some(env),
..Default::default()
}
}
#[test]
fn projection_restores_caller_per_target_vars() {
let krate = crate_with_build(tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3"));
let mut c = ctx();
c.template_vars_mut().set("Os", "sentinel-os");
assert!(c.template_vars().get("ArtifactID").is_none());
let got = config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap();
assert_eq!(got.as_deref(), Some("v3"));
assert_eq!(
c.template_vars().get("Os").map(String::as_str),
Some("sentinel-os"),
"caller's Os must be restored"
);
assert!(
c.template_vars().get("ArtifactID").is_none(),
"vars unset before the projection must end unset"
);
assert!(c.template_vars().get("Target").is_none());
}
#[test]
fn prebuilt_builds_mirror_the_planner_stamp() {
use crate::config::{BuilderKind, PrebuiltConfig};
let mut build = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
build.builder = Some(BuilderKind::Prebuilt);
build.prebuilt = Some(PrebuiltConfig {
path: "out/myapp_{{ Target }}".to_string(),
});
let krate = crate_with_build(build.clone());
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v1"),
"prebuilt x86-64 import carries the planner's v1 stamp, not its env"
);
build.amd64_variant = Some(Amd64Variant::V3);
let krate = crate_with_build(build.clone());
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3"),
"declared level overrides the prebuilt baseline"
);
build.amd64_variant = None;
build.targets = Some(vec!["aarch64-unknown-linux-gnu".to_string()]);
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, "aarch64-unknown-linux-gnu", &[], &mut c).unwrap(),
None
);
}
#[test]
fn declared_variant_overrides_detection_and_unrenderable_env() {
let mut build = tuned_build(LINUX_AMD64, "{{ BuildTimeOnlyVar }}");
build.amd64_variant = Some(Amd64Variant::V3);
let krate = crate_with_build(build.clone());
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3")
);
let mut build = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
build.amd64_variant = Some(Amd64Variant::V2);
let krate = crate_with_build(build.clone());
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v2")
);
let mut build = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
build.amd64_variant = Some(Amd64Variant::V3);
build.targets = Some(vec!["aarch64-unknown-linux-gnu".to_string()]);
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, "aarch64-unknown-linux-gnu", &[], &mut c).unwrap(),
None
);
}
#[test]
fn projection_follows_artifact_registration_order_not_config_order() {
use crate::config::{BuilderKind, PrebuiltConfig};
let mut copy = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
copy.copy_from = Some("other".to_string());
let live = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v2");
let krate = CrateConfig {
name: "myapp".to_string(),
builds: Some(vec![copy, live.clone()]),
..Default::default()
};
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v2"),
"the compile build registers its artifact before the copy_from job"
);
let mut prebuilt = BuildConfig {
binary: Some("myapp".to_string()),
targets: Some(vec![LINUX_AMD64.to_string()]),
..Default::default()
};
prebuilt.builder = Some(BuilderKind::Prebuilt);
prebuilt.prebuilt = Some(PrebuiltConfig {
path: "out/myapp".to_string(),
});
let krate = CrateConfig {
name: "myapp".to_string(),
builds: Some(vec![live, prebuilt]),
..Default::default()
};
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v1"),
"the prebuilt import's artifact is registered before compile jobs"
);
}
#[test]
fn copy_from_builds_detect_from_raw_env() {
let mut build = tuned_build(LINUX_AMD64, "-Ctarget-cpu={{ CpuVar }}");
build.copy_from = Some("other".to_string());
let krate = crate_with_build(build);
let mut c = ctx();
c.template_vars_mut().set("CpuVar", "x86-64-v3");
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c).unwrap(),
None,
"copy_from must not render env before detection"
);
let mut compiled = tuned_build(LINUX_AMD64, "-Ctarget-cpu={{ CpuVar }}");
compiled.copy_from = None;
let krate = crate_with_build(compiled);
let mut c = ctx();
c.template_vars_mut().set("CpuVar", "x86-64-v3");
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3"),
"the compile path renders the same value before detection"
);
let mut build = tuned_build(LINUX_AMD64, "-Ctarget-cpu=x86-64-v3");
build.copy_from = Some("other".to_string());
let krate = crate_with_build(build);
let mut c = ctx();
assert_eq!(
config_time_amd64_variant(&krate, LINUX_AMD64, &[], &mut c)
.unwrap()
.as_deref(),
Some("v3")
);
}
}