whisker_cli/
rustc_shim.rs1use anyhow::{Context, Result};
23use std::path::{Path, PathBuf};
24use std::time::{SystemTime, UNIX_EPOCH};
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
30pub struct CapturedRustcInvocation {
31 pub crate_name: String,
35 pub args: Vec<String>,
39 pub timestamp_micros: u128,
43}
44
45pub fn run() -> Result<()> {
47 let mut argv: Vec<String> = std::env::args().collect();
48 if argv.len() < 2 {
49 anyhow::bail!(
50 "whisker-rustc-shim: expected `<wrapper> <rustc-path> [rustc-args...]`, \
51 got {} arg(s)",
52 argv.len(),
53 );
54 }
55 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") {
61 let cache_dir = PathBuf::from(cache_dir);
62 let invocation = capture(&rustc_args)?;
63 save_invocation(&cache_dir, &invocation)
64 .with_context(|| format!("save to {}", cache_dir.display()))?;
65 }
66
67 let status = std::process::Command::new(&real_rustc)
69 .args(&rustc_args)
70 .status()
71 .with_context(|| format!("spawn {real_rustc}"))?;
72 std::process::exit(status.code().unwrap_or(1));
73}
74
75pub fn capture(rustc_args: &[String]) -> Result<CapturedRustcInvocation> {
80 Ok(CapturedRustcInvocation {
81 crate_name: extract_crate_name(rustc_args).unwrap_or_default(),
82 args: rustc_args.to_vec(),
83 timestamp_micros: SystemTime::now()
84 .duration_since(UNIX_EPOCH)
85 .map(|d| d.as_micros())
86 .unwrap_or(0),
87 })
88}
89
90pub fn extract_crate_name(args: &[String]) -> Option<String> {
95 let mut iter = args.iter();
96 while let Some(arg) = iter.next() {
97 if arg == "--crate-name" {
98 return iter.next().cloned();
99 }
100 if let Some(rest) = arg.strip_prefix("--crate-name=") {
101 return Some(rest.to_string());
102 }
103 }
104 None
105}
106
107pub fn invocation_filename(invocation: &CapturedRustcInvocation) -> String {
111 let crate_for_path = if invocation.crate_name.is_empty() {
112 "_unknown"
113 } else {
114 invocation.crate_name.as_str()
115 };
116 format!(
117 "{}-{}.json",
118 crate_for_path.replace(['-', '/'], "_"),
119 invocation.timestamp_micros,
120 )
121}
122
123pub fn save_invocation(cache_dir: &Path, invocation: &CapturedRustcInvocation) -> Result<()> {
126 std::fs::create_dir_all(cache_dir)
127 .with_context(|| format!("create {}", cache_dir.display()))?;
128 let path = cache_dir.join(invocation_filename(invocation));
129 let json = serde_json::to_string_pretty(invocation).context("serialize")?;
130 std::fs::write(&path, json).with_context(|| format!("write {}", path.display()))?;
131 Ok(())
132}
133
134#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::sync::atomic::{AtomicU64, Ordering};
142
143 fn s(v: &[&str]) -> Vec<String> {
144 v.iter().map(|s| s.to_string()).collect()
145 }
146
147 fn unique_tempdir() -> PathBuf {
148 static SEQ: AtomicU64 = AtomicU64::new(0);
149 let n = SEQ.fetch_add(1, Ordering::Relaxed);
150 let pid = std::process::id();
151 let p = std::env::temp_dir().join(format!("whisker-rustc-shim-test-{pid}-{n}"));
152 let _ = std::fs::remove_dir_all(&p);
153 std::fs::create_dir_all(&p).unwrap();
154 p
155 }
156
157 #[test]
160 fn extract_crate_name_from_separated_form() {
161 let args = s(&[
162 "--edition=2021",
163 "--crate-name",
164 "hello_world",
165 "--out-dir",
166 "x",
167 ]);
168 assert_eq!(extract_crate_name(&args).as_deref(), Some("hello_world"));
169 }
170
171 #[test]
172 fn extract_crate_name_from_equals_form() {
173 let args = s(&["--crate-name=foo_bar", "--edition=2021"]);
174 assert_eq!(extract_crate_name(&args).as_deref(), Some("foo_bar"));
175 }
176
177 #[test]
178 fn extract_crate_name_returns_none_when_absent() {
179 let args = s(&["--edition=2021", "--out-dir", "x"]);
180 assert_eq!(extract_crate_name(&args), None);
181 }
182
183 #[test]
184 fn extract_crate_name_first_occurrence_wins() {
185 let args = s(&["--crate-name", "first", "--crate-name", "second"]);
188 assert_eq!(extract_crate_name(&args).as_deref(), Some("first"));
189 }
190
191 #[test]
194 fn capture_includes_full_argv_unchanged() {
195 let argv = s(&[
196 "--crate-name",
197 "demo",
198 "--edition=2021",
199 "src/lib.rs",
200 "-C",
201 "opt-level=0",
202 ]);
203 let inv = capture(&argv).unwrap();
204 assert_eq!(inv.args, argv);
205 assert_eq!(inv.crate_name, "demo");
206 assert!(inv.timestamp_micros > 0);
207 }
208
209 #[test]
210 fn capture_with_no_crate_name_leaves_field_empty() {
211 let inv = capture(&s(&["--edition=2021", "src/lib.rs"])).unwrap();
212 assert_eq!(inv.crate_name, "");
213 }
214
215 #[test]
218 fn invocation_filename_uses_underscored_crate_name_and_timestamp() {
219 let inv = CapturedRustcInvocation {
220 crate_name: "hello-world".into(),
221 args: vec![],
222 timestamp_micros: 1_000_000,
223 };
224 assert_eq!(invocation_filename(&inv), "hello_world-1000000.json");
225 }
226
227 #[test]
228 fn invocation_filename_handles_anonymous_crate() {
229 let inv = CapturedRustcInvocation {
230 crate_name: "".into(),
231 args: vec![],
232 timestamp_micros: 42,
233 };
234 assert_eq!(invocation_filename(&inv), "_unknown-42.json");
235 }
236
237 #[test]
238 fn invocation_filename_strips_path_separators() {
239 let inv = CapturedRustcInvocation {
242 crate_name: "weird/name".into(),
243 args: vec![],
244 timestamp_micros: 7,
245 };
246 assert_eq!(invocation_filename(&inv), "weird_name-7.json");
247 }
248
249 #[test]
252 fn save_invocation_writes_a_readable_json_file() {
253 let dir = unique_tempdir();
254 let inv = CapturedRustcInvocation {
255 crate_name: "x".into(),
256 args: s(&["--crate-name", "x", "src/lib.rs"]),
257 timestamp_micros: 12345,
258 };
259 save_invocation(&dir, &inv).expect("save");
260
261 let path = dir.join(invocation_filename(&inv));
262 assert!(
263 path.is_file(),
264 "json file should exist at {}",
265 path.display()
266 );
267
268 let body = std::fs::read_to_string(&path).unwrap();
269 let parsed: CapturedRustcInvocation = serde_json::from_str(&body).unwrap();
270 assert_eq!(parsed, inv);
271
272 let _ = std::fs::remove_dir_all(&dir);
273 }
274
275 #[test]
276 fn save_invocation_creates_the_cache_dir_if_missing() {
277 let dir = unique_tempdir().join("nested/does/not/exist");
278 assert!(!dir.exists());
279 let inv = CapturedRustcInvocation {
280 crate_name: "x".into(),
281 args: vec![],
282 timestamp_micros: 1,
283 };
284 save_invocation(&dir, &inv).expect("save");
285 assert!(dir.is_dir());
286
287 let mut to_remove = dir;
289 for _ in 0..4 {
290 to_remove.pop();
291 }
292 let _ = std::fs::remove_dir_all(&to_remove);
293 }
294}