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