blob: 09b8857507c8bc10bf67aa7adc52de552c754fab [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;
extern crate rustc_span;
use arc_anyhow::{anyhow, bail, Result};
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::EarlyDiagCtxt;
/// 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) -> Result<R>
where
F: FnOnce(TyCtxt) -> 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 = EarlyDiagCtxt::new(ErrorOutputType::default());
rustc_driver::init_logger(
&early_error_handler,
rustc_log::LoggerConfig::from_env("CRUBIT_LOG"),
);
});
Lazy::force(&ENV_LOGGER_INIT);
AfterAnalysisCallback::new(rustc_args, callback).run()
}
struct AfterAnalysisCallback<'a, F, R>
where
F: FnOnce(TyCtxt) -> Result<R> + Send,
R: Send,
{
args: &'a [String],
callback_or_result: Either<F, Result<R>>,
}
impl<'a, F, R> AfterAnalysisCallback<'a, F, R>
where
F: FnOnce(TyCtxt) -> 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) -> 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.
let catch_fatal_errors_result: std::result::Result<
std::result::Result<(), rustc_span::ErrorGuaranteed>,
rustc_span::fatal_error::FatalError,
> = rustc_driver::catch_fatal_errors(|| {
rustc_driver::RunCompiler::new(self.args, &mut self).run()
});
match catch_fatal_errors_result {
Ok(Ok(())) => {}
// We can ignore the `Err` payloads because the error types have only one value.
_ => bail!("Errors reported by Rust compiler."),
};
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.
bail!("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) -> 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!("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 run_compiler_test_support::setup_rustc_target_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() -> 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() -> 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() -> 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() -> 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 mut 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(),
];
if let Some(target_arg) = setup_rustc_target_for_testing(tmpdir.path()) {
rustc_args.push(format!("--target={}", target_arg));
}
run_compiler(&rustc_args, |_tcx| Ok(()))?;
// Verify that compilation didn't continue after the initial analysis.
assert!(!out_path.exists());
Ok(())
}
}