use std::path::{Path, PathBuf};
pub fn windows_kits_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Ok(v) = std::env::var("DAY_WINDOWS_KITS_ROOT") {
roots.push(PathBuf::from(v));
}
if let Ok(v) = std::env::var("WindowsSdkDir") {
roots.push(PathBuf::from(v));
}
for pf in ["ProgramFiles(x86)", "ProgramFiles"] {
if let Ok(v) = std::env::var(pf) {
roots.push(PathBuf::from(v).join("Windows Kits").join("10"));
}
}
roots.dedup();
roots
}
pub fn cppwinrt_include() -> Option<PathBuf> {
if let Ok(v) = std::env::var("DAY_CPPWINRT") {
let p = PathBuf::from(v);
if p.join("winrt").join("base.h").exists() {
return Some(p);
}
return None;
}
let mut found: Vec<PathBuf> = Vec::new();
for root in windows_kits_roots() {
let Ok(rd) = std::fs::read_dir(root.join("Include")) else {
continue;
};
for entry in rd.flatten() {
let cppwinrt = entry.path().join("cppwinrt");
if cppwinrt.join("winrt").join("base.h").exists() {
found.push(cppwinrt);
}
}
}
found.sort(); found.pop()
}
pub fn cppwinrt_include_for_build_script() -> Option<PathBuf> {
for var in ["DAY_CPPWINRT", "DAY_WINDOWS_KITS_ROOT", "WindowsSdkDir"] {
println!("cargo:rerun-if-env-changed={var}");
}
cppwinrt_include()
}
pub fn windows_kit_tool(tool: &str) -> Option<PathBuf> {
if let Ok(root) = std::env::var("DAY_WINDOWS_KIT") {
let p = PathBuf::from(root).join(tool);
if p.exists() {
return Some(p);
}
}
if let Some(p) = on_path(tool) {
return Some(p);
}
let arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"x64"
};
for root in windows_kits_roots() {
let Ok(rd) = std::fs::read_dir(root.join("bin")) else {
continue;
};
let mut versions: Vec<PathBuf> = rd
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with("10."))
})
.collect();
versions.sort();
while let Some(v) = versions.pop() {
let candidate = v.join(arch).join(tool);
if candidate.exists() {
return Some(candidate);
}
}
}
None
}
pub fn makensis() -> Option<PathBuf> {
if let Ok(v) = std::env::var("DAY_MAKENSIS") {
let p = PathBuf::from(v);
if p.is_file() {
return Some(p);
}
return None; }
if let Some(p) = on_path("makensis").or_else(|| on_path("makensis.exe")) {
return Some(p);
}
for pf in ["ProgramFiles(x86)", "ProgramFiles"] {
if let Ok(v) = std::env::var(pf) {
let p = PathBuf::from(v).join("NSIS").join("makensis.exe");
if p.exists() {
return Some(p);
}
}
}
let choco = std::env::var("ChocolateyInstall")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(r"C:\ProgramData\chocolatey"));
let shim = choco.join("bin").join("makensis.exe");
if shim.is_file() {
return Some(shim);
}
let tools = choco.join("lib").join("nsis").join("tools");
if let Ok(entries) = std::fs::read_dir(&tools) {
for entry in entries.flatten() {
for candidate in [
entry.path().join("makensis.exe"),
entry.path().join("Bin").join("makensis.exe"),
] {
if candidate.is_file() {
return Some(candidate);
}
}
}
}
None
}
pub fn android_sdk_dir() -> PathBuf {
if let Ok(v) = std::env::var("ANDROID_HOME").or_else(|_| std::env::var("ANDROID_SDK_ROOT")) {
return PathBuf::from(v);
}
if cfg!(target_os = "windows")
&& let Ok(v) = std::env::var("LOCALAPPDATA")
{
return PathBuf::from(v).join("Android").join("Sdk");
}
let home = PathBuf::from(std::env::var("HOME").unwrap_or_default());
if cfg!(target_os = "macos") {
home.join("Library/Android/sdk")
} else {
home.join("Android/Sdk")
}
}
pub fn jdk_home() -> Option<PathBuf> {
if let Ok(v) = std::env::var("JAVA_HOME") {
return Some(PathBuf::from(v));
}
if cfg!(target_os = "macos") {
if let Ok(out) = std::process::Command::new("/usr/libexec/java_home")
.args(["-v", "17+"])
.output()
&& out.status.success()
{
let p = PathBuf::from(String::from_utf8_lossy(&out.stdout).trim());
if p.join("bin/java").exists() {
return Some(p);
}
}
for keg in ["openjdk", "openjdk@21", "openjdk@17"] {
for prefix in ["/opt/homebrew", "/usr/local"] {
let p = PathBuf::from(prefix).join("opt").join(keg);
if p.join("bin/java").exists() {
return Some(p);
}
}
}
}
None
}
pub fn rustup_cargo() -> Result<(PathBuf, PathBuf), String> {
let rustup_home = std::env::var("RUSTUP_HOME")
.map(PathBuf::from)
.or_else(|_| {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".rustup"))
.map_err(|e| e.to_string())
})?;
let toolchains = rustup_home.join("toolchains");
let mut entries: Vec<PathBuf> = std::fs::read_dir(&toolchains)
.map_err(|_| "no rustup toolchains (cross-std needs rustup, not Homebrew rust)")?
.flatten()
.map(|e| e.path())
.collect();
entries.sort();
let chosen = entries
.iter()
.find(|p| {
p.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with("stable-"))
})
.or_else(|| entries.first())
.ok_or("empty rustup toolchains dir")?;
let bin = chosen.join("bin");
Ok((bin.join("cargo"), bin))
}
fn on_path(tool: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|d| d.join(tool))
.find(|p| p.is_file())
}
pub fn is_dir(dir: &Path) -> bool {
dir.is_dir()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kits_roots_honor_override_first() {
unsafe { std::env::set_var("DAY_WINDOWS_KITS_ROOT", "/custom/kits/10") };
let roots = windows_kits_roots();
assert_eq!(roots[0], PathBuf::from("/custom/kits/10"));
unsafe { std::env::remove_var("DAY_WINDOWS_KITS_ROOT") };
}
#[test]
fn android_sdk_honors_android_home() {
unsafe { std::env::set_var("ANDROID_HOME", "/custom/android") };
assert_eq!(android_sdk_dir(), PathBuf::from("/custom/android"));
unsafe { std::env::remove_var("ANDROID_HOME") };
}
#[test]
fn explicit_cppwinrt_override_must_validate() {
unsafe { std::env::set_var("DAY_CPPWINRT", "/does/not/exist") };
assert_eq!(cppwinrt_include(), None); unsafe { std::env::remove_var("DAY_CPPWINRT") };
}
#[test]
fn explicit_makensis_override_must_validate() {
unsafe { std::env::set_var("DAY_MAKENSIS", "/does/not/exist/makensis.exe") };
assert_eq!(makensis(), None); unsafe { std::env::remove_var("DAY_MAKENSIS") };
}
#[test]
fn makensis_found_in_chocolatey_layouts() {
let base = std::env::temp_dir().join(format!("day-choco-probe-{}", std::process::id()));
let shimmed = base.join("shim");
let unpacked = base.join("unpacked");
let nested = base.join("nested");
let _ = std::fs::remove_dir_all(&base);
let shim_exe = shimmed.join("bin").join("makensis.exe");
std::fs::create_dir_all(shim_exe.parent().unwrap()).unwrap();
std::fs::write(&shim_exe, b"").unwrap();
let flat = unpacked
.join("lib/nsis/tools")
.join("nsis-3.10")
.join("makensis.exe");
std::fs::create_dir_all(flat.parent().unwrap()).unwrap();
std::fs::write(&flat, b"").unwrap();
let deep = nested
.join("lib/nsis/tools")
.join("nsis-3.10")
.join("Bin")
.join("makensis.exe");
std::fs::create_dir_all(deep.parent().unwrap()).unwrap();
std::fs::write(&deep, b"").unwrap();
let (path, pf, pf86) = (
std::env::var_os("PATH"),
std::env::var_os("ProgramFiles"),
std::env::var_os("ProgramFiles(x86)"),
);
unsafe {
std::env::remove_var("DAY_MAKENSIS");
std::env::set_var("PATH", "");
std::env::set_var("ProgramFiles", base.join("no-such-pf"));
std::env::set_var("ProgramFiles(x86)", base.join("no-such-pf86"));
}
let found: Vec<_> = [(&shimmed, &shim_exe), (&unpacked, &flat), (&nested, &deep)]
.iter()
.map(|(root, want)| {
unsafe { std::env::set_var("ChocolateyInstall", root) };
(makensis(), (*want).clone())
})
.collect();
unsafe {
std::env::remove_var("ChocolateyInstall");
match path {
Some(v) => std::env::set_var("PATH", v),
None => std::env::remove_var("PATH"),
}
match pf {
Some(v) => std::env::set_var("ProgramFiles", v),
None => std::env::remove_var("ProgramFiles"),
}
match pf86 {
Some(v) => std::env::set_var("ProgramFiles(x86)", v),
None => std::env::remove_var("ProgramFiles(x86)"),
}
}
let _ = std::fs::remove_dir_all(&base);
for (got, want) in found {
assert_eq!(got.as_ref(), Some(&want), "chocolatey layout {want:?}");
}
}
}