1use std::io;
2use std::path::{Path, PathBuf};
3use std::process::ExitStatus;
4use std::process::{Command, Stdio};
5#[cfg(target_os = "macos")]
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::mpsc;
8use std::thread;
9
10use crate::parse_trace::{MacroExpansion, MacroExpansionKind, parse_trace};
11
12#[derive(Debug, Clone, Default)]
14pub struct Args {
15 pub package: Option<String>,
16 pub bin: Option<String>,
17 pub lib: bool,
18 pub test: Option<String>,
19 pub example: Option<String>,
20 pub manifest_path: Option<String>,
21 pub cargo_args: Vec<String>,
22 pub hook_lib: PathBuf,
25}
26
27pub struct TraceMacros {
30 cargo_path: PathBuf,
31 args: Args,
32}
33
34pub struct MacroExpansionIter {
37 rx: mpsc::Receiver<io::Result<MacroExpansion>>,
38}
39
40pub struct TraceRun {
42 pub iter: MacroExpansionIter,
43 pub check_result: mpsc::Receiver<io::Result<CheckResult>>,
45}
46
47pub struct CheckResult {
49 pub success: bool,
50 pub stdout: String,
51 pub stderr: String,
52}
53
54impl MacroExpansionIter {
55 #[allow(clippy::result_unit_err)]
61 pub fn try_next(&mut self) -> Result<Option<io::Result<MacroExpansion>>, ()> {
62 match self.rx.try_recv() {
63 Ok(item) => Ok(Some(item)),
64 Err(mpsc::TryRecvError::Empty) => Ok(None),
65 Err(mpsc::TryRecvError::Disconnected) => Err(()),
66 }
67 }
68}
69
70impl Iterator for MacroExpansionIter {
71 type Item = io::Result<MacroExpansion>;
72
73 fn next(&mut self) -> Option<Self::Item> {
74 self.rx.recv().ok()
75 }
76}
77
78const HOOK_LINE_PREFIX: &str = "__MACRA_HOOK__:";
79#[cfg(target_os = "macos")]
80static LINKER_WRAPPER_COUNTER: AtomicU64 = AtomicU64::new(0);
81
82#[derive(serde::Deserialize)]
83struct HookRecord {
84 name: String,
85 kind: String,
86 #[serde(default)]
87 arguments: String,
88 input: String,
89 output: String,
90}
91
92fn parse_hook_json(json: &str) -> Option<MacroExpansion> {
93 let record: HookRecord = serde_json::from_str(json).ok()?;
94 let kind = match record.kind.as_str() {
95 "CustomDerive" => MacroExpansionKind::Derive,
96 "Attr" => MacroExpansionKind::Attribute,
97 _ => MacroExpansionKind::Bang,
98 };
99
100 let expanding = match kind {
101 MacroExpansionKind::Derive => record.name.clone(),
102 MacroExpansionKind::Attribute => {
103 if record.input.contains('(') || record.input.contains('{') {
104 record.input.clone()
105 } else {
106 format!("{} {{ {} }}", record.name, record.input)
107 }
108 }
109 MacroExpansionKind::Bang => record.input.clone(),
110 };
111
112 Some(MacroExpansion {
113 expanding,
114 arguments: record.arguments,
115 to: record.output,
116 name: record.name,
117 kind,
118 input: record.input,
119 })
120}
121
122impl TraceMacros {
123 pub fn new(cargo_path: &Path, args: &Args) -> Self {
124 Self {
125 cargo_path: cargo_path.to_path_buf(),
126 args: args.clone(),
127 }
128 }
129
130 pub fn args(&self) -> &Args {
131 &self.args
132 }
133
134 pub fn run(&self) -> io::Result<TraceRun> {
140 let mut cmd = Command::new(&self.cargo_path);
141 cmd.arg("check");
142 cmd.env("RUSTC_BOOTSTRAP", "1");
143
144 if let Some(ref pkg) = self.args.package {
145 cmd.arg("-p").arg(pkg);
146 }
147 if let Some(ref bin) = self.args.bin {
148 cmd.arg("--bin").arg(bin);
149 }
150 if self.args.lib {
151 cmd.arg("--lib");
152 }
153 if let Some(ref test) = self.args.test {
154 cmd.arg("--test").arg(test);
155 }
156 if let Some(ref example) = self.args.example {
157 cmd.arg("--example").arg(example);
158 }
159 if let Some(ref manifest_path) = self.args.manifest_path {
160 cmd.arg("--manifest-path").arg(manifest_path);
161 }
162
163 for arg in &self.args.cargo_args {
164 cmd.arg(arg);
165 }
166
167 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
169 if !rustflags.is_empty() {
170 rustflags.push(' ');
171 }
172 rustflags.push_str("-Z trace-macros");
173
174 if !self.args.hook_lib.as_os_str().is_empty() {
176 let lib = self
177 .args
178 .hook_lib
179 .canonicalize()
180 .unwrap_or_else(|_| self.args.hook_lib.clone());
181 if cfg!(target_os = "macos") {
182 cmd.env("DYLD_INSERT_LIBRARIES", &lib);
183 #[cfg(target_os = "macos")]
187 if let Ok(wrapper) = create_macos_linker_wrapper() {
188 cmd.env("CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER", &wrapper);
189 cmd.env("CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER", &wrapper);
190 cmd.env("CC", &wrapper);
192 }
193 } else if cfg!(target_os = "windows") {
194 #[cfg(target_os = "windows")]
197 if let Some(wrapper_exe) =
198 crate::find_wrapper_exe(std::env::current_exe().ok().as_deref())
199 {
200 cmd.env("RUSTC_WRAPPER", &wrapper_exe);
201 cmd.env("MACRA_HOOK_DLL_PATH", &lib);
202 }
203 } else {
204 cmd.env("LD_PRELOAD", &lib);
205 }
206
207 rustflags.push_str(" --cfg macra_hook_active");
214
215 }
228
229 cmd.env("RUSTFLAGS", rustflags);
230 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
231
232 let mut child = cmd.spawn()?;
233 let stdout = child
234 .stdout
235 .take()
236 .ok_or_else(|| io::Error::other("failed to capture stdout"))?;
237 let stderr = child
238 .stderr
239 .take()
240 .ok_or_else(|| io::Error::other("failed to capture stderr"))?;
241
242 let (tx, rx) = mpsc::channel();
243 let (status_tx, status_rx) = mpsc::channel();
244
245 let stdout_thread = thread::spawn(move || {
248 use std::io::Read;
249 let mut stdout = stdout;
250 let mut collected = String::new();
251 let mut buf = [0u8; 4096];
252 loop {
253 match stdout.read(&mut buf) {
254 Ok(0) | Err(_) => break,
255 Ok(n) => {
256 collected.push_str(&String::from_utf8_lossy(&buf[..n]));
257 }
258 }
259 }
260 collected
261 });
262
263 thread::spawn(move || {
266 use std::io::BufRead;
267 let reader = io::BufReader::new(stderr);
268 let mut stderr_buf = String::new();
269
270 for line in reader.lines() {
271 let line = match line {
272 Ok(l) => l,
273 Err(e) => {
274 let _ = tx.send(Err(e));
275 break;
276 }
277 };
278 if let Some(json) = line.strip_prefix(HOOK_LINE_PREFIX) {
281 if let Some(expansion) = parse_hook_json(json) {
282 let _ = tx.send(Ok(expansion));
283 }
284 } else {
285 stderr_buf.push_str(&line);
286 stderr_buf.push('\n');
287 }
288 }
289
290 let stdout_buf = stdout_thread.join().unwrap_or_default();
292 let wait_result: io::Result<ExitStatus> = child.wait();
293
294 for group in parse_trace(stderr_buf.as_bytes()) {
296 for expansion in group.expansions {
297 let _ = tx.send(Ok(expansion));
298 }
299 }
300 for group in parse_trace(stdout_buf.as_bytes()) {
301 for expansion in group.expansions {
302 let _ = tx.send(Ok(expansion));
303 }
304 }
305
306 match wait_result {
307 Ok(status) => {
308 let _ = status_tx.send(Ok(CheckResult {
309 success: status.success(),
310 stdout: stdout_buf,
311 stderr: stderr_buf,
312 }));
313 }
314 Err(e) => {
315 let _ = status_tx.send(Err(e));
316 }
317 }
318
319 });
321
322 Ok(TraceRun {
323 iter: MacroExpansionIter { rx },
324 check_result: status_rx,
325 })
326 }
327}
328
329#[cfg(target_os = "macos")]
330fn create_macos_linker_wrapper() -> io::Result<PathBuf> {
331 use std::fs;
332 use std::os::unix::fs::PermissionsExt;
333
334 let unique = LINKER_WRAPPER_COUNTER.fetch_add(1, Ordering::Relaxed);
335 let dir = std::env::temp_dir();
336 let bin_path = dir.join(format!(
337 "cargo-macra-linker-wrapper-{}-{}",
338 std::process::id(),
339 unique
340 ));
341
342 let c_src = concat!(
349 "#include <stdlib.h>\n",
350 "#include <unistd.h>\n",
351 "#include <string.h>\n",
352 "int main(int argc, char *argv[]) {\n",
353 " (void)argc;\n",
354 " unsetenv(\"DYLD_INSERT_LIBRARIES\");\n",
355 " if (argv[1]) {\n",
356 " const char *b = strrchr(argv[1], '/');\n",
357 " if (!b) b = argv[1]; else b++;\n",
358 " if (strcmp(b,\"cc\")==0||strcmp(b,\"clang\")==0||strcmp(b,\"gcc\")==0) {\n",
359 " execvp(argv[1], argv+1);\n",
360 " _exit(127);\n",
361 " }\n",
362 " }\n",
363 " argv[0] = \"/usr/bin/cc\";\n",
364 " execvp(\"/usr/bin/cc\", argv);\n",
365 " _exit(127);\n",
366 "}\n",
367 );
368
369 let src_path = dir.join(format!(
370 "cargo-macra-linker-wrapper-{}-{}.c",
371 std::process::id(),
372 unique
373 ));
374 fs::write(&src_path, c_src)?;
375 let compile = std::process::Command::new("cc")
376 .arg("-o")
377 .arg(&bin_path)
378 .arg(&src_path)
379 .status();
380 let _ = fs::remove_file(&src_path);
381
382 if let Ok(st) = compile {
383 if st.success() {
384 return Ok(bin_path);
385 }
386 }
387
388 let script_path = dir.join(format!(
390 "cargo-macra-linker-wrapper-{}-{}.sh",
391 std::process::id(),
392 unique
393 ));
394 let script = r#"#!/bin/sh
395unset DYLD_INSERT_LIBRARIES
396if [ "$#" -gt 0 ]; then
397 case "$1" in
398 */cc|cc|*/clang|clang|*/gcc|gcc)
399 linker="$1"
400 shift
401 exec "$linker" "$@"
402 ;;
403 esac
404fi
405exec /usr/bin/cc "$@"
406"#;
407 fs::write(&script_path, script)?;
408 fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
409 Ok(script_path)
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn test_parse_hook_json_bang() {
418 let json = r#"{"name":"println","kind":"Bang","arguments":"","input":"println!(\"hello\")","output":"{ ::std::io::_print(format_args!(\"hello\\n\")); }"}"#;
419 let exp = parse_hook_json(json).unwrap();
420 assert_eq!(exp.name, "println");
421 assert_eq!(exp.kind, MacroExpansionKind::Bang);
422 assert_eq!(exp.expanding, "println!(\"hello\")");
423 }
424
425 #[test]
426 fn test_parse_hook_json_derive() {
427 let json = r#"{"name":"Debug","kind":"CustomDerive","arguments":"","input":"struct Foo {}","output":"impl Debug for Foo {}"}"#;
428 let exp = parse_hook_json(json).unwrap();
429 assert_eq!(exp.name, "Debug");
430 assert_eq!(exp.kind, MacroExpansionKind::Derive);
431 assert_eq!(exp.expanding, "Debug");
432 }
433
434 #[test]
435 fn test_parse_hook_json_attribute() {
436 let json = r#"{"name":"test","kind":"Attr","arguments":"","input":"fn foo() {}","output":"fn foo() { /* test */ }"}"#;
438 let exp = parse_hook_json(json).unwrap();
439 assert_eq!(exp.name, "test");
440 assert_eq!(exp.kind, MacroExpansionKind::Attribute);
441 assert_eq!(exp.expanding, "fn foo() {}");
442 }
443
444 #[test]
445 fn test_parse_hook_json_attribute_simple_input() {
446 let json = r#"{"name":"cfg","kind":"Attr","arguments":"","input":"feature = \"foo\"","output":""}"#;
448 let exp = parse_hook_json(json).unwrap();
449 assert_eq!(exp.name, "cfg");
450 assert_eq!(exp.kind, MacroExpansionKind::Attribute);
451 assert_eq!(exp.expanding, "cfg { feature = \"foo\" }");
452 }
453
454 #[test]
455 fn test_parse_hook_json_invalid() {
456 assert!(parse_hook_json("not json").is_none());
457 }
458}