| // Part of the Crubit project, under the Apache License v2.0 with LLVM |
| // Exceptions. See /LICENSE for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| //! The `run_compiler` crate mostly wraps and simplifies a subset of APIs |
| //! from the `rustc_driver` crate, providing an easy way to `run_compiler`. |
| #![feature(rustc_private)] |
| #![deny(rustc::internal)] |
| |
| extern crate rustc_driver; |
| extern crate rustc_interface; |
| extern crate rustc_lint_defs; |
| extern crate rustc_middle; |
| extern crate rustc_session; |
| |
| use anyhow::anyhow; |
| use either::Either; |
| use rustc_interface::interface::Compiler; |
| use rustc_interface::Queries; |
| use rustc_middle::ty::TyCtxt; // See also <internal link>/ty.html#import-conventions |
| use rustc_session::config::ErrorOutputType; |
| use rustc_session::EarlyErrorHandler; |
| |
| /// Wrapper around `rustc_driver::RunCompiler::run` that exposes a |
| /// simplified API: |
| /// - Takes a `callback` that will be invoked from within Rust compiler, after |
| /// parsing and analysis are done, |
| /// - Compilation will stop after parsing, analysis, and the `callback` are |
| /// done, |
| /// - Returns the combined results from the Rust compiler *and* the `callback`. |
| /// - Is safe to run from unit tests (which may run in parallel / on multiple |
| /// threads). |
| pub fn run_compiler<F, R>(rustc_args: &[String], callback: F) -> anyhow::Result<R> |
| where |
| F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| R: Send, |
| { |
| // Calling `init_env_logger` 1) here and 2) via `sync::Lazy` helps to ensure |
| // that logging is intialized exactly once, even if the `run_compiler` |
| // function is invoked by mutliple unit tests running in parallel on |
| // separate threads. This is important for avoiding flaky/racy |
| // panics related to 1) multiple threads entering |
| // `!tracing::dispatcher::has_been_set()` code in `rustc_driver_impl/src/ |
| // lib.rs` and 2) `rustc_log/src/lib.rs` assumming that |
| // `tracing::subscriber::set_global_default` always succeeds. |
| use once_cell::sync::Lazy; |
| static ENV_LOGGER_INIT: Lazy<()> = Lazy::new(|| { |
| let early_error_handler = EarlyErrorHandler::new(ErrorOutputType::default()); |
| rustc_driver::init_env_logger(&early_error_handler, "CRUBIT_LOG"); |
| }); |
| Lazy::force(&ENV_LOGGER_INIT); |
| |
| AfterAnalysisCallback::new(rustc_args, callback).run() |
| } |
| |
| struct AfterAnalysisCallback<'a, F, R> |
| where |
| F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| R: Send, |
| { |
| args: &'a [String], |
| callback_or_result: Either<F, anyhow::Result<R>>, |
| } |
| |
| impl<'a, F, R> AfterAnalysisCallback<'a, F, R> |
| where |
| F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| R: Send, |
| { |
| fn new(args: &'a [String], callback: F) -> Self { |
| Self { args, callback_or_result: Either::Left(callback) } |
| } |
| |
| /// Runs Rust compiler, and then invokes the stored callback (with |
| /// `TyCtxt` of the parsed+analyzed Rust crate as the callback's |
| /// argument), and then finally returns the combined results |
| /// from Rust compiler *and* the callback. |
| fn run(mut self) -> anyhow::Result<R> { |
| // Rust compiler unwinds with a special sentinel value to abort compilation on |
| // fatal errors. We use `catch_fatal_errors` to 1) catch such panics and |
| // translate them into a Result, and 2) resume and propagate other panics. |
| use rustc_interface::interface::Result; |
| let rustc_result: Result<Result<()>> = rustc_driver::catch_fatal_errors(|| { |
| rustc_driver::RunCompiler::new(self.args, &mut self).run() |
| }); |
| |
| // Flatten `Result<Result<T, ...>>` into `Result<T, ...>` (i.e. combine the |
| // result from `RunCompiler::run` and `catch_fatal_errors`). |
| // |
| // TODO(lukasza): Use `Result::flatten` API when it gets stabilized. See also |
| // https://github.com/rust-lang/rust/issues/70142 |
| let rustc_result: Result<()> = rustc_result.and_then(|result| result); |
| |
| // Translate `rustc_interface::interface::Result` into `anyhow::Result`. (Can't |
| // use `?` because the trait `std::error::Error` is not implemented for |
| // `ErrorGuaranteed` which is required by the impl of |
| // `From<ErrorGuaranteed>` for `anyhow::Error`.) |
| let rustc_result: anyhow::Result<()> = rustc_result.map_err(|_err| { |
| // We can ignore `_err` because it has no payload / because this type has only |
| // one valid/possible value. |
| anyhow!("Errors reported by Rust compiler.") |
| }); |
| |
| // Return either `rustc_result` or `self.callback_result` or a new error. |
| rustc_result.and_then(|()| { |
| self.callback_or_result.right_or_else(|_left| { |
| // When rustc cmdline arguments (i.e. `self.args`) are empty (or contain |
| // `--help`) then the `after_analysis` callback won't be invoked. Handle |
| // this case by emitting an explicit error at the Crubit level. |
| Err(anyhow!("The Rust compiler had no crate to compile and analyze")) |
| }) |
| }) |
| } |
| } |
| |
| impl<'a, F, R> rustc_driver::Callbacks for AfterAnalysisCallback<'a, F, R> |
| where |
| F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| R: Send, |
| { |
| /// Configures how `rustc` internals work when invoked via `run_compiler`. |
| /// Note that `run_compiler_test_support` uses a separate `Config`. |
| fn config(&mut self, config: &mut rustc_interface::interface::Config) { |
| // Silence warnings in the target crate to avoid reporting them twice: once when |
| // compiling the crate via `rustc` and once when "compiling" the crate |
| // via `cc_bindings_from_rs` (the `config` here affects the latter one). |
| config.opts.lint_opts.push(("warnings".to_string(), rustc_lint_defs::Level::Allow)); |
| } |
| |
| fn after_analysis<'tcx>( |
| &mut self, |
| _compiler: &Compiler, |
| queries: &'tcx Queries<'tcx>, |
| ) -> rustc_driver::Compilation { |
| // `after_analysis` is only called by `rustc_driver` if earlier compiler |
| // analysis was successful (which as the *last* compilation phase |
| // presumably covers *all* errors). |
| let mut query_context = queries |
| .global_ctxt() |
| .expect("Expecting no compile errors inside `after_analysis` callback."); |
| query_context.enter(|tcx| { |
| let callback = { |
| let temporary_placeholder = Either::Right(Err(anyhow::anyhow!("unused"))); |
| std::mem::replace(&mut self.callback_or_result, temporary_placeholder) |
| .left_or_else(|_| panic!("`after_analysis` should only run once")) |
| }; |
| self.callback_or_result = Either::Right(callback(tcx)); |
| }); |
| |
| rustc_driver::Compilation::Stop |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use run_compiler_test_support::get_sysroot_for_testing; |
| use tempfile::tempdir; |
| |
| const DEFAULT_RUST_SOURCE_FOR_TESTING: &'static str = r#" pub mod public_module { |
| pub fn public_function() { |
| private_function() |
| } |
| |
| fn private_function() {} |
| } |
| "#; |
| |
| #[test] |
| fn test_run_compiler_rustc_error_propagation() -> anyhow::Result<()> { |
| let rustc_args = vec![ |
| "run_compiler_unittest_executable".to_string(), |
| "--unrecognized-rustc-flag".to_string(), |
| ]; |
| let err = run_compiler(&rustc_args, |_tcx| Ok(())) |
| .expect_err("--unrecognized-rustc-flag should trigger an error"); |
| |
| let msg = format!("{err:#}"); |
| assert_eq!("Errors reported by Rust compiler.", msg); |
| Ok(()) |
| } |
| |
| /// `test_run_compiler_empty_args` tests that we gracefully handle scenarios |
| /// where `rustc` doesn't compile anything (e.g. when there are no |
| /// cmdline args). |
| #[test] |
| fn test_run_compiler_no_args_except_argv0() -> anyhow::Result<()> { |
| let rustc_args = vec!["run_compiler_unittest_executable".to_string()]; |
| let err = run_compiler(&rustc_args, |_tcx| Ok(())) |
| .expect_err("Empty `rustc_args` should trigger an error"); |
| |
| let msg = format!("{err:#}"); |
| assert_eq!("The Rust compiler had no crate to compile and analyze", msg); |
| Ok(()) |
| } |
| |
| /// `test_run_compiler_help` tests that we gracefully handle scenarios where |
| /// `rustc` doesn't compile anything (e.g. when passing `--help`). |
| #[test] |
| fn test_run_compiler_help() -> anyhow::Result<()> { |
| let rustc_args = vec!["run_compiler_unittest_executable".to_string(), "--help".to_string()]; |
| let err = run_compiler(&rustc_args, |_tcx| Ok(())) |
| .expect_err("--help passed to rustc should trigger an error"); |
| |
| let msg = format!("{err:#}"); |
| assert_eq!("The Rust compiler had no crate to compile and analyze", msg); |
| Ok(()) |
| } |
| |
| /// `test_run_compiler_no_output_file` tests that we stop the compilation |
| /// midway (i.e. that we return `Stop` from `after_analysis`). |
| #[test] |
| fn test_run_compiler_no_output_file() -> anyhow::Result<()> { |
| let tmpdir = tempdir()?; |
| |
| let rs_path = tmpdir.path().join("input_crate.rs"); |
| std::fs::write(&rs_path, DEFAULT_RUST_SOURCE_FOR_TESTING)?; |
| |
| let out_path = tmpdir.path().join("unexpected_output.o"); |
| |
| let rustc_args = vec![ |
| // Default parameters. |
| "run_compiler_unittest_executable".to_string(), |
| "--crate-type=lib".to_string(), |
| format!("--sysroot={}", get_sysroot_for_testing().display()), |
| rs_path.display().to_string(), |
| // Test-specific parameter: asking for after-analysis output |
| "-o".to_string(), |
| out_path.display().to_string(), |
| ]; |
| |
| run_compiler(&rustc_args, |_tcx| Ok(()))?; |
| |
| // Verify that compilation didn't continue after the initial analysis. |
| assert!(!out_path.exists()); |
| Ok(()) |
| } |
| } |