use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct CapturedRustcInvocation {
pub crate_name: String,
pub args: Vec<String>,
pub timestamp_micros: u128,
}
pub fn run() -> Result<()> {
let mut argv: Vec<String> = std::env::args().collect();
if argv.len() < 2 {
anyhow::bail!(
"whisker-rustc-shim: expected `<wrapper> <rustc-path> [rustc-args...]`, \
got {} arg(s)",
argv.len(),
);
}
let _wrapper = argv.remove(0); let real_rustc = argv.remove(0); let rustc_args = argv;
if let Some(cache_dir) = std::env::var_os("WHISKER_RUSTC_CACHE_DIR") {
let cache_dir = PathBuf::from(cache_dir);
let invocation = capture(&rustc_args)?;
save_invocation(&cache_dir, &invocation)
.with_context(|| format!("save to {}", cache_dir.display()))?;
}
let status = std::process::Command::new(&real_rustc)
.args(&rustc_args)
.status()
.with_context(|| format!("spawn {real_rustc}"))?;
std::process::exit(status.code().unwrap_or(1));
}
pub fn capture(rustc_args: &[String]) -> Result<CapturedRustcInvocation> {
Ok(CapturedRustcInvocation {
crate_name: extract_crate_name(rustc_args).unwrap_or_default(),
args: rustc_args.to_vec(),
timestamp_micros: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros())
.unwrap_or(0),
})
}
pub fn extract_crate_name(args: &[String]) -> Option<String> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--crate-name" {
return iter.next().cloned();
}
if let Some(rest) = arg.strip_prefix("--crate-name=") {
return Some(rest.to_string());
}
}
None
}
pub fn invocation_filename(invocation: &CapturedRustcInvocation) -> String {
let crate_for_path = if invocation.crate_name.is_empty() {
"_unknown"
} else {
invocation.crate_name.as_str()
};
format!(
"{}-{}.json",
crate_for_path.replace(['-', '/'], "_"),
invocation.timestamp_micros,
)
}
pub fn save_invocation(cache_dir: &Path, invocation: &CapturedRustcInvocation) -> Result<()> {
std::fs::create_dir_all(cache_dir)
.with_context(|| format!("create {}", cache_dir.display()))?;
let path = cache_dir.join(invocation_filename(invocation));
let json = serde_json::to_string_pretty(invocation).context("serialize")?;
std::fs::write(&path, json).with_context(|| format!("write {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn unique_tempdir() -> PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let p = std::env::temp_dir().join(format!("whisker-rustc-shim-test-{pid}-{n}"));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn extract_crate_name_from_separated_form() {
let args = s(&[
"--edition=2021",
"--crate-name",
"hello_world",
"--out-dir",
"x",
]);
assert_eq!(extract_crate_name(&args).as_deref(), Some("hello_world"));
}
#[test]
fn extract_crate_name_from_equals_form() {
let args = s(&["--crate-name=foo_bar", "--edition=2021"]);
assert_eq!(extract_crate_name(&args).as_deref(), Some("foo_bar"));
}
#[test]
fn extract_crate_name_returns_none_when_absent() {
let args = s(&["--edition=2021", "--out-dir", "x"]);
assert_eq!(extract_crate_name(&args), None);
}
#[test]
fn extract_crate_name_first_occurrence_wins() {
let args = s(&["--crate-name", "first", "--crate-name", "second"]);
assert_eq!(extract_crate_name(&args).as_deref(), Some("first"));
}
#[test]
fn capture_includes_full_argv_unchanged() {
let argv = s(&[
"--crate-name",
"demo",
"--edition=2021",
"src/lib.rs",
"-C",
"opt-level=0",
]);
let inv = capture(&argv).unwrap();
assert_eq!(inv.args, argv);
assert_eq!(inv.crate_name, "demo");
assert!(inv.timestamp_micros > 0);
}
#[test]
fn capture_with_no_crate_name_leaves_field_empty() {
let inv = capture(&s(&["--edition=2021", "src/lib.rs"])).unwrap();
assert_eq!(inv.crate_name, "");
}
#[test]
fn invocation_filename_uses_underscored_crate_name_and_timestamp() {
let inv = CapturedRustcInvocation {
crate_name: "hello-world".into(),
args: vec![],
timestamp_micros: 1_000_000,
};
assert_eq!(invocation_filename(&inv), "hello_world-1000000.json");
}
#[test]
fn invocation_filename_handles_anonymous_crate() {
let inv = CapturedRustcInvocation {
crate_name: "".into(),
args: vec![],
timestamp_micros: 42,
};
assert_eq!(invocation_filename(&inv), "_unknown-42.json");
}
#[test]
fn invocation_filename_strips_path_separators() {
let inv = CapturedRustcInvocation {
crate_name: "weird/name".into(),
args: vec![],
timestamp_micros: 7,
};
assert_eq!(invocation_filename(&inv), "weird_name-7.json");
}
#[test]
fn save_invocation_writes_a_readable_json_file() {
let dir = unique_tempdir();
let inv = CapturedRustcInvocation {
crate_name: "x".into(),
args: s(&["--crate-name", "x", "src/lib.rs"]),
timestamp_micros: 12345,
};
save_invocation(&dir, &inv).expect("save");
let path = dir.join(invocation_filename(&inv));
assert!(
path.is_file(),
"json file should exist at {}",
path.display()
);
let body = std::fs::read_to_string(&path).unwrap();
let parsed: CapturedRustcInvocation = serde_json::from_str(&body).unwrap();
assert_eq!(parsed, inv);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn save_invocation_creates_the_cache_dir_if_missing() {
let dir = unique_tempdir().join("nested/does/not/exist");
assert!(!dir.exists());
let inv = CapturedRustcInvocation {
crate_name: "x".into(),
args: vec![],
timestamp_micros: 1,
};
save_invocation(&dir, &inv).expect("save");
assert!(dir.is_dir());
let mut to_remove = dir;
for _ in 0..4 {
to_remove.pop();
}
let _ = std::fs::remove_dir_all(&to_remove);
}
}