blob: 310f9b3829428ede0f63284309960ac34d12f63b [file] [log] [blame] [edit]
// 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,
/// Include guard for the C++ header file with bindings.
#[clap(long, value_parser, value_name = "STRING")]
pub h_out_include_guard: Option<String>,
/// 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 dependency crates, generated by previous
/// invocations of Crubit. Keys are crate names, and values are include
/// paths. Example: "--crate-header=foo=some/path/foo_cc_api.h".
// TODO(b/262878759): Remove alias after next toolchain release.
#[clap(long = "crate-header", visible_alias = "bindings-from-dependency", value_parser = parse_crate_header,
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 crate_headers: Vec<(String, String)>,
/// The default feature flags enabled for all crates
#[clap(long = "default-features", value_parser = parse_features,
value_name = "CRUBIT_FEATURES", required=false, default_value = "")]
pub default_crate_features: flagset::FlagSet<crubit_feature::CrubitFeature>,
/// Feature flags enabled for a given crate. Keys are crate names, and
/// values are feature flags. All crates must have their features fully
/// specified, and consistently across rustc invocations, or the
/// behavior is undefined. As a special case, the crate name `self`
/// refers to the current crate, whose bindings are being generated.
///
/// Example: "--crate-feature=foo=experimental".
#[clap(long = "crate-feature", value_parser = parse_crate_feature,
value_name = "CRATE_NAME=CRUBIT_FEATURE")]
pub crate_features: Vec<(String, flagset::FlagSet<crubit_feature::CrubitFeature>)>,
/// Feature flags disabled for a given crate. Keys are crate names, and
/// values are feature flags. All crates must have their features fully
/// specified, and consistently across rustc invocations, or the
/// behavior is undefined. As a special case, the crate name `self`
/// refers to the current crate, whose bindings are being generated.
///
/// If a feature is specified in either `default-features` or
/// `crate-feature`, and also in `crate-disabled-feature`, the latter
/// takes precedence.
///
/// Example: "--crate-disabled-feature=foo=supported".
#[clap(long = "crate-disabled-feature", value_parser = parse_crate_feature,
value_name = "CRATE_NAME=CRUBIT_FEATURE")]
pub crate_disabled_features: Vec<(String, flagset::FlagSet<crubit_feature::CrubitFeature>)>,
/// 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>,
/// This is for golden tests only. Using this in production may cause
/// undefined behavior.
#[clap(long, value_parser, value_name = "BOOL")]
pub no_thunk_name_mangling: bool,
/// The top level namespace of the C++ bindings for a given crate. Keys are
/// crate names, and values are namespaces. Example:
/// "--crate-namespace=foo=a_namespace::b_namespace
#[clap(long = "crate-namespace", value_parser = parse_crate_namespace,
value_name = "CRATE_NAME=NAMESPACE")]
pub crate_namespaces: Vec<(String, String)>,
/// The name of a Rust crate and the new name that should be used within
/// the generated bindings.
#[clap(long = "crate-rename", value_parser = parse_key_value_pair,
value_name = "CRATE_NAME=RENAMED")]
pub crate_rename: Vec<(String, String)>,
/// The name of the crate to generate bindings for.
///
/// If provided, this value must correspond to the name of an extern crate provided to the
/// `rustc_args`. If not specified, the bindings will be generated for the crate being
/// compiled via the provided `rustc_args`.
#[clap(long, value_parser, value_name = "STRING")]
pub source_crate_name: Option<String>,
}
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 = rustc_driver::args::arg_expand_all(&early_error_handler, args);
// 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:`"crate_name=include_path"`.
///
/// Adapted from
/// https://github.com/clap-rs/clap/blob/cc1474f97c78002f3d99261699114e61d70b0634/examples/typed-derive.rs#L47-L59
fn parse_crate_header(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()))
}
fn parse_key_value_pair(s: &str) -> Result<(String, String)> {
let Some(pos) = s.find('=') else {
bail!("Expected KEY=VALUE syntax but no `=` found in `{s}`");
};
let key = &s[..pos];
ensure!(!key.is_empty(), "Empty key names are invalid");
let value = &s[(pos + 1)..];
ensure!(!value.is_empty(), "Empty values are invalid");
Ok((key.to_string(), value.to_string()))
}
fn parse_crate_namespace(s: &str) -> Result<(String, String)> {
parse_key_value_pair(s)
}
/// Parses features as a comma separated list.
fn parse_features(s: &str) -> Result<flagset::FlagSet<crubit_feature::CrubitFeature>> {
if s.is_empty() {
return Ok(Default::default());
}
let mut features = flagset::FlagSet::<crubit_feature::CrubitFeature>::default();
for feature in s.split(',') {
let Some(feature) = crubit_feature::named_features(feature.as_bytes()) else {
bail!("Invalid Crubit feature name: {feature:?}");
};
features |= feature;
}
Ok(features)
}
/// Parse cmdline arguments of the following form:`"crate_name=crubit_feature"`.
fn parse_crate_feature(
s: &str,
) -> Result<(String, flagset::FlagSet<crubit_feature::CrubitFeature>)> {
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 feature_name = &s[(pos + 1)..];
let Some(features) = crubit_feature::named_features(feature_name.as_bytes()) else {
bail!("Invalid Crubit feature name: {feature_name:?}");
};
Ok((crate_name.to_string(), features))
}
#[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.crate_headers.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,
);
}
#[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_crate_headers_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",
// The two names are interchangeable:
"--bindings-from-dependency=dep1=path1",
"--crate-header=dep2=path2",
"--bindings-from-dependency=dep3=path3",
"--crate-header=dep4=path4",
])
.unwrap();
assert_eq!(4, cmdline.crate_headers.len());
assert_eq!("dep1", cmdline.crate_headers[0].0);
assert_eq!("path1", cmdline.crate_headers[0].1);
assert_eq!("dep2", cmdline.crate_headers[1].0);
assert_eq!("path2", cmdline.crate_headers[1].1);
assert_eq!("dep3", cmdline.crate_headers[2].0);
assert_eq!("path3", cmdline.crate_headers[2].1);
assert_eq!("dep4", cmdline.crate_headers[3].0);
assert_eq!("path4", cmdline.crate_headers[3].1);
}
#[test]
fn test_parse_crate_header() {
assert_eq!(parse_crate_header("foo=bar").unwrap(), ("foo".into(), "bar".into()),);
assert_eq!(
parse_crate_header("").unwrap_err().to_string(),
"Expected KEY=VALUE syntax but no `=` found in ``",
);
assert_eq!(
parse_crate_header("no-equal-char").unwrap_err().to_string(),
"Expected KEY=VALUE syntax but no `=` found in `no-equal-char`",
);
assert_eq!(
parse_crate_header("=bar").unwrap_err().to_string(),
"Empty crate names are invalid",
);
assert_eq!(
parse_crate_header("foo=").unwrap_err().to_string(),
"Empty include paths are invalid",
);
}
#[test]
fn test_crate_features_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",
"--crate-feature=dep1=supported",
"--crate-feature=dep1=experimental",
"--crate-feature=dep2=experimental",
])
.unwrap();
assert_eq!(3, cmdline.crate_features.len());
assert_eq!("dep1", cmdline.crate_features[0].0);
assert_eq!(
flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
crubit_feature::CrubitFeature::Supported
),
cmdline.crate_features[0].1
);
assert_eq!("dep1", cmdline.crate_features[1].0);
assert_eq!(
flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
crubit_feature::CrubitFeature::Experimental
),
cmdline.crate_features[1].1
);
assert_eq!("dep2", cmdline.crate_features[2].0);
assert_eq!(
flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
crubit_feature::CrubitFeature::Experimental
),
cmdline.crate_features[2].1
);
}
#[test]
fn test_parse_crate_feature() {
assert_eq!(
parse_crate_feature("foo=supported").unwrap(),
(
"foo".into(),
flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
crubit_feature::CrubitFeature::Supported
)
),
);
assert_eq!(
parse_crate_feature("").unwrap_err().to_string(),
"Expected KEY=VALUE syntax but no `=` found in ``",
);
assert_eq!(
parse_crate_feature("no-equal-char").unwrap_err().to_string(),
"Expected KEY=VALUE syntax but no `=` found in `no-equal-char`",
);
assert_eq!(
parse_crate_feature("=bar").unwrap_err().to_string(),
"Empty crate names are invalid",
);
assert_eq!(
parse_crate_feature("foo=").unwrap_err().to_string(),
"Invalid Crubit feature name: \"\"",
);
}
#[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",
"--crate-header=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",
"--crate-header=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));
}
}