blob: 9fc4a88efa02de0ad6ea31781931745ba15c01b7 [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
#![feature(never_type)]
#![feature(rustc_private)]
#![deny(rustc::internal)]
extern crate rustc_driver;
extern crate rustc_error_codes;
extern crate rustc_errors;
extern crate rustc_feature;
extern crate rustc_hir;
extern crate rustc_interface;
extern crate rustc_lint_defs;
extern crate rustc_middle;
extern crate rustc_session;
extern crate rustc_span;
extern crate rustc_target;
// TODO(b/254679226): `bindings` and `cmdline` should be separate crates.
mod bindings;
mod cmdline;
use anyhow::Context;
use itertools::Itertools;
use rustc_middle::ty::TyCtxt;
use std::path::Path;
use bindings::GeneratedBindings;
use cmdline::Cmdline;
use token_stream_printer::{
cc_tokens_to_formatted_string, rs_tokens_to_formatted_string, RustfmtConfig,
};
/// This mostly wraps and simplifies a subset of APIs from the `rustc_driver`
/// module.
mod bindings_driver {
use anyhow::anyhow;
use either::Either;
use rustc_interface::interface::Compiler;
use rustc_interface::Queries;
use rustc_middle::ty::TyCtxt;
use crate::bindings::enter_tcx;
/// 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`.
pub fn run_after_analysis_and_stop<F, R>(
rustc_args: &[String],
callback: F,
) -> anyhow::Result<R>
where
F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send,
R: Send,
{
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::format_err!("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,
{
fn after_analysis<'tcx>(
&mut self,
_compiler: &Compiler,
queries: &'tcx Queries<'tcx>,
) -> rustc_driver::Compilation {
let rustc_result = enter_tcx(queries, |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));
});
// `expect`ing no errors in `rustc_result`, because `after_analysis` is only
// called by `rustc_driver` if earlier compiler analysis was successful
// (which as the *last* compilation phase presumably covers *all*
// errors).
rustc_result.expect("Expecting no compile errors inside `after_analysis` callback.");
rustc_driver::Compilation::Stop
}
}
}
fn write_file(path: &Path, content: &str) -> anyhow::Result<()> {
std::fs::write(path, content)
.with_context(|| format!("Error when writing to {}", path.display()))
}
fn run_with_tcx(cmdline: &Cmdline, tcx: TyCtxt) -> anyhow::Result<()> {
let bindings = GeneratedBindings::generate(tcx)?;
{
let h_body = cc_tokens_to_formatted_string(bindings.h_body)?;
write_file(&cmdline.h_out, &h_body)?;
}
{
let rustfmt_config =
RustfmtConfig::new(&cmdline.rustfmt_exe_path, cmdline.rustfmt_config_path.as_deref());
let rs_body = rs_tokens_to_formatted_string(bindings.rs_body, &rustfmt_config)?;
write_file(&cmdline.rs_out, &rs_body)?;
}
Ok(())
}
/// Main entrypoint that (unlike `main`) doesn't do any intitializations that
/// should only happen once for the binary (e.g. it doesn't call
/// `install_ice_hook`) and therefore can be used from the tests module below.
fn run_with_cmdline_args(args: &[String]) -> anyhow::Result<()> {
let cmdline = Cmdline::new(args)?;
bindings_driver::run_after_analysis_and_stop(&cmdline.rustc_args, |tcx| {
run_with_tcx(&cmdline, tcx)
})
}
fn main() -> anyhow::Result<()> {
rustc_driver::init_env_logger("CRUBIT_LOG");
// TODO: Investigate if we should install a signal handler here. See also how
// compiler/rustc_driver/src/lib.rs calls `signal_handler::install()`.
// TODO(b/254689400): Provide Crubit-specific panic hook message (we shouldn't use
// `rustc_driver::install_ice_hook` because it's message asks to file bugs at
// https://github.com/rust-lang/rust/issues/new.
// `std::env::args()` will panic if any of the cmdline arguments are not valid
// Unicode. This seems okay.
let args = std::env::args().collect_vec();
run_with_cmdline_args(&args)
.map_err(|anyhow_err| match anyhow_err.downcast::<clap::Error>() {
// Explicitly call `clap::Error::exit`, because 1) it results in *colored* output and
// 2) it uses a zero exit code for specific "errors" (e.g. for `--help` output).
Ok(clap_err) => {
let _ : ! = clap_err.exit();
},
// Return `other_err` from `main`. This will print the error message (no color codes
// though) and terminate the process with a non-zero exit code.
Err(other_err) => other_err,
})
}
#[cfg(test)]
mod tests {
use super::run_with_cmdline_args;
use crate::bindings::tests::get_sysroot_for_testing;
use itertools::Itertools;
use std::path::PathBuf;
use tempfile::{tempdir, TempDir};
use token_stream_printer::RUSTFMT_EXE_PATH_FOR_TESTING;
/// Test data builder (see also
/// https://testing.googleblog.com/2018/02/testing-on-toilet-cleanly-create-test.html).
struct TestArgs {
h_path: Option<String>,
extra_crubit_args: Vec<String>,
/// Arg for the following `rustc` flag: `--codegen=panic=<arg>`.
panic_mechanism: String,
/// Other `rustc` flags.
extra_rustc_args: Vec<String>,
tempdir: TempDir,
}
/// Result of `TestArgs::run` that helps tests access test outputs (e.g. the
/// internally generated `h_path` and/or `rs_input_path`).
#[derive(Debug)]
struct TestResult {
h_path: PathBuf,
rs_path: PathBuf,
}
impl TestArgs {
fn default_args() -> anyhow::Result<Self> {
Ok(Self {
h_path: None,
extra_crubit_args: vec![],
panic_mechanism: "abort".to_string(),
extra_rustc_args: vec![],
tempdir: tempdir()?,
})
}
/// Use the specified `h_path` rather than auto-generating one in
/// `self`-managed temporary directory.
fn with_h_path(mut self, h_path: &str) -> Self {
self.h_path = Some(h_path.to_string());
self
}
/// Replaces the default `--codegen=panic=abort` with the specified
/// `panic_mechanism`.
fn with_panic_mechanism(mut self, panic_mechanism: &str) -> Self {
self.panic_mechanism = panic_mechanism.to_string();
self
}
/// Appends `extra_rustc_args` at the end of the cmdline (i.e. as
/// additional rustc args, in addition to `--sysroot`,
/// `--crate-type=...`, etc.).
fn with_extra_rustc_args<T>(mut self, extra_rustc_args: T) -> Self
where
T: IntoIterator,
T::Item: Into<String>,
{
self.extra_rustc_args = extra_rustc_args.into_iter().map(|t| t.into()).collect_vec();
self
}
/// Appends `extra_crubit_args` before the first `--`.
fn with_extra_crubit_args<T>(mut self, extra_crubit_args: T) -> Self
where
T: IntoIterator,
T::Item: Into<String>,
{
self.extra_crubit_args = extra_crubit_args.into_iter().map(|t| t.into()).collect_vec();
self
}
/// Invokes `super::run_with_cmdline_args` with default `test_crate.rs`
/// input (and with other default args + args gathered by
/// `self`).
///
/// Returns the path to the `h_out` file. The file's lifetime is the
/// same as `&self`.
fn run(&self) -> anyhow::Result<TestResult> {
let h_path = match self.h_path.as_ref() {
None => self.tempdir.path().join("test_crate_cc_api.h"),
Some(s) => PathBuf::from(s),
};
let rs_path = self.tempdir.path().join("test_crate_cc_api_impl.rs");
let rs_input_path = self.tempdir.path().join("test_crate.rs");
std::fs::write(
&rs_input_path,
r#" pub fn public_function() {
private_function()
}
fn private_function() {}
"#,
)?;
let mut args = vec![
"cc_bindings_from_rs_unittest_executable".to_string(),
format!("--h-out={}", h_path.display()),
format!("--rs-out={}", rs_path.display()),
format!("--rustfmt-exe-path={RUSTFMT_EXE_PATH_FOR_TESTING}"),
];
args.extend(self.extra_crubit_args.iter().cloned());
args.extend([
"--".to_string(),
format!("--codegen=panic={}", &self.panic_mechanism),
// See comments about `SymbolManglingVersion::V0`in `test::run_compiler` in
// `bindings.rs` for rationale behind using `symbol-mangling-version=v0`.
"--codegen=symbol-mangling-version=v0".to_string(),
"--crate-type=lib".to_string(),
format!("--sysroot={}", get_sysroot_for_testing().display()),
rs_input_path.display().to_string(),
]);
args.extend(self.extra_rustc_args.iter().cloned());
run_with_cmdline_args(&args)?;
Ok(TestResult { h_path, rs_path })
}
}
#[test]
fn test_happy_path() -> anyhow::Result<()> {
let test_args = TestArgs::default_args()?;
let test_result = test_args.run().expect("Default args should succeed");
assert!(test_result.h_path.exists());
let h_body = std::fs::read_to_string(&test_result.h_path)?;
assert_eq!(
h_body,
r#"// Automatically @generated C++ bindings for the following Rust crate:
// test_crate
#pragma once
namespace test_crate {
namespace __crubit_internal {
extern "C" void
__crubit_thunk__RNvCslKXfKXPWofF_10test_crate15public_function();
}
inline void public_function() {
return __crubit_internal::
__crubit_thunk__RNvCslKXfKXPWofF_10test_crate15public_function();
}
} // namespace test_crate"#
);
assert!(test_result.rs_path.exists());
let rs_body = std::fs::read_to_string(&test_result.rs_path)?;
assert_eq!(
rs_body,
r#"// Automatically @generated C++ bindings for the following Rust crate:
// test_crate
#![allow(improper_ctypes_definitions)]
#[no_mangle]
extern "C" fn __crubit_thunk__RNvCslKXfKXPWofF_10test_crate15public_function() -> () {
test_crate::public_function()
}
"#
);
Ok(())
}
#[test]
fn test_cmdline_error_propagation() -> anyhow::Result<()> {
// Tests that errors from `Cmdline::new` get propagated. Broader coverage of
// various error types can be found in tests in `cmdline.rs`.
let err = TestArgs::default_args()?
.with_extra_crubit_args(["--unrecognized-crubit-flag"])
.run()
.expect_err("--unrecognized_crubit_flag should trigger an error");
let msg = format!("{err:#}");
assert!(
msg.contains("Found argument '--unrecognized-crubit-flag' which wasn't expected"),
"msg = {}",
msg,
);
Ok(())
}
#[test]
fn test_rustc_error_propagation() -> anyhow::Result<()> {
// Tests that `rustc` errors are propagated.
let err = TestArgs::default_args()?
.with_extra_rustc_args(["--unrecognized-rustc-flag"])
.run()
.expect_err("--unrecognized-rustc-flag should trigger an error");
let msg = format!("{err:#}");
assert_eq!("Errors reported by Rust compiler.", msg);
Ok(())
}
#[test]
fn test_rustc_help() -> anyhow::Result<()> {
// Tests that we gracefully handle scenarios where `rustc` doesn't compile
// anything (e.g. when there are no rustc cmdline arguments, or when
// `--help` is present).
let err = TestArgs::default_args()?
.with_extra_rustc_args(["--help"])
.run()
.expect_err("--help passed to rustc should trigger Crubit-level error");
let msg = format!("{err:#}");
assert_eq!("The Rust compiler had no crate to compile and analyze", msg);
Ok(())
}
#[test]
fn test_rustc_unsupported_panic_mechanism() -> anyhow::Result<()> {
// Tests that `panic=unwind` results in an error.
let err = TestArgs::default_args()?
.with_panic_mechanism("unwind")
.run()
.expect_err("panic=unwind should trigger an error");
let msg = format!("{err:#}");
assert_eq!("No support for panic=unwind strategy (b/254049425)", msg);
Ok(())
}
#[test]
fn test_invalid_h_out_path() -> anyhow::Result<()> {
// Tests not only the specific problem of an invalid `--h-out` argument, but
// also tests that errors from `run_with_tcx` are propagated.
let err = TestArgs::default_args()?
.with_h_path("../..")
.run()
.expect_err("Unwriteable --h-out should trigger an error");
let msg = format!("{err:#}");
assert_eq!("Error when writing to ../..: Is a directory (os error 21)", msg);
Ok(())
}
#[test]
fn test_no_output_file() -> anyhow::Result<()> {
// Tests that we stop the compilation midway.
let tmpdir = tempdir()?;
let out_path = tmpdir.path().join("unexpected_output.o");
TestArgs::default_args()?
.with_extra_rustc_args(vec!["-o", &out_path.display().to_string()])
.run()
.expect("No rustc or Crubit errors are expected in this test");
assert!(!out_path.exists());
Ok(())
}
}