blob: d0ad46002490bd3b6cce27698163b22d5ee0170c [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(rustc_private)]
#![deny(rustc::internal)]
extern crate rustc_driver;
extern crate rustc_session;
use anyhow::{bail, ensure, Result};
use clap::Parser;
use rustc_session::config::ErrorOutputType;
use rustc_session::EarlyDiagCtxt;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[clap(name = "cc_bindings_from_rs")]
#[clap(about = "Generates C++ bindings for a Rust crate", long_about = None)]
pub struct Cmdline {
/// Output path for C++ header file with bindings.
#[clap(long, value_parser, value_name = "FILE")]
pub h_out: PathBuf,
/// Output path for Rust implementation of the bindings.
#[clap(long, value_parser, value_name = "FILE")]
pub rs_out: PathBuf,
/// This is the format to `#include` Crubit C++ support library headers,
/// using `{header}` as the placeholder. Example:
/// `<crubit/support/{header}>` will produce `#include
/// <crubit/support/hdr.h>`.
#[clap(long, value_parser = validate_crubit_support_path_format, value_name = "STRING")]
pub crubit_support_path_format: String,
/// Path to a clang-format executable that will be used to format the
/// C++ header files generated by the tool.
#[clap(long, value_parser, value_name = "FILE")]
pub clang_format_exe_path: PathBuf,
/// Include paths of bindings for dependencies of the current crate
/// (generated by previous invocations of the tool).
/// Example: "--bindings-from-dependency=foo=some/path/foo_cc_api.h".
#[clap(long = "bindings-from-dependency", value_parser = parse_bindings_from_dependency,
value_name = "CRATE_NAME=INCLUDE_PATH")]
// TODO(b/271857814): A `CRATE_NAME` might not be globally unique - the key needs to also cover
// a "hash" of the crate version and compilation flags.
pub bindings_from_dependencies: Vec<(String, String)>,
/// Path to a rustfmt executable that will be used to format the
/// Rust source files generated by the tool.
#[clap(long, value_parser, value_name = "FILE")]
pub rustfmt_exe_path: PathBuf,
/// Path to a rustfmt.toml file that should replace the
/// default formatting of the .rs files generated by the tool.
#[clap(long, value_parser, value_name = "FILE")]
pub rustfmt_config_path: Option<PathBuf>,
/// Command line arguments of the Rust compiler.
#[clap(last = true, value_parser)]
pub rustc_args: Vec<String>,
/// Path to the error reporting output file.
#[clap(long, value_parser, value_name = "FILE")]
pub error_report_out: Option<PathBuf>,
}
impl Cmdline {
pub fn new(args: &[String]) -> Result<Self> {
assert_ne!(
0,
args.len(),
"`args` should include the name of the executable (i.e. argsv[0])"
);
let exe_name = args[0].clone();
let early_error_handler = EarlyDiagCtxt::new(ErrorOutputType::default());
// Ensure that `@file` expansion also covers *our* args.
//
// TODO(b/254688847): Decide whether to replace this with a `clap`-declared,
// `--help`-exposed `--flagfile <path>`.
let args = match rustc_driver::args::arg_expand_all(&early_error_handler, args) {
Ok(args) => args,
Err(_) => bail!("Error from rustc_driver::args::arg_expand_all"),
};
// Parse `args` using the parser `derive`d by the `clap` crate.
let mut cmdline = Self::try_parse_from(args)?;
// For compatibility with `rustc_driver` expectations, we prepend `exe_name` to
// `rustc_args. This is needed, because `rustc_driver::RunCompiler::new`
// expects that its `at_args` includes the name of the executable -
// `handle_options` in `rustc_driver/src/lib.rs` throws away the first
// element.
cmdline.rustc_args.insert(0, exe_name);
Ok(cmdline)
}
}
fn validate_crubit_support_path_format(s: &str) -> Result<String> {
ensure!(s.contains("{header}"), "Cannot find placeholder `{{header}}`");
Ok(s.to_string())
}
/// Parse cmdline arguments of the following form:`"crateName=includePath"`.
///
/// Adapted from
/// https://github.com/clap-rs/clap/blob/cc1474f97c78002f3d99261699114e61d70b0634/examples/typed-derive.rs#L47-L59
fn parse_bindings_from_dependency(s: &str) -> Result<(String, String)> {
let Some(pos) = s.find('=') else {
bail!("Expected KEY=VALUE syntax but no `=` found in `{s}`");
};
let crate_name = &s[..pos];
ensure!(!crate_name.is_empty(), "Empty crate names are invalid");
let include = &s[(pos + 1)..];
ensure!(!include.is_empty(), "Empty include paths are invalid");
Ok((crate_name.to_string(), include.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;
use std::path::Path;
use tempfile::tempdir;
fn new_cmdline<'a>(args: impl IntoIterator<Item = &'a str>) -> Result<Cmdline> {
// When `Cmdline::new` is invoked from `main.rs`, it includes not only the
// "real" cmdline arguments, but also the name of the executable.
let args = std::iter::once("cc_bindings_from_rs_unittest_executable")
.chain(args)
.map(|s| s.to_string())
.collect_vec();
Cmdline::new(&args)
}
#[test]
fn test_happy_path() {
let cmdline = new_cmdline([
"--h-out=foo.h",
"--rs-out=foo_impl.rs",
"--crubit-support-path-format=<crubit/support/{header}>",
"--clang-format-exe-path=clang-format.exe",
"--rustfmt-exe-path=rustfmt.exe",
])
.unwrap();
assert_eq!(Path::new("foo.h"), cmdline.h_out);
assert_eq!(Path::new("foo_impl.rs"), cmdline.rs_out);
assert_eq!("<crubit/support/{header}>", cmdline.crubit_support_path_format.as_str());
assert_eq!(Path::new("clang-format.exe"), cmdline.clang_format_exe_path);
assert_eq!(Path::new("rustfmt.exe"), cmdline.rustfmt_exe_path);
assert!(cmdline.bindings_from_dependencies.is_empty());
assert!(cmdline.rustfmt_config_path.is_none());
// Ignoring `rustc_args` in this test - they are covered in a separate
// test below: `test_rustc_args_happy_path`.
}
#[test]
fn test_rustc_args_happy_path() {
// Note that this test would fail without the `--` separator.
let cmdline = new_cmdline([
"--h-out=foo.h",
"--rs-out=foo_impl.rs",
"--crubit-support-path-format=<crubit/support/{header}>",
"--clang-format-exe-path=clang-format.exe",
"--rustfmt-exe-path=rustfmt.exe",
"--",
"test.rs",
"--crate-type=lib",
])
.unwrap();
let rustc_args = &cmdline.rustc_args;
assert!(
itertools::equal(
["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"],
rustc_args
),
"rustc_args = {:?}",
rustc_args,
);
}
/// The `test_help` unit test below has multiple purposes:
/// - Direct/obvious purpose: testing that `--help` works
/// - Double-checking the overall shape of our cmdline "API" (i.e.
/// verification that the way we use `clap` attributes results in the
/// desired cmdline "API"). This is a good enough coverage to avoid having
/// flag-specifc tests (e.g. avoiding hypothetical
/// `test_h_out_missing_flag`, `test_h_out_missing_arg`,
/// `test_h_out_duplicated`).
/// - Exhaustively checking runtime asserts (assumming that tests run in a
/// debug build; other tests also trigger these asserts). See also:
/// - https://github.com/clap-rs/clap/issues/2740#issuecomment-907240414
/// - `clap::builder::App::debug_assert`
///
/// To regenerate `expected_msg` do the following steps:
/// - Run `bazel run :cc_bindings_from_rs -- --help`
/// - Copy&paste the output of the command below
/// - Replace the 2nd `cc_bindings_from_rs` with
/// `cc_bindings_from_rs_unittest_executable`
#[test]
fn test_help() {
let anyhow_err = new_cmdline(["--help"]).expect_err("--help should trigger an error");
let clap_err = anyhow_err.downcast::<clap::Error>().unwrap();
let expected_msg = r#"Generates C++ bindings for a Rust crate
Usage: cc_bindings_from_rs_unittest_executable [OPTIONS] --h-out <FILE> --rs-out <FILE> --crubit-support-path-format <STRING> --clang-format-exe-path <FILE> --rustfmt-exe-path <FILE> [-- <RUSTC_ARGS>...]
Arguments:
[RUSTC_ARGS]... Command line arguments of the Rust compiler
Options:
--h-out <FILE>
Output path for C++ header file with bindings
--rs-out <FILE>
Output path for Rust implementation of the bindings
--crubit-support-path-format <STRING>
This is the format to `#include` Crubit C++ support library headers, using `{header}` as the placeholder. Example: `<crubit/support/{header}>` will produce `#include <crubit/support/hdr.h>`
--clang-format-exe-path <FILE>
Path to a clang-format executable that will be used to format the C++ header files generated by the tool
--bindings-from-dependency <CRATE_NAME=INCLUDE_PATH>
Include paths of bindings for dependencies of the current crate (generated by previous invocations of the tool). Example: "--bindings-from-dependency=foo=some/path/foo_cc_api.h"
--rustfmt-exe-path <FILE>
Path to a rustfmt executable that will be used to format the Rust source files generated by the tool
--rustfmt-config-path <FILE>
Path to a rustfmt.toml file that should replace the default formatting of the .rs files generated by the tool
--error-report-out <FILE>
Path to the error reporting output file
-h, --help
Print help
"#;
let actual_msg = clap_err.to_string();
assert_eq!(
expected_msg, actual_msg,
"Unexpected --help output\n\
EXPECTED OUTPUT:\n\
{expected_msg}\n\
ACTUAL OUTPUT:\n\
{actual_msg}"
);
}
#[test]
fn test_here_file() -> anyhow::Result<()> {
let tmpdir = tempdir()?;
let tmpfile = tmpdir.path().join("herefile");
let file_lines = vec![
"--h-out=foo.h",
"--rs-out=foo_impl.rs",
"--crubit-support-path-format=<crubit/support/{header}>",
"--clang-format-exe-path=clang-format.exe",
"--rustfmt-exe-path=rustfmt.exe",
"--",
"test.rs",
"--crate-type=lib",
];
std::fs::write(&tmpfile, file_lines.as_slice().join("\n"))?;
let flag_file_arg = format!("@{}", tmpfile.display());
let cmdline = new_cmdline([flag_file_arg.as_str()]).unwrap();
assert_eq!(Path::new("foo.h"), cmdline.h_out);
assert_eq!(Path::new("foo_impl.rs"), cmdline.rs_out);
let rustc_args = &cmdline.rustc_args;
assert!(
itertools::equal(
["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"],
rustc_args
),
"rustc_args = {:?}",
rustc_args,
);
Ok(())
}
#[test]
fn test_bindings_from_dependencies_as_multiple_separate_cmdline_args() {
let cmdline = new_cmdline([
"--h-out=foo.h",
"--rs-out=foo_impl.rs",
"--crubit-support-path-format=<crubit/support/{header}>",
"--clang-format-exe-path=clang-format.exe",
"--rustfmt-exe-path=rustfmt.exe",
"--bindings-from-dependency=dep1=path1",
"--bindings-from-dependency=dep2=path2",
])
.unwrap();
assert_eq!(2, cmdline.bindings_from_dependencies.len());
assert_eq!("dep1", cmdline.bindings_from_dependencies[0].0);
assert_eq!("path1", cmdline.bindings_from_dependencies[0].1);
assert_eq!("dep2", cmdline.bindings_from_dependencies[1].0);
assert_eq!("path2", cmdline.bindings_from_dependencies[1].1);
}
#[test]
fn test_parse_bindings_from_dependency() {
assert_eq!(
parse_bindings_from_dependency("foo=bar").unwrap(),
("foo".into(), "bar".into()),
);
assert_eq!(
parse_bindings_from_dependency("").unwrap_err().to_string(),
"Expected KEY=VALUE syntax but no `=` found in ``",
);
assert_eq!(
parse_bindings_from_dependency("no-equal-char").unwrap_err().to_string(),
"Expected KEY=VALUE syntax but no `=` found in `no-equal-char`",
);
assert_eq!(
parse_bindings_from_dependency("=bar").unwrap_err().to_string(),
"Empty crate names are invalid",
);
assert_eq!(
parse_bindings_from_dependency("foo=").unwrap_err().to_string(),
"Empty include paths are invalid",
);
}
#[test]
fn test_crubit_support_path_format_arg_happy_path() {
let cmdline = new_cmdline([
"--h-out=foo.h",
"--rs-out=foo_impl.rs",
"--crubit-support-path-format=<crubit/support/{header}>",
"--clang-format-exe-path=clang-format.exe",
"--rustfmt-exe-path=rustfmt.exe",
"--bindings-from-dependency=dep1=path1",
])
.unwrap();
assert_eq!("<crubit/support/{header}>", cmdline.crubit_support_path_format.as_str());
}
#[test]
fn test_crubit_support_path_format_arg_no_placeholder() {
let anyhow_err = new_cmdline([
"--h-out=foo.h",
"--rs-out=foo_impl.rs",
"--crubit-support-path-format=\"crubit/support\"",
"--clang-format-exe-path=clang-format.exe",
"--rustfmt-exe-path=rustfmt.exe",
"--bindings-from-dependency=dep1=path1",
])
.expect_err("crubit-support-path without `{header}` should trigger an error");
let clap_err = anyhow_err.downcast::<clap::Error>().unwrap();
let expected_msg = "Cannot find placeholder `{header}`";
assert!(clap_err.to_string().contains(expected_msg));
}
}