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