Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 1 | // Part of the Crubit project, under the Apache License v2.0 with LLVM |
| 2 | // Exceptions. See /LICENSE for license information. |
| 3 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 4 | |
| 5 | #![feature(rustc_private)] |
| 6 | #![deny(rustc::internal)] |
| 7 | |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 8 | extern crate rustc_driver; |
| 9 | extern crate rustc_error_codes; |
| 10 | extern crate rustc_errors; |
| 11 | extern crate rustc_hir; |
| 12 | extern crate rustc_interface; |
| 13 | extern crate rustc_middle; |
| 14 | extern crate rustc_session; |
| 15 | extern crate rustc_span; |
| 16 | |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 17 | // TODO(lukasza): Make `cmdline` and `lib` a separate crate (once we move to |
| 18 | // Bazel). |
| 19 | mod cmdline; |
| 20 | mod lib; |
| 21 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 22 | use anyhow::Context; |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 23 | use itertools::Itertools; |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 24 | use rustc_middle::ty::TyCtxt; |
| 25 | use std::path::Path; |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 26 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 27 | use cmdline::Cmdline; |
| 28 | use lib::GeneratedBindings; |
| 29 | use token_stream_printer::tokens_to_string; |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 30 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 31 | /// This mostly wraps and simplifies a subset of APIs from the `rustc_driver` |
| 32 | /// module. |
Lukasz Anforowicz | c345284 | 2022-09-23 08:02:58 -0700 | [diff] [blame] | 33 | mod bindings_driver { |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 34 | |
Lukasz Anforowicz | 31ca049 | 2022-09-28 15:55:23 -0700 | [diff] [blame^] | 35 | use either::Either; |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 36 | use rustc_interface::interface::Compiler; |
| 37 | use rustc_interface::Queries; |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 38 | use rustc_middle::ty::TyCtxt; |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 39 | |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 40 | use crate::lib::enter_tcx; |
| 41 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 42 | /// Wrapper around `rustc_driver::RunCompiler::run` that exposes a |
| 43 | /// simplified API: |
| 44 | /// - Takes a `callback` that will be invoked from within Rust compiler, |
| 45 | /// after parsing and analysis are done, |
| 46 | /// - Compilation will stop after parsing, analysis, and the `callback are |
| 47 | /// done, |
| 48 | /// - Returns the combined results from the Rust compiler *and* the |
| 49 | /// `callback`. |
| 50 | pub fn run_after_analysis_and_stop<F, R>( |
| 51 | rustc_args: &[String], |
| 52 | callback: F, |
| 53 | ) -> anyhow::Result<R> |
| 54 | where |
| 55 | F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| 56 | R: Send, |
| 57 | { |
| 58 | AfterAnalysisCallback::new(rustc_args, callback).run() |
| 59 | } |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 60 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 61 | struct AfterAnalysisCallback<'a, F, R> |
| 62 | where |
| 63 | F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| 64 | R: Send, |
| 65 | { |
| 66 | args: &'a [String], |
Lukasz Anforowicz | 31ca049 | 2022-09-28 15:55:23 -0700 | [diff] [blame^] | 67 | callback_or_result: Either<F, anyhow::Result<R>>, |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 68 | } |
| 69 | |
| 70 | impl<'a, F, R> AfterAnalysisCallback<'a, F, R> |
| 71 | where |
| 72 | F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| 73 | R: Send, |
| 74 | { |
| 75 | fn new(args: &'a [String], callback: F) -> Self { |
Lukasz Anforowicz | 31ca049 | 2022-09-28 15:55:23 -0700 | [diff] [blame^] | 76 | Self { args, callback_or_result: Either::Left(callback) } |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 77 | } |
| 78 | |
Lukasz Anforowicz | c345284 | 2022-09-23 08:02:58 -0700 | [diff] [blame] | 79 | /// Runs Rust compiler and then passes the `TyCtxt` of the |
| 80 | /// parsed+analyzed Rust crate into `bindings_main::main`. |
| 81 | /// Returns the combined results from Rust compiler *and* |
| 82 | /// `bindings_main::main`. |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 83 | fn run(mut self) -> anyhow::Result<R> { |
Lukasz Anforowicz | c345284 | 2022-09-23 08:02:58 -0700 | [diff] [blame] | 84 | // Rust compiler unwinds with a special sentinel value to abort compilation on |
| 85 | // fatal errors. We use `catch_fatal_errors` to 1) catch such panics and |
| 86 | // translate them into a Result, and 2) resume and propagate other panics. |
| 87 | let rustc_result = rustc_driver::catch_fatal_errors(|| { |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 88 | rustc_driver::RunCompiler::new(self.args, &mut self).run() |
Lukasz Anforowicz | c345284 | 2022-09-23 08:02:58 -0700 | [diff] [blame] | 89 | }); |
| 90 | |
| 91 | // Flatten `Result<Result<T, ...>>` into `Result<T, ...>` (i.e. get the Result |
| 92 | // from `RunCompiler::run` rather than the Result from |
| 93 | // `catch_fatal_errors`). |
| 94 | let rustc_result = rustc_result.and_then(|result| result); |
| 95 | |
| 96 | // Translate `rustc_interface::interface::Result` into `anyhow::Result`. (Can't |
| 97 | // use `?` because the trait `std::error::Error` is not implemented for |
| 98 | // `ErrorGuaranteed` which is required by the impl of |
| 99 | // `From<ErrorGuaranteed>` for `anyhow::Error`.) |
| 100 | let rustc_result = rustc_result.map_err(|_err| { |
| 101 | // We can ignore `_err` because it has no payload / because this type has only |
| 102 | // one valid/possible value. |
| 103 | anyhow::format_err!("Errors reported by Rust compiler.") |
| 104 | }); |
| 105 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 106 | // Return either `rustc_result` or `self.callback_result`. |
Lukasz Anforowicz | c345284 | 2022-09-23 08:02:58 -0700 | [diff] [blame] | 107 | rustc_result.and_then(|()| { |
Lukasz Anforowicz | 31ca049 | 2022-09-28 15:55:23 -0700 | [diff] [blame^] | 108 | self.callback_or_result.right_or_else(|_| panic!("The callback should have been called by now")) |
Lukasz Anforowicz | c345284 | 2022-09-23 08:02:58 -0700 | [diff] [blame] | 109 | }) |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 110 | } |
| 111 | } |
| 112 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 113 | impl<'a, F, R> rustc_driver::Callbacks for AfterAnalysisCallback<'_, F, R> |
| 114 | where |
| 115 | F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| 116 | R: Send, |
| 117 | { |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 118 | fn after_analysis<'tcx>( |
| 119 | &mut self, |
| 120 | _compiler: &Compiler, |
| 121 | queries: &'tcx Queries<'tcx>, |
| 122 | ) -> rustc_driver::Compilation { |
Lukasz Anforowicz | b8279db | 2022-09-22 07:40:55 -0700 | [diff] [blame] | 123 | let rustc_result = enter_tcx(queries, |tcx| { |
Lukasz Anforowicz | 31ca049 | 2022-09-28 15:55:23 -0700 | [diff] [blame^] | 124 | let callback = { |
| 125 | let temporary_placeholder = Either::Right(Err(anyhow::anyhow!("unused"))); |
| 126 | std::mem::replace(&mut self.callback_or_result, temporary_placeholder) |
| 127 | .left_or_else(|_| panic!("`after_analysis` should only run once")) |
| 128 | }; |
| 129 | self.callback_or_result = Either::Right(callback(tcx)); |
Lukasz Anforowicz | b8279db | 2022-09-22 07:40:55 -0700 | [diff] [blame] | 130 | }); |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 131 | |
| 132 | // `expect`ing no errors in `rustc_result`, because `after_analysis` is only |
| 133 | // called by `rustc_driver` if earlier compiler analysis was successful |
| 134 | // (which as the *last* compilation phase presumably covers *all* |
| 135 | // errors). |
Lukasz Anforowicz | b8279db | 2022-09-22 07:40:55 -0700 | [diff] [blame] | 136 | rustc_result.expect("Expecting no compile errors inside `after_analysis` callback."); |
Lukasz Anforowicz | bf5a4ee | 2022-09-22 07:38:33 -0700 | [diff] [blame] | 137 | |
| 138 | rustc_driver::Compilation::Stop |
| 139 | } |
| 140 | } |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 141 | } |
| 142 | |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 143 | fn write_file(path: &Path, content: &str) -> anyhow::Result<()> { |
| 144 | std::fs::write(path, content) |
| 145 | .with_context(|| format!("Error when writing to {}", path.display())) |
| 146 | } |
| 147 | |
| 148 | fn run_with_tcx(cmdline: &Cmdline, tcx: TyCtxt) -> anyhow::Result<()> { |
| 149 | let bindings = GeneratedBindings::generate(tcx); |
| 150 | write_file(cmdline.h_out(), tokens_to_string(bindings.h_body)?.as_str()) |
| 151 | } |
| 152 | |
Lukasz Anforowicz | f018e4d | 2022-09-28 07:35:59 -0700 | [diff] [blame] | 153 | /// Main entrypoint that (unlike `main`) doesn't do any intitializations that |
| 154 | /// should only happen once for the binary (e.g. it doesn't call |
| 155 | /// `install_ice_hook`) and therefore can be used from the tests module below. |
| 156 | fn run_with_cmdline_args(args: &[String]) -> anyhow::Result<()> { |
| 157 | let cmdline = Cmdline::new(args)?; |
Lukasz Anforowicz | 1093f78 | 2022-09-28 12:45:10 -0700 | [diff] [blame] | 158 | bindings_driver::run_after_analysis_and_stop(cmdline.rustc_args(), |tcx| { |
| 159 | run_with_tcx(&cmdline, tcx) |
| 160 | }) |
Lukasz Anforowicz | f018e4d | 2022-09-28 07:35:59 -0700 | [diff] [blame] | 161 | } |
| 162 | |
| 163 | // TODO(lukasza): Add end-to-end shell tests that invoke our executable |
| 164 | // and verify 1) the happy path (zero exit code) and 2) any random |
| 165 | // error path (non-zero exit code). |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 166 | fn main() -> anyhow::Result<()> { |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 167 | rustc_driver::init_env_logger("CRUBIT_LOG"); |
| 168 | |
| 169 | // TODO: Investigate if we should install a signal handler here. See also how |
| 170 | // compiler/rustc_driver/src/lib.rs calls `signal_handler::install()`. |
| 171 | |
| 172 | rustc_driver::install_ice_hook(); |
| 173 | |
Lukasz Anforowicz | f018e4d | 2022-09-28 07:35:59 -0700 | [diff] [blame] | 174 | // `std::env::args()` will panic if any of the cmdline arguments are not valid |
| 175 | // Unicode. This seems okay. |
| 176 | let args = std::env::args().collect_vec(); |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 177 | |
Lukasz Anforowicz | f018e4d | 2022-09-28 07:35:59 -0700 | [diff] [blame] | 178 | run_with_cmdline_args(&args) |
| 179 | } |
| 180 | |
| 181 | #[cfg(test)] |
| 182 | mod tests { |
| 183 | use super::run_with_cmdline_args; |
| 184 | |
| 185 | use crate::lib::tests::get_sysroot_for_testing; |
| 186 | use itertools::Itertools; |
| 187 | use std::path::PathBuf; |
| 188 | use tempfile::{tempdir, TempDir}; |
| 189 | |
| 190 | /// Test data builder (see also |
| 191 | /// https://testing.googleblog.com/2018/02/testing-on-toilet-cleanly-create-test.html). |
| 192 | struct TestArgs { |
| 193 | h_path: Option<String>, |
| 194 | extra_crubit_args: Vec<String>, |
| 195 | extra_rustc_args: Vec<String>, |
| 196 | tempdir: TempDir, |
| 197 | } |
| 198 | |
| 199 | impl TestArgs { |
| 200 | fn default_args() -> anyhow::Result<Self> { |
| 201 | Ok(Self { |
| 202 | h_path: None, |
| 203 | extra_crubit_args: vec![], |
| 204 | extra_rustc_args: vec![], |
| 205 | tempdir: tempdir()?, |
| 206 | }) |
| 207 | } |
| 208 | |
| 209 | /// Use the specified `h_path` rather than auto-generating one in |
| 210 | /// `self`-managed temporary directory. |
| 211 | fn with_h_path(mut self, h_path: &str) -> Self { |
| 212 | self.h_path = Some(h_path.to_string()); |
| 213 | self |
| 214 | } |
| 215 | |
| 216 | /// Appends `extra_rustc_args` at the end of the cmdline (i.e. as |
| 217 | /// additional rustc args, in addition to `--sysroot`, |
| 218 | /// `--crate-type=...`, etc.). |
| 219 | fn with_extra_rustc_args<T>(mut self, extra_rustc_args: T) -> Self |
| 220 | where |
| 221 | T: IntoIterator, |
| 222 | T::Item: Into<String>, |
| 223 | { |
| 224 | self.extra_rustc_args = extra_rustc_args.into_iter().map(|t| t.into()).collect_vec(); |
| 225 | self |
| 226 | } |
| 227 | |
| 228 | /// Appends `extra_crubit_args` before the first `--`. |
| 229 | fn with_extra_crubit_args<T>(mut self, extra_crubit_args: T) -> Self |
| 230 | where |
| 231 | T: IntoIterator, |
| 232 | T::Item: Into<String>, |
| 233 | { |
| 234 | self.extra_crubit_args = extra_crubit_args.into_iter().map(|t| t.into()).collect_vec(); |
| 235 | self |
| 236 | } |
| 237 | |
| 238 | /// Invokes `super::run_with_cmdline_args` with default `test_crate.rs` |
| 239 | /// input (and with other default args + args gathered by |
| 240 | /// `self`). |
| 241 | /// |
| 242 | /// Returns the path to the `h_out` file. The file's lifetime is the |
| 243 | /// same as `&self`. |
| 244 | fn run(&self) -> anyhow::Result<PathBuf> { |
| 245 | let h_path = match self.h_path.as_ref() { |
| 246 | None => self.tempdir.path().join("test_crate.rs"), |
| 247 | Some(s) => PathBuf::from(s), |
| 248 | }; |
| 249 | |
| 250 | let rs_input_path = self.tempdir.path().join("test_crate.rs"); |
| 251 | std::fs::write( |
| 252 | &rs_input_path, |
| 253 | r#" pub fn public_function() { |
| 254 | private_function() |
| 255 | } |
| 256 | |
| 257 | fn private_function() {} |
| 258 | "#, |
| 259 | )?; |
| 260 | |
| 261 | let mut args = vec![ |
| 262 | "cc_bindings_from_rs_unittest_executable".to_string(), |
| 263 | format!("--h_out={}", h_path.display()), |
| 264 | ]; |
| 265 | args.extend(self.extra_crubit_args.iter().cloned()); |
| 266 | args.extend([ |
| 267 | "--".to_string(), |
| 268 | "--crate-type=lib".to_string(), |
| 269 | format!("--sysroot={}", get_sysroot_for_testing().display()), |
| 270 | rs_input_path.display().to_string(), |
| 271 | ]); |
| 272 | args.extend(self.extra_rustc_args.iter().cloned()); |
| 273 | |
| 274 | run_with_cmdline_args(&args)?; |
| 275 | |
| 276 | Ok(h_path) |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | #[test] |
| 281 | fn test_happy_path() -> anyhow::Result<()> { |
| 282 | let test_args = TestArgs::default_args()?; |
| 283 | let h_path = test_args.run().expect("Default args should succeed"); |
| 284 | |
| 285 | assert!(h_path.exists()); |
| 286 | let h_body = std::fs::read_to_string(&h_path)?; |
| 287 | assert_eq!( |
| 288 | h_body, |
| 289 | "// Automatically @generated C++ bindings for the following Rust crate:\n\ |
| 290 | // test_crate\n\ |
| 291 | \n\ |
| 292 | // List of public functions:\n\ |
| 293 | // public_function\n" |
| 294 | ); |
| 295 | Ok(()) |
| 296 | } |
| 297 | |
| 298 | #[test] |
| 299 | fn test_cmdline_error_propagation() -> anyhow::Result<()> { |
| 300 | // Tests that errors from `Cmdline::new` get propagated. Broader coverage of |
| 301 | // various error types can be found in tests in `cmdline.rs`. |
| 302 | let err = TestArgs::default_args()? |
| 303 | .with_extra_crubit_args(["--unrecognized-crubit-flag"]) |
| 304 | .run() |
| 305 | .expect_err("--unrecognized_crubit_flag should trigger an error"); |
| 306 | |
| 307 | let msg = err.to_string(); |
| 308 | assert_eq!("Unrecognized option: 'unrecognized-crubit-flag'", msg); |
| 309 | Ok(()) |
| 310 | } |
| 311 | |
| 312 | #[test] |
| 313 | fn test_rustc_error_propagation() -> anyhow::Result<()> { |
| 314 | // Tests that `rustc` errors are propagated. |
| 315 | let err = TestArgs::default_args()? |
| 316 | .with_extra_rustc_args(["--unrecognized-rustc-flag"]) |
| 317 | .run() |
| 318 | .expect_err("--unrecognized-rustc-flag should trigger an error"); |
| 319 | |
| 320 | let msg = err.to_string(); |
| 321 | assert_eq!("Errors reported by Rust compiler.", msg); |
| 322 | Ok(()) |
| 323 | } |
| 324 | |
| 325 | #[test] |
| 326 | fn test_invalid_h_out_path() -> anyhow::Result<()> { |
| 327 | // Tests not only the specific problem of an invalid `--h_out` argument, but |
| 328 | // also tests that errors from `bindings_main::main` are propagated. |
| 329 | let err = TestArgs::default_args()? |
| 330 | .with_h_path("../..") |
| 331 | .run() |
| 332 | .expect_err("Unwriteable --h_out should trigger an error"); |
| 333 | |
| 334 | let msg = err.to_string(); |
| 335 | assert_eq!("Error when writing to ../..", msg); |
| 336 | Ok(()) |
| 337 | } |
| 338 | |
| 339 | #[test] |
| 340 | fn test_no_output_file() -> anyhow::Result<()> { |
| 341 | // Tests that we stop the compilation midway. |
| 342 | let tmpdir = tempdir()?; |
| 343 | let out_path = tmpdir.path().join("unexpected_output.o"); |
| 344 | TestArgs::default_args()? |
| 345 | .with_extra_rustc_args(vec!["-o", &out_path.display().to_string()]) |
| 346 | .run() |
| 347 | .expect("No rustc or Crubit errors are expected in this test"); |
| 348 | |
| 349 | assert!(!out_path.exists()); |
| 350 | Ok(()) |
| 351 | } |
Lukasz Anforowicz | bda1cfe | 2022-09-20 06:25:43 -0700 | [diff] [blame] | 352 | } |