Skip to main content

compiletest/runtest/
debuginfo.rs

1use std::ffi::{OsStr, OsString};
2use std::io::{BufRead, BufReader};
3use std::process::{Command, Output, Stdio};
4
5use camino::Utf8Path;
6use tracing::debug;
7
8use super::debugger::DebuggerCommands;
9use super::{Debugger, Emit, ProcRes, TestCx, Truncated, WillExecute};
10use crate::debuggers::extract_gdb_version;
11
12impl TestCx<'_> {
13    pub(super) fn run_debuginfo_test(&self) {
14        match self.config.debugger.unwrap() {
15            Debugger::Cdb => self.run_debuginfo_cdb_test(),
16            Debugger::Gdb => self.run_debuginfo_gdb_test(),
17            Debugger::Lldb => self.run_debuginfo_lldb_test(),
18        }
19    }
20
21    fn run_debuginfo_cdb_test(&self) {
22        let exe_file = self.make_exe_name();
23
24        // Existing PDB files are update in-place. When changing the debuginfo
25        // the compiler generates for something, this can lead to the situation
26        // where both the old and the new version of the debuginfo for the same
27        // type is present in the PDB, which is very confusing.
28        // Therefore we delete any existing PDB file before compiling the test
29        // case.
30        // FIXME: If can reliably detect that MSVC's link.exe is used, then
31        //        passing `/INCREMENTAL:NO` might be a cleaner way to do this.
32        let pdb_file = exe_file.with_extension(".pdb");
33        if pdb_file.exists() {
34            std::fs::remove_file(pdb_file).unwrap();
35        }
36
37        // compile test file (it should have 'compile-flags:-g' in the directive)
38        let should_run = self.run_if_enabled();
39        let compile_result = self.compile_test(should_run, Emit::None);
40        if !compile_result.status.success() {
41            self.fatal_proc_rec("compilation failed!", &compile_result);
42        }
43        if let WillExecute::Disabled = should_run {
44            return;
45        }
46
47        // Parse debugger commands etc from test files
48        let dbg_cmds = DebuggerCommands::parse_from(&self.testpaths.file, "cdb", self.revision)
49            .unwrap_or_else(|e| self.fatal(&e));
50
51        // https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-commands
52        let mut script_str = String::with_capacity(2048);
53        script_str.push_str("version\n"); // List CDB (and more) version info in test output
54        script_str.push_str(".nvlist\n"); // List loaded `*.natvis` files, bulk of custom MSVC debug
55
56        // If a .js file exists next to the source file being tested, then this is a JavaScript
57        // debugging extension that needs to be loaded.
58        let mut js_extension = self.testpaths.file.clone();
59        js_extension.set_extension("cdb.js");
60        if js_extension.exists() {
61            script_str.push_str(&format!(".scriptload \"{}\"\n", js_extension));
62        }
63
64        // Set breakpoints on every line that contains the string "#break"
65        let source_file_name = self.testpaths.file.file_name().unwrap();
66        for line in &dbg_cmds.breakpoint_lines {
67            script_str.push_str(&format!("bp `{}:{}`\n", source_file_name, line));
68        }
69
70        // Append the other `cdb-command:`s
71        for line in &dbg_cmds.commands {
72            script_str.push_str(line);
73            script_str.push('\n');
74        }
75
76        script_str.push_str("qq\n"); // Quit the debugger (including remote debugger, if any)
77
78        // Write the script into a file
79        debug!("script_str = {}", script_str);
80        self.dump_output_file(&script_str, "debugger.script");
81        let debugger_script = self.make_out_name("debugger.script");
82
83        let cdb_path = &self.config.cdb.as_ref().unwrap();
84        let mut cdb = Command::new(cdb_path);
85        cdb.arg("-lines") // Enable source line debugging.
86            .arg("-cf")
87            .arg(&debugger_script)
88            .arg(&exe_file);
89
90        let debugger_run_result = self.compose_and_run(
91            cdb,
92            self.config.target_run_lib_path.as_path(),
93            None, // aux_path
94            None, // input
95        );
96
97        if !debugger_run_result.status.success() {
98            self.fatal_proc_rec("Error while running CDB", &debugger_run_result);
99        }
100
101        if let Err(e) = dbg_cmds.check_output(&debugger_run_result) {
102            self.fatal_proc_rec(&e, &debugger_run_result);
103        }
104    }
105
106    fn run_debuginfo_gdb_test(&self) {
107        let dbg_cmds = DebuggerCommands::parse_from(&self.testpaths.file, "gdb", self.revision)
108            .unwrap_or_else(|e| self.fatal(&e));
109        let mut cmds = dbg_cmds.commands.join("\n");
110
111        // compile test file (it should have 'compile-flags:-g' in the directive)
112        let should_run = self.run_if_enabled();
113        let compiler_run_result = self.compile_test(should_run, Emit::None);
114        if !compiler_run_result.status.success() {
115            self.fatal_proc_rec("compilation failed!", &compiler_run_result);
116        }
117        if let WillExecute::Disabled = should_run {
118            return;
119        }
120
121        let exe_file = self.make_exe_name();
122
123        let debugger_run_result;
124        // If bootstrap gave us an `--android-cross-path`, assume the target
125        // needs Android-specific handling.
126        if let Some(android_cross_path) = self.config.android_cross_path.as_deref() {
127            cmds = cmds.replace("run", "continue");
128
129            // write debugger script
130            let mut script_str = String::with_capacity(2048);
131            script_str.push_str(&format!("set charset {}\n", Self::charset()));
132            script_str.push_str(&format!("set sysroot {android_cross_path}\n"));
133            script_str.push_str(&format!("file {}\n", exe_file));
134            script_str.push_str("target remote :5039\n");
135            script_str.push_str(&format!(
136                "set solib-search-path \
137                 ./{}/stage2/lib/rustlib/{}/lib/\n",
138                self.config.host, self.config.target
139            ));
140            for line in &dbg_cmds.breakpoint_lines {
141                script_str.push_str(
142                    format!("break {}:{}\n", self.testpaths.file.file_name().unwrap(), *line)
143                        .as_str(),
144                );
145            }
146            script_str.push_str(&cmds);
147            script_str.push_str("\nquit\n");
148
149            debug!("script_str = {}", script_str);
150            self.dump_output_file(&script_str, "debugger.script");
151
152            // Note: when `--android-cross-path` is specified, we expect both `adb_path` and
153            // `adb_test_dir` to be available.
154            let adb_path = self.config.adb_path.as_ref().expect("`adb_path` must be specified");
155            let adb_test_dir =
156                self.config.adb_test_dir.as_ref().expect("`adb_test_dir` must be specified");
157
158            Command::new(adb_path)
159                .arg("push")
160                .arg(&exe_file)
161                .arg(adb_test_dir)
162                .status()
163                .unwrap_or_else(|e| panic!("failed to exec `{adb_path:?}`: {e:?}"));
164
165            Command::new(adb_path)
166                .args(&["forward", "tcp:5039", "tcp:5039"])
167                .status()
168                .unwrap_or_else(|e| panic!("failed to exec `{adb_path:?}`: {e:?}"));
169
170            let adb_arg = format!(
171                "export LD_LIBRARY_PATH={}; \
172                 gdbserver{} :5039 {}/{}",
173                adb_test_dir,
174                if self.config.target.contains("aarch64") { "64" } else { "" },
175                adb_test_dir,
176                exe_file.file_name().unwrap()
177            );
178
179            debug!("adb arg: {}", adb_arg);
180            let mut adb = Command::new(adb_path)
181                .args(&["shell", &adb_arg])
182                .stdout(Stdio::piped())
183                .stderr(Stdio::inherit())
184                .spawn()
185                .unwrap_or_else(|e| panic!("failed to exec `{adb_path:?}`: {e:?}"));
186
187            // Wait for the gdbserver to print out "Listening on port ..."
188            // at which point we know that it's started and then we can
189            // execute the debugger below.
190            let mut stdout = BufReader::new(adb.stdout.take().unwrap());
191            let mut line = String::new();
192            loop {
193                line.clear();
194                stdout.read_line(&mut line).unwrap();
195                if line.starts_with("Listening on port 5039") {
196                    break;
197                }
198            }
199            drop(stdout);
200
201            let mut debugger_script = OsString::from("-command=");
202            debugger_script.push(self.make_out_name("debugger.script"));
203            let debugger_opts: &[&OsStr] =
204                &["-quiet".as_ref(), "-batch".as_ref(), "-nx".as_ref(), &debugger_script];
205
206            let gdb_path = self.config.gdb.as_ref().unwrap();
207            let Output { status, stdout, stderr } = Command::new(&gdb_path)
208                .args(debugger_opts)
209                .output()
210                .unwrap_or_else(|e| panic!("failed to exec `{gdb_path:?}`: {e:?}"));
211            let cmdline = {
212                let mut gdb = Command::new(&format!("{}-gdb", self.config.target));
213                gdb.args(debugger_opts);
214                // FIXME(jieyouxu): don't pass an empty Path
215                let cmdline = self.make_cmdline(&gdb, Utf8Path::new(""));
216                self.logv(format_args!("executing {cmdline}"));
217                cmdline
218            };
219
220            debugger_run_result = ProcRes {
221                status,
222                stdout: String::from_utf8(stdout).unwrap(),
223                stderr: String::from_utf8(stderr).unwrap(),
224                truncated: Truncated::No,
225                cmdline,
226            };
227            if adb.kill().is_err() {
228                writeln!(self.stdout, "Adb process is already finished.");
229            }
230        } else {
231            let rust_pp_module_abs_path = self.config.src_root.join("src").join("etc");
232            // write debugger script
233            let mut script_str = String::with_capacity(2048);
234            script_str.push_str(&format!("set charset {}\n", Self::charset()));
235            script_str.push_str("show version\n");
236
237            match self.config.gdb_version {
238                Some(version) => {
239                    writeln!(
240                        self.stdout,
241                        "NOTE: compiletest thinks it is using GDB version {}",
242                        version
243                    );
244
245                    if !self.props.disable_gdb_pretty_printers
246                        && version > extract_gdb_version("7.4").unwrap()
247                    {
248                        // Add the directory containing the pretty printers to
249                        // GDB's script auto loading safe path
250                        script_str.push_str(&format!(
251                            "add-auto-load-safe-path {}\n",
252                            rust_pp_module_abs_path.as_str().replace(r"\", r"\\")
253                        ));
254
255                        // Add the directory containing the output binary to
256                        // include embedded pretty printers to GDB's script
257                        // auto loading safe path
258                        script_str.push_str(&format!(
259                            "add-auto-load-safe-path {}\n",
260                            self.output_base_dir().as_str().replace(r"\", r"\\")
261                        ));
262
263                        // GDB visualizer scripts aren't properly embedded on `*-windows-gnu`
264                        // at the moment (see: issue #156687), so we need to load them in
265                        // manually.
266                        #[cfg(target_os = "windows")]
267                        {
268                            script_str.push_str(&format!(
269                                "source {}\n",
270                                self.config
271                                    .src_root
272                                    .join("src/etc/gdb_load_rust_pretty_printers.py")
273                            ));
274                        }
275                    }
276                }
277                _ => {
278                    writeln!(
279                        self.stdout,
280                        "NOTE: compiletest does not know which version of \
281                         GDB it is using"
282                    );
283                }
284            }
285
286            // The following line actually doesn't have to do anything with
287            // pretty printing, it just tells GDB to print values on one line:
288            script_str.push_str("set print pretty off\n");
289
290            // Add the pretty printer directory to GDB's source-file search path
291            script_str.push_str(&format!(
292                "directory {}\n",
293                rust_pp_module_abs_path.as_str().replace(r"\", r"\\")
294            ));
295
296            // Load the target executable
297            script_str.push_str(&format!("file {}\n", exe_file.as_str().replace(r"\", r"\\")));
298
299            // Force GDB to print values in the Rust format.
300            script_str.push_str("set language rust\n");
301
302            // Add line breakpoints
303            for line in &dbg_cmds.breakpoint_lines {
304                script_str.push_str(&format!(
305                    "break '{}':{}\n",
306                    self.testpaths.file.file_name().unwrap(),
307                    *line
308                ));
309            }
310
311            script_str.push_str(&cmds);
312            script_str.push_str("\nquit\n");
313
314            debug!("script_str = {}", script_str);
315            self.dump_output_file(&script_str, "debugger.script");
316
317            let mut debugger_script = OsString::from("-command=");
318            debugger_script.push(self.make_out_name("debugger.script"));
319
320            let debugger_opts: &[&OsStr] =
321                &["-quiet".as_ref(), "-batch".as_ref(), "-nx".as_ref(), &debugger_script];
322
323            let mut gdb = Command::new(self.config.gdb.as_ref().unwrap());
324
325            let pythonpath = with_pythonpath_prepended(&rust_pp_module_abs_path);
326            gdb.args(debugger_opts).env("PYTHONPATH", pythonpath);
327
328            debugger_run_result =
329                self.compose_and_run(gdb, self.config.target_run_lib_path.as_path(), None, None);
330        }
331
332        if !debugger_run_result.status.success() {
333            self.fatal_proc_rec("gdb failed to execute", &debugger_run_result);
334        }
335
336        if let Err(e) = dbg_cmds.check_output(&debugger_run_result) {
337            self.fatal_proc_rec(&e, &debugger_run_result);
338        }
339    }
340
341    fn run_debuginfo_lldb_test(&self) {
342        let Some(ref lldb) = self.config.lldb else {
343            self.fatal("Can't run LLDB test because LLDB's path is not set.");
344        };
345
346        // compile test file (it should have 'compile-flags:-g' in the directive)
347        let should_run = self.run_if_enabled();
348        let compile_result = self.compile_test(should_run, Emit::None);
349        if !compile_result.status.success() {
350            self.fatal_proc_rec("compilation failed!", &compile_result);
351        }
352        if let WillExecute::Disabled = should_run {
353            return;
354        }
355
356        let exe_file = self.make_exe_name();
357
358        match self.config.lldb_version {
359            Some(ref version) => {
360                writeln!(
361                    self.stdout,
362                    "NOTE: compiletest thinks it is using LLDB version {}",
363                    version
364                );
365            }
366            _ => {
367                writeln!(
368                    self.stdout,
369                    "NOTE: compiletest does not know which version of \
370                     LLDB it is using"
371                );
372            }
373        }
374
375        // Parse debugger commands etc from test files
376        let dbg_cmds = DebuggerCommands::parse_from(&self.testpaths.file, "lldb", self.revision)
377            .unwrap_or_else(|e| self.fatal(&e));
378
379        // Write debugger script:
380        // We don't want to hang when calling `quit` while the process is still running
381        let mut script_str = String::from("settings set auto-confirm true\n");
382
383        // macOS has a system for restricting access to files and peripherals
384        // called Transparency, Consent, and Control (TCC), which can be
385        // configured using the "Security & Privacy" tab in your settings.
386        //
387        // This system is provenance-based: if Terminal.app is given access to
388        // your Desktop, and you launch a binary within Terminal.app, the new
389        // binary also has access to the files on your Desktop.
390        //
391        // By default though, LLDB launches binaries in very isolated
392        // contexts. This includes resetting any TCC grants that might
393        // otherwise have been inherited.
394        //
395        // In effect, this means that if the developer has placed the rust
396        // repository under one of the system-protected folders, they will get
397        // a pop-up _for each binary_ asking for permissions to access the
398        // folder - quite annoying.
399        //
400        // To avoid this, we tell LLDB to spawn processes with TCC grants
401        // inherited from the parent process.
402        //
403        // Setting this also avoids unnecessary overhead from XprotectService
404        // when running with the Developer Tool grant.
405        //
406        // TIP: If you want to allow launching `lldb ~/Desktop/my_binary`
407        // without being prompted, you can put this in your `~/.lldbinit` too.
408        if self.config.host.contains("darwin") {
409            script_str.push_str("settings set target.inherit-tcc true\n");
410        }
411
412        // Make LLDB emit its version, so we have it documented in the test output
413        script_str.push_str("version\n");
414
415        // Switch LLDB into "Rust mode".
416        let rust_pp_module_abs_path = self.config.src_root.join("src/etc");
417
418        script_str.push_str(&format!(
419            "command script import {}/lldb_lookup.py\n",
420            rust_pp_module_abs_path
421        ));
422        script_str.push_str("script print(lldb_lookup.FEATURE_FLAGS)\n");
423
424        // Set breakpoints on every line that contains the string "#break"
425        let source_file_name = self.testpaths.file.file_name().unwrap();
426        for line in &dbg_cmds.breakpoint_lines {
427            script_str.push_str(&format!(
428                "breakpoint set --file '{}' --line {}\n",
429                source_file_name, line
430            ));
431        }
432
433        // Append the other commands
434        for line in &dbg_cmds.commands {
435            script_str.push_str(line);
436            script_str.push('\n');
437        }
438
439        // Finally, quit the debugger
440        script_str.push_str("\nquit\n");
441
442        // Write the script into a file
443        debug!("script_str = {}", script_str);
444        self.dump_output_file(&script_str, "debugger.script");
445        let debugger_script = self.make_out_name("debugger.script");
446
447        // Let LLDB execute the script via lldb_batchmode.py
448        let debugger_run_result = self.run_lldb(lldb, &exe_file, &debugger_script);
449
450        if !debugger_run_result.status.success() {
451            self.fatal_proc_rec("Error while running LLDB", &debugger_run_result);
452        }
453
454        if let Err(e) = dbg_cmds.check_output(&debugger_run_result) {
455            self.fatal_proc_rec(&e, &debugger_run_result);
456        }
457    }
458
459    fn run_lldb(
460        &self,
461        lldb: &Utf8Path,
462        test_executable: &Utf8Path,
463        debugger_script: &Utf8Path,
464    ) -> ProcRes {
465        // Path containing `lldb_batchmode.py`, so that the `script` command can import it.
466        let rust_pp_module_abs_path = self.config.src_root.join("src/etc");
467        let pythonpath = with_pythonpath_prepended(&rust_pp_module_abs_path);
468        // make sure `PATH` points to all the dlls necessary to run the debugee
469        let path = prepend_to_path(&self.config.target_run_lib_path);
470
471        let mut cmd = Command::new(lldb);
472        cmd.arg("--one-line")
473            .arg("script --language python -- import lldb_batchmode; lldb_batchmode.main()")
474            .env("LLDB_BATCHMODE_TARGET_PATH", test_executable)
475            .env("LLDB_BATCHMODE_SCRIPT_PATH", debugger_script)
476            .env("PYTHONUNBUFFERED", "1") // Help debugging #78665
477            .env("PYTHONPATH", pythonpath)
478            .env("PATH", path);
479
480        self.run_command_to_procres(&mut cmd)
481    }
482}
483
484fn with_pythonpath_prepended(some_path: &Utf8Path) -> String {
485    // FIXME: we are propagating `PYTHONPATH` from the environment, not a compiletest flag!
486    if let Ok(pp) = std::env::var("PYTHONPATH") {
487        #[cfg(target_os = "windows")]
488        {
489            format!("{pp};{some_path}")
490        }
491        #[cfg(not(target_os = "windows"))]
492        {
493            format!("{pp}:{some_path}")
494        }
495    } else {
496        some_path.to_string()
497    }
498}
499
500fn prepend_to_path(some_path: &Utf8Path) -> String {
501    if let Ok(path) = std::env::var("PATH") {
502        #[cfg(target_os = "windows")]
503        {
504            format!("{some_path};{path}")
505        }
506        #[cfg(not(target_os = "windows"))]
507        {
508            format!("{some_path}:{path}")
509        }
510    } else {
511        some_path.to_string()
512    }
513}