Skip to main content

compiletest/runtest/
debuginfo.rs

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