use anodizer_core::env_source::{EnvSource, ProcessEnvSource};
use anodizer_core::harness_signing::EphemeralSigningKeys;
use std::collections::HashMap;
use std::path::Path;
pub(super) const HARNESS_ENV_ALLOWLIST: &[&str] = &[
"RUSTUP_HOME",
"CI",
"NO_COLOR",
"ANODIZER_COLOR",
"GITHUB_REPOSITORY",
"GITHUB_SHA",
"GITHUB_REF",
"GITHUB_REF_NAME",
"GITHUB_RUN_ID",
"GITHUB_RUN_NUMBER",
"GITHUB_WORKFLOW",
"GITHUB_ACTOR",
"RUNNER_OS",
"RUNNER_ARCH",
"RUNNER_NAME",
];
#[cfg(windows)]
const WINDOWS_ENV_DENYLIST: &[&str] = &[
"GITHUB_TOKEN",
"GH_TOKEN",
"GH_PAT",
"CARGO_REGISTRY_TOKEN",
"CARGO_REGISTRIES_CRATES_IO_TOKEN",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"GOOGLE_APPLICATION_CREDENTIALS",
"GCP_SERVICE_ACCOUNT_KEY",
"AZURE_CLIENT_SECRET",
"CHOCOLATEY_API_KEY",
"DOCKER_TOKEN",
"DOCKERHUB_TOKEN",
"GPG_PRIVATE_KEY",
"GPG_PASSPHRASE",
"COSIGN_KEY",
"COSIGN_PASSWORD",
"SNAPCRAFT_STORE_CREDENTIALS",
"CLOUDSMITH_TOKEN",
"MCP_GITHUB_TOKEN",
"SMTP_PASSWORD",
"ARTIFACTORY_TOKEN",
"APK_PRIVATE_KEY",
"ACTIONS_RUNTIME_TOKEN",
"ACTIONS_RUNTIME_URL",
"ACTIONS_CACHE_URL",
"ACTIONS_RESULTS_URL",
"RUNNER_TOKEN",
];
#[cfg(windows)]
fn windows_env_should_drop(key: &str) -> bool {
if WINDOWS_ENV_DENYLIST
.iter()
.any(|d| d.eq_ignore_ascii_case(key))
{
return true;
}
if key.starts_with("ACTIONS_") || key.eq_ignore_ascii_case("RUNNER_TOKEN") {
return true;
}
if key.eq_ignore_ascii_case("CARGO_ENCODED_RUSTFLAGS")
|| key.eq_ignore_ascii_case("RUSTFLAGS")
|| key.eq_ignore_ascii_case("CARGO_BUILD_RUSTFLAGS")
{
return true;
}
let lower = key.to_ascii_lowercase();
if lower.starts_with("cargo_target_") && lower.ends_with("_rustflags") {
return true;
}
if key.eq_ignore_ascii_case("RUSTC_WRAPPER")
|| key.eq_ignore_ascii_case("RUSTC_WORKSPACE_WRAPPER")
|| lower.starts_with("sccache_")
{
return true;
}
for suffix in [
"_token",
"_key",
"_secret",
"_password",
"_passphrase",
"_credentials",
] {
if lower.ends_with(suffix) {
return true;
}
}
if key.starts_with("GITHUB_") || key.starts_with("RUNNER_") {
let in_allowlist = HARNESS_ENV_ALLOWLIST
.iter()
.any(|a| a.eq_ignore_ascii_case(key));
if !in_allowlist {
return true;
}
}
false
}
pub(crate) struct BuildSubprocessEnv<'a> {
pub cargo_home: &'a Path,
pub cargo_target: &'a Path,
pub tmpdir: &'a Path,
pub home_dir: &'a Path,
pub sde: i64,
pub worktree: &'a Path,
pub targets: &'a [String],
pub signing_keys: Option<&'a EphemeralSigningKeys>,
}
pub(super) fn allow_listed_path_with_env(env: &dyn EnvSource) -> String {
env.var("PATH").unwrap_or_default()
}
#[allow(dead_code)]
pub(super) fn allow_listed_path() -> String {
allow_listed_path_with_env(&ProcessEnvSource)
}
pub(crate) fn build_subprocess_env_with_env(
inputs: &BuildSubprocessEnv<'_>,
host_env: &dyn EnvSource,
host_is_windows_msvc: bool,
) -> HashMap<String, String> {
let mut env = HashMap::new();
env.insert(
"CARGO_HOME".into(),
inputs.cargo_home.to_string_lossy().into_owned(),
);
env.insert(
"CARGO_TARGET_DIR".into(),
inputs.cargo_target.to_string_lossy().into_owned(),
);
env.insert(
"TMPDIR".into(),
inputs.tmpdir.to_string_lossy().into_owned(),
);
env.insert(
"HOME".into(),
inputs.home_dir.to_string_lossy().into_owned(),
);
if let Some(docker_config) = host_env.var("DOCKER_CONFIG").or_else(|| {
host_env.var("HOME").map(|home| {
Path::new(&home)
.join(".docker")
.to_string_lossy()
.into_owned()
})
}) {
env.insert("DOCKER_CONFIG".into(), docker_config);
}
env.insert("SOURCE_DATE_EPOCH".into(), inputs.sde.to_string());
env.insert("PATH".into(), allow_listed_path_with_env(host_env));
env.insert("RUSTC_WRAPPER".into(), String::new());
env.insert("RUSTC_WORKSPACE_WRAPPER".into(), String::new());
env.insert("CARGO_NET_OFFLINE".into(), "true".into());
let mut rustflags = host_env.var("RUSTFLAGS").unwrap_or_default();
let worktree_str = inputs.worktree.to_string_lossy();
let cargo_home_str = inputs.cargo_home.to_string_lossy();
let cargo_target_str = inputs.cargo_target.to_string_lossy();
for (label, raw) in [
("worktree", worktree_str.as_ref()),
("cargo_home", cargo_home_str.as_ref()),
("cargo_target", cargo_target_str.as_ref()),
] {
if raw.chars().any(char::is_whitespace) {
panic!(
"determinism harness {label} path {raw:?} contains whitespace; \
RUSTFLAGS has no quoting support and embedded spaces would \
misparse --remap-path-prefix. Re-run with a scratch directory \
free of whitespace."
);
}
}
for (from, to) in [
(worktree_str.as_ref(), "/anodize"),
(cargo_home_str.as_ref(), "/cargo"),
(cargo_target_str.as_ref(), "/target"),
] {
if from.is_empty() {
continue;
}
let flag = format!("--remap-path-prefix={}={}", from, to);
if !rustflags.is_empty() {
rustflags.push(' ');
}
rustflags.push_str(&flag);
}
if !rustflags.is_empty() {
env.insert("RUSTFLAGS".into(), rustflags.clone());
}
let inject_msvc = if inputs.targets.is_empty() {
host_is_windows_msvc
} else {
inputs
.targets
.iter()
.all(|t| anodizer_core::target::is_windows_msvc(t))
};
if inject_msvc {
rustflags = anodizer_core::determinism::merge_msvc_determinism_rustflags(&rustflags);
env.insert("RUSTFLAGS".into(), rustflags.clone());
}
for target in inputs.targets {
for (key, value) in anodizer_core::determinism::msvc_c_toolchain_env(target) {
env.insert(key, value);
}
}
if inputs.targets.is_empty() && host_is_windows_msvc {
if let Ok(host_triple) = anodizer_core::partial::detect_host_target() {
for (key, value) in anodizer_core::determinism::msvc_c_toolchain_env(&host_triple) {
env.insert(key, value);
}
}
}
for &key in HARNESS_ENV_ALLOWLIST {
if let Some(v) = host_env.var(key) {
env.insert(key.into(), v);
}
}
#[cfg(windows)]
for (key, value) in host_env.vars() {
if windows_env_should_drop(&key) {
continue;
}
env.entry(key).or_insert(value);
}
env.entry("RUSTUP_HOME".into()).or_insert_with(|| {
let host_home = host_env
.var("HOME")
.or_else(|| host_env.var("USERPROFILE"))
.map(std::path::PathBuf::from)
.unwrap_or_default();
host_home.join(".rustup").to_string_lossy().into_owned()
});
env.entry("CI".into()).or_insert_with(|| "true".into());
env.entry("RUST_LOG".into())
.or_insert_with(|| "error".into());
env.insert("ANODIZER_IN_DETERMINISM_HARNESS".into(), "1".into());
let llvm_profraw = std::env::temp_dir()
.join("anodize-harness-llvm")
.join("default_%m_%p.profraw");
env.insert(
"LLVM_PROFILE_FILE".into(),
llvm_profraw.to_string_lossy().into_owned(),
);
if let Some(keys) = inputs.signing_keys {
env.insert("COSIGN_KEY".into(), keys.cosign_key_contents.clone());
env.insert("COSIGN_PASSWORD".into(), keys.cosign_password.clone());
env.insert(
"GNUPGHOME".into(),
anodizer_core::harness_signing::path_for_subprocess_env(&keys.gnupg_home),
);
env.insert("GPG_FINGERPRINT".into(), keys.gpg_fingerprint.clone());
env.insert("GPG_TTY".into(), "/dev/null".into());
env.insert(
"GPG_KEY_PATH".into(),
anodizer_core::harness_signing::path_for_subprocess_env(&keys.gpg_key_path),
);
if let Some(apk) = &keys.apk_key_path {
env.insert(
"APK_PRIVATE_KEY_PATH".into(),
anodizer_core::harness_signing::path_for_subprocess_env(apk),
);
}
}
env
}
pub(crate) fn build_subprocess_env(inputs: &BuildSubprocessEnv<'_>) -> HashMap<String, String> {
build_subprocess_env_with_env(
inputs,
&ProcessEnvSource,
anodizer_core::determinism::host_is_windows_msvc(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use anodizer_core::env_source::MapEnvSource;
fn inputs(scratch: &Path) -> BuildSubprocessEnv<'_> {
BuildSubprocessEnv {
cargo_home: scratch,
cargo_target: scratch,
tmpdir: scratch,
home_dir: scratch,
sde: 1_715_000_000,
worktree: scratch,
targets: &[],
signing_keys: None,
}
}
fn build_with(scratch: &Path, host: &[(&str, &str)]) -> HashMap<String, String> {
build_with_host(scratch, host, false)
}
fn build_with_host(
scratch: &Path,
host: &[(&str, &str)],
host_is_windows_msvc: bool,
) -> HashMap<String, String> {
let mut map = MapEnvSource::new();
for (k, v) in host {
map.set(*k, *v);
}
build_subprocess_env_with_env(&inputs(scratch), &map, host_is_windows_msvc)
}
#[test]
fn allow_listed_path_reads_through_env_source() {
let env = MapEnvSource::new().with("PATH", "/fixture/bin:/usr/bin");
assert_eq!(allow_listed_path_with_env(&env), "/fixture/bin:/usr/bin");
}
#[test]
fn signing_keys_with_apk_key_export_apk_private_key_path() {
let scratch = tempfile::tempdir().unwrap();
let keys = EphemeralSigningKeys::for_test(Some(scratch.path().join("apk-harness.pem")));
let mut inputs = inputs(scratch.path());
inputs.signing_keys = Some(&keys);
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
assert!(
env.contains_key("APK_PRIVATE_KEY_PATH"),
"APK_PRIVATE_KEY_PATH must be exported when an apk key is present"
);
}
#[test]
fn signing_keys_without_apk_key_omit_apk_private_key_path() {
let scratch = tempfile::tempdir().unwrap();
let keys = EphemeralSigningKeys::for_test(None);
let mut inputs = inputs(scratch.path());
inputs.signing_keys = Some(&keys);
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
assert!(
!env.contains_key("APK_PRIVATE_KEY_PATH"),
"APK_PRIVATE_KEY_PATH must be absent when no apk key was provisioned"
);
}
#[test]
fn harness_env_caps_child_tracing_at_error() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[]);
assert_eq!(
env.get("RUST_LOG").map(String::as_str),
Some("error"),
"child env must carry RUST_LOG=error so replica builds don't \
re-emit the outer process's static-config warnings"
);
}
#[test]
fn harness_env_always_marks_determinism_children() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[]);
assert_eq!(
env.get("ANODIZER_IN_DETERMINISM_HARNESS")
.map(String::as_str),
Some("1"),
"harness marker must be present without signing keys"
);
}
#[test]
fn harness_env_seals_child_builds_offline() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("CARGO_NET_OFFLINE", "false")]);
assert_eq!(
env.get("CARGO_NET_OFFLINE").map(String::as_str),
Some("true"),
"child rebuild env must force CARGO_NET_OFFLINE=true regardless \
of the host value so rebuilds can never reach the network"
);
}
#[test]
fn harness_env_does_not_leak_github_token() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("GITHUB_TOKEN", "ghp_secret_value")]);
assert!(
!env.contains_key("GITHUB_TOKEN"),
"GITHUB_TOKEN must NOT propagate into the harness subprocess env"
);
assert!(
!env.values().any(|v| v == "ghp_secret_value"),
"no env entry may carry the token value"
);
}
#[test]
fn harness_env_does_not_leak_actions_runtime_token() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("ACTIONS_RUNTIME_TOKEN", "actions_secret")]);
assert!(
!env.contains_key("ACTIONS_RUNTIME_TOKEN"),
"ACTIONS_RUNTIME_TOKEN must NOT propagate into the harness subprocess env"
);
}
#[test]
fn harness_env_does_not_leak_actions_cache_url() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(
tmp.path(),
&[("ACTIONS_CACHE_URL", "https://cache.example")],
);
assert!(
!env.contains_key("ACTIONS_CACHE_URL"),
"ACTIONS_CACHE_URL must NOT propagate (network-reach surface)"
);
}
#[test]
fn harness_env_includes_github_repository_when_set() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("GITHUB_REPOSITORY", "toss45/anodizer")]);
assert_eq!(
env.get("GITHUB_REPOSITORY").map(String::as_str),
Some("toss45/anodizer"),
"GITHUB_REPOSITORY is identity and must propagate"
);
}
#[test]
fn harness_env_includes_github_sha_when_set() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("GITHUB_SHA", "deadbeefcafe")]);
assert_eq!(
env.get("GITHUB_SHA").map(String::as_str),
Some("deadbeefcafe"),
"GITHUB_SHA is identity and must propagate"
);
}
#[test]
fn harness_env_includes_runner_identity_vars_when_set() {
let tmp = tempfile::tempdir().unwrap();
let cases = [
("RUNNER_OS", "Linux"),
("RUNNER_ARCH", "X64"),
("RUNNER_NAME", "self-hosted-1"),
];
let env = build_with(tmp.path(), &cases);
for (k, v) in cases {
assert_eq!(
env.get(k).map(String::as_str),
Some(v),
"{k} is identity and must propagate (value `{v}`)"
);
}
}
#[test]
fn harness_env_derives_docker_config_from_host_home() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("HOME", "/home/runner")]);
let expected = std::path::Path::new("/home/runner")
.join(".docker")
.to_string_lossy()
.into_owned();
assert_eq!(
env.get("DOCKER_CONFIG").map(String::as_str),
Some(expected.as_str()),
"DOCKER_CONFIG must derive from the real host HOME, not the sealed child HOME"
);
}
#[test]
fn harness_env_passes_through_explicit_docker_config() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(
tmp.path(),
&[
("HOME", "/home/runner"),
("DOCKER_CONFIG", "/custom/docker"),
],
);
assert_eq!(
env.get("DOCKER_CONFIG").map(String::as_str),
Some("/custom/docker"),
"explicit host DOCKER_CONFIG must pass through, overriding the HOME-derived default"
);
}
#[test]
fn harness_env_omits_docker_config_when_underivable() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[]);
assert!(
!env.contains_key("DOCKER_CONFIG"),
"DOCKER_CONFIG must be absent when neither HOME nor DOCKER_CONFIG is set on the host"
);
}
#[test]
fn harness_env_omits_unset_github_vars() {
let tmp = tempfile::tempdir().unwrap();
let all_identity = [
"GITHUB_REPOSITORY",
"GITHUB_SHA",
"GITHUB_REF",
"GITHUB_REF_NAME",
"GITHUB_RUN_ID",
"GITHUB_RUN_NUMBER",
"GITHUB_WORKFLOW",
"GITHUB_ACTOR",
];
let env = build_with(tmp.path(), &[]);
for k in all_identity {
assert!(
!env.contains_key(k),
"unset host var `{k}` must not appear in env (no empty-string default)"
);
}
}
#[test]
fn harness_env_does_not_leak_runner_temp() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("RUNNER_TEMP", "/some/host/tmpdir")]);
assert!(
!env.contains_key("RUNNER_TEMP"),
"RUNNER_TEMP must NOT propagate — harness owns TMPDIR"
);
}
#[test]
fn harness_env_neutralizes_compile_cache_wrapper() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(
tmp.path(),
&[
("RUSTC_WRAPPER", "sccache"),
("RUSTC_WORKSPACE_WRAPPER", "sccache"),
("SCCACHE_GHA_ENABLED", "true"),
],
);
assert_eq!(
env.get("RUSTC_WRAPPER").map(String::as_str),
Some(""),
"RUSTC_WRAPPER must be neutralized to empty so cargo runs no compile cache"
);
assert_eq!(
env.get("RUSTC_WORKSPACE_WRAPPER").map(String::as_str),
Some(""),
"RUSTC_WORKSPACE_WRAPPER must be neutralized to empty"
);
}
#[test]
#[cfg(windows)]
fn harness_env_windows_neutralizes_compile_cache_wrapper() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(
tmp.path(),
&[
("RUSTC_WRAPPER", "sccache"),
("RUSTC_WORKSPACE_WRAPPER", "sccache"),
("SCCACHE_GHA_ENABLED", "true"),
("WINDIR", r"C:\Windows"),
],
);
assert_eq!(
env.get("RUSTC_WRAPPER").map(String::as_str),
Some(""),
"host RUSTC_WRAPPER=sccache must NOT clobber the authoritative empty on Windows"
);
assert_eq!(
env.get("RUSTC_WORKSPACE_WRAPPER").map(String::as_str),
Some(""),
"host RUSTC_WORKSPACE_WRAPPER=sccache must NOT clobber the authoritative empty on Windows"
);
assert!(
!env.contains_key("SCCACHE_GHA_ENABLED"),
"sccache_* config namespace must be dropped by the Windows pass"
);
assert_eq!(
env.get("WINDIR").map(String::as_str),
Some(r"C:\Windows"),
"non-credential host vars must still inherit, proving the passthrough loop ran"
);
}
#[test]
#[cfg(windows)]
fn windows_env_should_drop_compile_cache_wrapper() {
for key in [
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"rustc_wrapper",
"SCCACHE_GHA_ENABLED",
"sccache_dir",
] {
assert!(
windows_env_should_drop(key),
"{key} is a compile-cache wrapper/config var and MUST be dropped"
);
}
assert!(
!windows_env_should_drop("WINDIR"),
"WINDIR is a load-bearing system var and MUST NOT be dropped"
);
}
#[test]
fn harness_env_sets_ci_true_when_host_lacks_it() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[]);
assert_eq!(
env.get("CI").map(String::as_str),
Some("true"),
"harness defaults CI=true when host has no CI var set"
);
}
#[test]
fn harness_env_defaults_rustup_home_from_host_home_when_unset() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("HOME", "/host/home/user")]);
let rh = env
.get("RUSTUP_HOME")
.expect("RUSTUP_HOME must be defaulted when unset")
.replace('\\', "/");
assert_eq!(
rh, "/host/home/user/.rustup",
"harness must default RUSTUP_HOME to <host HOME>/.rustup"
);
}
#[test]
fn harness_env_rustup_home_explicit_wins_over_default() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(
tmp.path(),
&[
("HOME", "/host/home/user"),
("RUSTUP_HOME", "/operator/override"),
],
);
assert_eq!(
env.get("RUSTUP_HOME").map(String::as_str),
Some("/operator/override"),
"an explicit host RUSTUP_HOME must take precedence over the synthesized default"
);
}
#[test]
#[cfg(windows)]
fn harness_env_windows_inherits_host_system_vars() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("PROGRAMFILES", r"C:\fake\Program Files")]);
assert_eq!(
env.get("PROGRAMFILES").map(String::as_str),
Some(r"C:\fake\Program Files"),
"Windows pass must inherit non-credential host system vars (PROGRAMFILES is load-bearing for cc-rs link.exe discovery)"
);
}
#[test]
#[cfg(windows)]
fn harness_env_windows_drops_credentials() {
let tmp = tempfile::tempdir().unwrap();
let keys = [
("GITHUB_TOKEN", "ghp_x"),
("CARGO_REGISTRY_TOKEN", "cratesio_y"),
("SOMETHING_TOKEN", "z"),
("SOMETHING_PASSWORD", "w"),
];
let env = build_with(tmp.path(), &keys);
for (k, _) in keys {
assert!(
!env.contains_key(k),
"credential-bearing host var `{k}` must NOT propagate on Windows"
);
}
for (_, v) in keys {
assert!(
!env.values().any(|got| got == v),
"credential value `{v}` leaked under a different key"
);
}
}
#[test]
#[cfg(windows)]
fn harness_env_windows_drops_actions_workflow_internals() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("ACTIONS_RUNTIME_TOKEN", "actions_x")]);
assert!(
!env.contains_key("ACTIONS_RUNTIME_TOKEN"),
"ACTIONS_* workflow-internal vars must be dropped by the Windows pass"
);
}
#[test]
#[cfg(windows)]
fn harness_env_windows_drops_runner_temp_for_hermeticity() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("RUNNER_TEMP", r"C:\fake\temp")]);
assert!(
!env.contains_key("RUNNER_TEMP"),
"RUNNER_TEMP must NOT propagate on Windows — it points at the runner's on-host scratch and the harness owns TMPDIR"
);
}
#[test]
#[cfg(windows)]
fn harness_env_windows_drops_runner_workspace_for_hermeticity() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("RUNNER_WORKSPACE", r"C:\fake\workspace")]);
assert!(
!env.contains_key("RUNNER_WORKSPACE"),
"RUNNER_WORKSPACE must NOT propagate on Windows — host workflow state, not identity"
);
}
#[test]
#[cfg(windows)]
fn harness_env_windows_drops_github_workspace_for_hermeticity() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("GITHUB_WORKSPACE", r"C:\fake\gh_workspace")]);
assert!(
!env.contains_key("GITHUB_WORKSPACE"),
"GITHUB_WORKSPACE must NOT propagate on Windows — points at the GH-runner-owned checkout, not the hermetic worktree"
);
}
#[test]
fn harness_env_injects_remap_path_prefix_for_worktree() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[]);
let rf = env
.get("RUSTFLAGS")
.expect("RUSTFLAGS must be injected so worktree paths don't leak into the binary");
let needle = format!(
"--remap-path-prefix={}=/anodize",
tmp.path().to_string_lossy()
);
assert!(
rf.contains(&needle),
"RUSTFLAGS must remap the worktree path. got={rf}, expected substring={needle}"
);
assert!(
rf.contains("=/cargo"),
"CARGO_HOME must be remapped to /cargo"
);
assert!(
rf.contains("=/target"),
"CARGO_TARGET_DIR must be remapped to /target"
);
}
#[test]
fn harness_env_preserves_host_rustflags() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(
tmp.path(),
&[("RUSTFLAGS", "-C linker=link.exe -C link-arg=/DEBUG")],
);
let rf = env.get("RUSTFLAGS").unwrap();
assert!(
rf.contains("-C linker=link.exe"),
"host RUSTFLAGS must survive the harness append. got={rf}"
);
assert!(
rf.contains("--remap-path-prefix="),
"remap-path-prefix must be appended even when host RUSTFLAGS is set. got={rf}"
);
}
#[test]
fn harness_env_carries_msvc_flags_via_global_rustflags_not_per_target() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec![
"x86_64-pc-windows-msvc".to_string(),
"aarch64-pc-windows-msvc".to_string(),
];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
let rf = env
.get("RUSTFLAGS")
.expect("global RUSTFLAGS must be set when every resolved target is windows-msvc");
for needle in ["-C link-arg=/Brepro", "-C link-arg=/DEBUG:NONE"] {
assert!(
rf.contains(needle),
"global RUSTFLAGS must carry `{needle}` for an all-msvc target set. got={rf}"
);
}
assert!(
rf.contains("--remap-path-prefix="),
"global RUSTFLAGS must also carry --remap-path-prefix. got={rf}"
);
for triple_env in [
"CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS",
"CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_RUSTFLAGS",
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS",
"CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS",
] {
assert!(
!env.contains_key(triple_env),
"{triple_env} must never be injected — the always-present global \
RUSTFLAGS out-precedences it, so it would be dead. got keys={:?}",
env.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn harness_env_injects_msvc_flags_into_global_rustflags_for_windows_host() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with_host(tmp.path(), &[], true);
let rf = env.get("RUSTFLAGS").expect(
"RUSTFLAGS must be set for a windows-msvc host so host builds (no --target) are reproducible",
);
for needle in [
"-C codegen-units=1",
"-C link-arg=/Brepro",
"-C link-arg=/OPT:NOICF",
"-C link-arg=/INCREMENTAL:NO",
"-C link-arg=/DEBUG:NONE",
"-C strip=symbols",
] {
assert!(
rf.contains(needle),
"global RUSTFLAGS must carry `{needle}` for a windows host. got={rf}"
);
}
assert!(
rf.contains("--remap-path-prefix="),
"global RUSTFLAGS must also carry --remap-path-prefix. got={rf}"
);
assert_eq!(
rf.matches("/Brepro").count(),
1,
"/Brepro must appear exactly once in global RUSTFLAGS. got={rf}"
);
}
#[test]
fn harness_env_omits_global_msvc_flags_for_non_windows_host() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with_host(tmp.path(), &[], false);
if let Some(rf) = env.get("RUSTFLAGS") {
assert!(
!rf.contains("/Brepro"),
"non-windows host must NOT carry MSVC-only /Brepro in global RUSTFLAGS. got={rf}"
);
}
assert!(
!env.contains_key("CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS"),
"no CARGO_TARGET_<triple>_RUSTFLAGS key is ever injected — cargo's \
always-present global RUSTFLAGS out-precedences it"
);
}
#[test]
fn harness_env_injects_global_msvc_flags_when_targets_all_msvc_despite_false_probe() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec!["x86_64-pc-windows-msvc".to_string()];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
let rf = env
.get("RUSTFLAGS")
.expect("global RUSTFLAGS must be set when the build is an all-msvc target set");
for needle in ["-C link-arg=/Brepro", "-C strip=symbols"] {
assert!(
rf.contains(needle),
"all-msvc target set must inject `{needle}` into global RUSTFLAGS \
even when the host probe is false. got={rf}"
);
}
}
#[test]
fn harness_env_omits_global_msvc_flags_for_non_msvc_targets() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec!["x86_64-unknown-linux-gnu".to_string()];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
if let Some(rf) = env.get("RUSTFLAGS") {
assert!(
!rf.contains("/Brepro"),
"a non-msvc target set must NOT carry MSVC-only /Brepro in global RUSTFLAGS. got={rf}"
);
}
}
#[test]
fn harness_env_omits_global_msvc_flags_for_mixed_targets() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec![
"x86_64-pc-windows-msvc".to_string(),
"x86_64-unknown-linux-gnu".to_string(),
];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
if let Some(rf) = env.get("RUSTFLAGS") {
assert!(
!rf.contains("/Brepro"),
"a mixed target set must NOT carry MSVC-only /Brepro in global RUSTFLAGS. got={rf}"
);
}
}
#[test]
fn harness_env_pins_clang_cl_for_msvc_target() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec!["x86_64-pc-windows-msvc".to_string()];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
for key in [
"CC_x86_64-pc-windows-msvc",
"CC_x86_64_pc_windows_msvc",
"CXX_x86_64-pc-windows-msvc",
"CXX_x86_64_pc_windows_msvc",
] {
assert_eq!(
env.get(key).map(String::as_str),
Some("clang-cl"),
"expected {key}=clang-cl in child env, got={env:?}"
);
}
}
#[test]
fn harness_env_omits_clang_cl_for_non_msvc_target() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec!["x86_64-unknown-linux-gnu".to_string()];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
assert!(
!env.keys()
.any(|k| k.starts_with("CC_") || k.starts_with("CXX_")),
"non-msvc target must not carry a CC_/CXX_ pin. got={env:?}"
);
}
#[test]
fn harness_env_pins_clang_cl_for_msvc_member_of_mixed_targets() {
let tmp = tempfile::tempdir().unwrap();
let targets = vec![
"aarch64-pc-windows-msvc".to_string(),
"x86_64-unknown-linux-gnu".to_string(),
];
let mut inputs = inputs(tmp.path());
inputs.targets = &targets;
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
assert_eq!(
env.get("CC_aarch64-pc-windows-msvc").map(String::as_str),
Some("clang-cl")
);
assert_eq!(
env.get("CC_aarch64_pc_windows_msvc").map(String::as_str),
Some("clang-cl")
);
}
#[test]
fn harness_env_omits_clang_cl_for_empty_targets_when_host_not_msvc() {
let tmp = tempfile::tempdir().unwrap();
let inputs = inputs(tmp.path());
let env = build_subprocess_env_with_env(&inputs, &MapEnvSource::new(), false);
assert!(
!env.keys()
.any(|k| k.starts_with("CC_") || k.starts_with("CXX_")),
"host_is_windows_msvc=false must not pin clang-cl. got={env:?}"
);
}
#[test]
#[cfg(windows)]
fn windows_env_should_drop_rustflags_family() {
for key in [
"CARGO_ENCODED_RUSTFLAGS",
"cargo_encoded_rustflags",
"RUSTFLAGS",
"CARGO_BUILD_RUSTFLAGS",
"CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS",
"CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_RUSTFLAGS",
"cargo_target_x86_64_pc_windows_msvc_rustflags",
] {
assert!(
windows_env_should_drop(key),
"{key} is a rustflags-family var that out-precedences /Brepro and MUST be dropped"
);
}
for key in ["CARGO_HOME", "PATH", "CARGO_TARGET_DIR"] {
assert!(
!windows_env_should_drop(key),
"{key} is unrelated to rustflags and MUST NOT be dropped by the rustflags sweep"
);
}
}
#[test]
#[cfg(windows)]
fn harness_env_windows_keeps_runner_os_allow_listed() {
let tmp = tempfile::tempdir().unwrap();
let env = build_with(tmp.path(), &[("RUNNER_OS", "Windows")]);
assert_eq!(
env.get("RUNNER_OS").map(String::as_str),
Some("Windows"),
"RUNNER_OS is on the identity allow-list and MUST propagate even though the namespace gate would otherwise drop it"
);
}
}