use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::time::Instant;
pub const TOOLCHAIN_DIR: &str = "toolchain";
static CACHE_ROOT: RwLock<Option<PathBuf>> = RwLock::new(None);
pub fn set_cache_root(cache_root: &Path) {
*CACHE_ROOT.write().unwrap_or_else(|e| e.into_inner()) = Some(cache_root.to_path_buf());
}
pub fn cache_root() -> Option<PathBuf> {
CACHE_ROOT.read().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn cache_dir() -> Option<PathBuf> {
cache_root().map(|root| root.join(TOOLCHAIN_DIR))
}
#[derive(Serialize, Deserialize)]
struct Entry {
key: String,
value: String,
}
pub fn file_identity(path: &Path) -> String {
match fs::metadata(path) {
Ok(meta) => {
let mtime = meta
.modified()
.ok()
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{}|{}|{}", path.display(), meta.len(), mtime)
}
Err(_) => format!("{}|missing", path.display()),
}
}
pub fn rustup_selection_key(cwd: &Path) -> String {
let env = |name: &str| std::env::var_os(name).map(|v| v.to_string_lossy().into_owned());
let rustup_home = env("RUSTUP_HOME")
.map(PathBuf::from)
.or_else(|| env("HOME").map(|h| PathBuf::from(h).join(".rustup")));
let settings = rustup_home
.map(|h| file_identity(&h.join("settings.toml")))
.unwrap_or_default();
let mut toolchain_file = String::from("none");
for dir in cwd.ancestors() {
for name in ["rust-toolchain.toml", "rust-toolchain"] {
let candidate = dir.join(name);
if let Ok(content) = fs::read(&candidate) {
toolchain_file = format!(
"{}|{}",
candidate.display(),
crate::core::hash::Sha256::digest(&content)
);
break;
}
}
if toolchain_file != "none" {
break;
}
}
format!(
"RUSTUP_TOOLCHAIN={:?}|RUSTUP_HOME={:?}|settings={}|toolchain_file={}",
env("RUSTUP_TOOLCHAIN"),
env("RUSTUP_HOME"),
settings,
toolchain_file
)
}
pub fn is_rustup_proxy(rustc_path: &Path) -> bool {
let Some(dir) = rustc_path.parent() else {
return false;
};
let rustup = dir.join("rustup");
let (Ok(a), Ok(b)) = (fs::metadata(rustc_path), fs::metadata(&rustup)) else {
return false;
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if a.dev() == b.dev() && a.ino() == b.ino() {
return true;
}
}
a.len() == b.len() && a.modified().ok() == b.modified().ok()
}
pub fn rustc_probe_key(rustc_path: &Path, extra: &str) -> String {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
format!(
"{}|{}|{}",
file_identity(rustc_path),
rustup_selection_key(&cwd),
extra
)
}
pub fn load_or_probe<E>(
kind: &str,
key: &str,
cacheable: bool,
probe: impl FnOnce() -> Result<String, E>,
) -> Result<String, E> {
let dir = match cache_dir() {
Some(dir) if cacheable => dir,
_ => return probe(),
};
let file = dir.join(format!(
"{kind}-{}.json",
&crate::core::hash::Sha256::digest(key.as_bytes())[..16]
));
if let Some(value) = read_entry(&file, key) {
return Ok(value);
}
let value = probe()?;
write_entry(&dir, &file, key, &value);
Ok(value)
}
fn read_entry(file: &Path, key: &str) -> Option<String> {
let content = fs::read(file).ok()?;
let entry: Entry = serde_json::from_slice(&content).ok()?;
(entry.key == key).then_some(entry.value)
}
fn write_entry(dir: &Path, file: &Path, key: &str, value: &str) {
let _ = fs::create_dir_all(dir);
let tmp = dir.join(format!(
".{}.{}.tmp",
file.file_name().unwrap_or_default().to_string_lossy(),
std::process::id()
));
let entry = Entry {
key: key.to_string(),
value: value.to_string(),
};
let Ok(bytes) = serde_json::to_vec(&entry) else {
return;
};
if fs::write(&tmp, bytes).is_ok() && fs::rename(&tmp, file).is_err() {
let _ = fs::remove_file(&tmp);
}
}
pub fn rustup_which_rustc(rustup_bin: &Path) -> Option<PathBuf> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let key = format!(
"{}|{}",
file_identity(rustup_bin),
rustup_selection_key(&cwd)
);
let probe = || -> Result<String, ()> {
let t0 = Instant::now();
let out = std::process::Command::new(rustup_bin)
.arg("which")
.arg("rustc")
.output();
super::compiler_resolve::record_rustc_subproc(t0.elapsed());
let out = out.map_err(|_| ())?;
if !out.status.success() {
return Err(());
}
let path = PathBuf::from(String::from_utf8(out.stdout).map_err(|_| ())?.trim());
if !path.is_file() {
return Err(());
}
Ok(format!("{}\n{}", path.display(), file_identity(&path)))
};
let stored = load_or_probe("rustup-which", &key, true, probe).ok()?;
let (path, identity) = stored.split_once('\n')?;
let path = PathBuf::from(path);
if file_identity(&path) == identity {
return Some(path);
}
let fresh = match cache_dir() {
Some(dir) => {
let _ = fs::remove_file(dir.join(format!(
"rustup-which-{}.json",
&crate::core::hash::Sha256::digest(key.as_bytes())[..16]
)));
load_or_probe("rustup-which", &key, true, probe).ok()?
}
None => probe().ok()?,
};
fresh.split_once('\n').map(|(p, _)| PathBuf::from(p))
}
pub fn rustc_verbose_version(rustc_path: &Path) -> Result<String, String> {
load_or_probe(
"rustc-vV",
&rustc_probe_key(rustc_path, "vV"),
!is_rustup_proxy(rustc_path),
|| run_rustc(rustc_path, &["-vV"]),
)
}
pub fn rustc_print_cfg(rustc_path: &Path, target: Option<&str>) -> Result<String, String> {
let mut args = vec!["--print", "cfg"];
if let Some(t) = target {
args.extend(["--target", t]);
}
load_or_probe(
"rustc-cfg",
&rustc_probe_key(rustc_path, &format!("cfg|target={target:?}")),
!is_rustup_proxy(rustc_path),
|| run_rustc(rustc_path, &args),
)
}
fn run_rustc(rustc_path: &Path, args: &[&str]) -> Result<String, String> {
let t0 = Instant::now();
let output = std::process::Command::new(rustc_path).args(args).output();
super::compiler_resolve::record_rustc_subproc(t0.elapsed());
let output = output.map_err(|e| format!("Failed to invoke {rustc_path:?} {args:?}: {e}"))?;
if !output.status.success() {
return Err(format!(
"{rustc_path:?} {args:?} failed with exit code {:?}: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
String::from_utf8(output.stdout)
.map_err(|e| format!("{rustc_path:?} {args:?} output is not valid utf8: {e}"))
}