blob: 0a2d26de4e7e4027c9ea2f6899c52cc148d147b8 [file] [log] [blame]
// 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_log;
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_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());
#[cfg(google3_internal_rustc_contains_commit_581a317bbbbad807fd88eab490516787f1e9249e)]
rustc_driver::init_logger(
&early_error_handler,
rustc_log::LoggerConfig::from_env("CRUBIT_LOG"),
);
#[cfg(not(
google3_internal_rustc_contains_commit_581a317bbbbad807fd88eab490516787f1e9249e
))]
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(())
}
}