use std::ffi::{OsStr, OsString};
use std::process::Command;
use find_msvc_tools::{Env, EnvGetter};
const SEARCH_LISTS: [&str; 3] = ["PATH", "LIB", "INCLUDE"];
pub(super) fn prepend_search_directories(command: &mut Command) {
let declared = Declared(
command
.get_envs()
.filter_map(|(name, value)| Some((name.to_os_string(), value?.to_os_string())))
.collect(),
);
if declared.value("VCINSTALLDIR").is_some() {
return;
}
let Some(linker) =
find_msvc_tools::find_tool_with_env(std::env::consts::ARCH, "link.exe", &declared)
else {
return;
};
for (name, directories) in linker.env() {
if !is_search_list(name) {
continue;
}
let joined = std::env::split_paths(directories)
.map(std::path::PathBuf::into_os_string)
.chain(declared.value(name).map(OsStr::to_os_string))
.filter(|entry| !entry.is_empty())
.fold(OsString::new(), |mut joined, entry| {
if !joined.is_empty() {
joined.push(";");
}
joined.push(entry);
joined
});
command.env(name, joined);
}
}
fn is_search_list(name: impl AsRef<OsStr>) -> bool {
SEARCH_LISTS
.iter()
.any(|list| name.as_ref().eq_ignore_ascii_case(list))
}
struct Declared(Vec<(OsString, OsString)>);
impl Declared {
fn value(&self, name: impl AsRef<OsStr>) -> Option<&OsStr> {
self.0
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name.as_ref()))
.map(|(_, value)| value.as_os_str())
}
}
impl EnvGetter for Declared {
fn get_env(&self, name: &'static str) -> Option<Env> {
if is_search_list(name) {
return None;
}
self.value(name)
.map(|value| Env::Owned(value.to_os_string()))
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
reason = "test code"
)]
mod tests {
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::Command;
fn declared<'a>(command: &'a Command, name: &str) -> Vec<&'a OsStr> {
command
.get_envs()
.filter(|(key, _)| key.eq_ignore_ascii_case(name))
.filter_map(|(_, value)| value)
.collect()
}
#[test]
fn the_linker_directory_precedes_the_declared_search_path() {
let mut command = Command::new("cmd.exe");
command.env_clear().env("Path", r"C:\declared");
super::prepend_search_directories(&mut command);
let path = declared(&command, "PATH");
assert_eq!(path.len(), 1, "one PATH under every spelling: {path:?}");
let directories: Vec<PathBuf> = std::env::split_paths(path[0]).collect();
assert!(
directories
.first()
.is_some_and(|first| first.join("link.exe").is_file()),
"MSVC's link.exe directory leads PATH: {directories:?}"
);
assert_eq!(directories.last(), Some(&PathBuf::from(r"C:\declared")));
assert_eq!(declared(&command, "LIB").len(), 1);
}
#[test]
fn a_declared_developer_prompt_is_left_alone() {
let mut command = Command::new("cmd.exe");
command
.env_clear()
.env("VCINSTALLDIR", r"C:\vc\")
.env("PATH", r"C:\declared");
super::prepend_search_directories(&mut command);
assert_eq!(declared(&command, "PATH"), [OsStr::new(r"C:\declared")]);
assert!(declared(&command, "LIB").is_empty());
}
}