Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -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 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 5 | use anyhow::Result; |
| 6 | use clap::Parser; |
| 7 | use std::path::PathBuf; |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 8 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 9 | #[derive(Debug, Parser)] |
| 10 | #[clap(name = "cc_bindings_from_rs")] |
| 11 | #[clap(about = "Generates C++ bindings for a Rust crate", long_about = None)] |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 12 | pub struct Cmdline { |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 13 | /// Output path for C++ header file with bindings. |
| 14 | #[clap(long, value_parser, value_name = "FILE")] |
| 15 | pub h_out: PathBuf, |
| 16 | |
Lukasz Anforowicz | e7a2500 | 2022-11-10 06:21:42 -0800 | [diff] [blame] | 17 | /// Output path for Rust implementation of the bindings. |
| 18 | #[clap(long, value_parser, value_name = "FILE")] |
| 19 | pub rs_out: PathBuf, |
| 20 | |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 21 | /// Path to a clang-format executable that will be used to format the |
| 22 | /// C++ header files generated by the tool. |
| 23 | #[clap(long, value_parser, value_name = "FILE")] |
| 24 | pub clang_format_exe_path: PathBuf, |
| 25 | |
Lukasz Anforowicz | 5c081b2 | 2022-11-11 08:40:17 -0800 | [diff] [blame] | 26 | /// Path to a rustfmt executable that will be used to format the |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 27 | /// Rust source files generated by the tool. |
Lukasz Anforowicz | 5c081b2 | 2022-11-11 08:40:17 -0800 | [diff] [blame] | 28 | #[clap(long, value_parser, value_name = "FILE")] |
| 29 | pub rustfmt_exe_path: PathBuf, |
| 30 | |
| 31 | /// Path to a rustfmt.toml file that should replace the |
| 32 | /// default formatting of the .rs files generated by the tool. |
| 33 | #[clap(long, value_parser, value_name = "FILE")] |
| 34 | pub rustfmt_config_path: Option<PathBuf>, |
| 35 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 36 | /// Command line arguments of the Rust compiler. |
| 37 | #[clap(last = true, value_parser)] |
| 38 | pub rustc_args: Vec<String>, |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 39 | } |
| 40 | |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 41 | impl Cmdline { |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 42 | pub fn new(args: &[String]) -> Result<Self> { |
| 43 | assert_ne!( |
| 44 | 0, |
| 45 | args.len(), |
| 46 | "`args` should include the name of the executable (i.e. argsv[0])" |
| 47 | ); |
| 48 | let exe_name = args[0].clone(); |
| 49 | |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 50 | // Ensure that `@file` expansion also covers *our* args. |
Lukasz Anforowicz | 13794df | 2022-10-21 07:56:34 -0700 | [diff] [blame] | 51 | // |
| 52 | // TODO(b/254688847): Decide whether to replace this with a `clap`-declared, |
| 53 | // `--help`-exposed `--flagfile <path>`. |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 54 | let args = rustc_driver::args::arg_expand_all(args); |
| 55 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 56 | // Parse `args` using the parser `derive`d by the `clap` crate. |
| 57 | let mut cmdline = Self::try_parse_from(args)?; |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 58 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 59 | // For compatibility with `rustc_driver` expectations, we prepend `exe_name` to |
| 60 | // `rustc_args. This is needed, because `rustc_driver::RunCompiler::new` |
| 61 | // expects that its `at_args` includes the name of the executable - |
| 62 | // `handle_options` in `rustc_driver/src/lib.rs` throws away the first |
| 63 | // element. |
| 64 | cmdline.rustc_args.insert(0, exe_name); |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 65 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 66 | Ok(cmdline) |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 67 | } |
| 68 | } |
| 69 | |
| 70 | #[cfg(test)] |
| 71 | mod tests { |
| 72 | use super::*; |
| 73 | |
| 74 | use itertools::Itertools; |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 75 | use std::path::Path; |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 76 | use tempfile::tempdir; |
| 77 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 78 | fn new_cmdline<'a>(args: impl IntoIterator<Item = &'a str>) -> Result<Cmdline> { |
Lukasz Anforowicz | f018e4d | 2022-09-28 07:35:59 -0700 | [diff] [blame] | 79 | // When `Cmdline::new` is invoked from `main.rs`, it includes not only the |
| 80 | // "real" cmdline arguments, but also the name of the executable. |
| 81 | let args = std::iter::once("cc_bindings_from_rs_unittest_executable") |
| 82 | .chain(args) |
| 83 | .map(|s| s.to_string()) |
| 84 | .collect_vec(); |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 85 | Cmdline::new(&args) |
| 86 | } |
| 87 | |
| 88 | #[test] |
Lukasz Anforowicz | e7a2500 | 2022-11-10 06:21:42 -0800 | [diff] [blame] | 89 | fn test_happy_path() { |
Lukasz Anforowicz | 5c081b2 | 2022-11-11 08:40:17 -0800 | [diff] [blame] | 90 | let cmdline = new_cmdline([ |
| 91 | "--h-out=foo.h", |
| 92 | "--rs-out=foo_impl.rs", |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 93 | "--clang-format-exe-path=clang-format.exe", |
Lukasz Anforowicz | 5c081b2 | 2022-11-11 08:40:17 -0800 | [diff] [blame] | 94 | "--rustfmt-exe-path=rustfmt.exe", |
| 95 | ]) |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 96 | .unwrap(); |
| 97 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 98 | assert_eq!(Path::new("foo.h"), cmdline.h_out); |
Lukasz Anforowicz | e7a2500 | 2022-11-10 06:21:42 -0800 | [diff] [blame] | 99 | assert_eq!(Path::new("foo_impl.rs"), cmdline.rs_out); |
Lukasz Anforowicz | 5c081b2 | 2022-11-11 08:40:17 -0800 | [diff] [blame] | 100 | assert_eq!(Path::new("rustfmt.exe"), cmdline.rustfmt_exe_path); |
| 101 | assert!(cmdline.rustfmt_config_path.is_none()); |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 102 | } |
| 103 | |
| 104 | #[test] |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 105 | fn test_rustc_args_happy_path() { |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 106 | // Note that this test would fail without the `--` separator. |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 107 | let cmdline = new_cmdline([ |
| 108 | "--h-out=foo.h", |
| 109 | "--rs-out=foo_impl.rs", |
| 110 | "--clang-format-exe-path=clang-format.exe", |
| 111 | "--rustfmt-exe-path=rustfmt.exe", |
| 112 | "--", |
| 113 | "test.rs", |
| 114 | "--crate-type=lib", |
| 115 | ]) |
| 116 | .unwrap(); |
| 117 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 118 | let rustc_args = &cmdline.rustc_args; |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 119 | assert!( |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 120 | itertools::equal( |
| 121 | ["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"], |
| 122 | rustc_args |
| 123 | ), |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 124 | "rustc_args = {:?}", |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 125 | rustc_args, |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 126 | ); |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 127 | } |
| 128 | |
| 129 | #[test] |
| 130 | fn test_help() { |
| 131 | // This test has multiple purposes: |
| 132 | // - Direct/obvious purpose: testing that `--help` works |
Lukasz Anforowicz | ca87e47 | 2022-10-14 14:58:30 -0700 | [diff] [blame] | 133 | // - Double-checking the overall shape of our cmdline "API" (i.e. verification that the way |
| 134 | // we use `clap` attributes results in the desired cmdline "API"). This is a good enough |
| 135 | // coverage to avoid having flag-specifc tests (e.g. avoiding hypothetical |
| 136 | // `test_h_out_missing_flag`, `test_h_out_missing_arg`, `test_h_out_duplicated`). |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 137 | // - Exhaustively checking runtime asserts (assumming that tests run in a debug |
| 138 | // build; other tests also trigger these asserts). See also: |
| 139 | // - https://github.com/clap-rs/clap/issues/2740#issuecomment-907240414 |
| 140 | // - `clap::builder::App::debug_assert` |
Lukasz Anforowicz | e7a2500 | 2022-11-10 06:21:42 -0800 | [diff] [blame] | 141 | // |
| 142 | // To regenerate `expected_msg` do the following steps: |
| 143 | // - Run `bazel run :cc_bindings_from_rs_legacy_toolchain_runner -- --help` |
| 144 | // - Copy&paste the output of the command below |
| 145 | // - Replace the 2nd `cc_bindings_from_rs` with `cc_bindings_from_rs_unittest_executable` |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 146 | let anyhow_err = new_cmdline(["--help"]).expect_err("--help should trigger an error"); |
| 147 | let clap_err = anyhow_err.downcast::<clap::Error>().expect("Expecting `clap` error"); |
| 148 | let expected_msg = r#"cc_bindings_from_rs |
| 149 | Generates C++ bindings for a Rust crate |
| 150 | |
| 151 | USAGE: |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 152 | cc_bindings_from_rs_unittest_executable [OPTIONS] --h-out <FILE> --rs-out <FILE> --clang-format-exe-path <FILE> --rustfmt-exe-path <FILE> [-- <RUSTC_ARGS>...] |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 153 | |
| 154 | ARGS: |
| 155 | <RUSTC_ARGS>... Command line arguments of the Rust compiler |
| 156 | |
| 157 | OPTIONS: |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 158 | --clang-format-exe-path <FILE> |
| 159 | Path to a clang-format executable that will be used to format the C++ header files |
| 160 | generated by the tool |
| 161 | |
| 162 | --h-out <FILE> |
| 163 | Output path for C++ header file with bindings |
| 164 | |
| 165 | -h, --help |
| 166 | Print help information |
| 167 | |
| 168 | --rs-out <FILE> |
| 169 | Output path for Rust implementation of the bindings |
| 170 | |
| 171 | --rustfmt-config-path <FILE> |
| 172 | Path to a rustfmt.toml file that should replace the default formatting of the .rs files |
| 173 | generated by the tool |
| 174 | |
| 175 | --rustfmt-exe-path <FILE> |
| 176 | Path to a rustfmt executable that will be used to format the Rust source files generated |
| 177 | by the tool |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 178 | "#; |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 179 | let actual_msg = clap_err.to_string(); |
| 180 | assert_eq!( |
| 181 | expected_msg, actual_msg, |
| 182 | "Unexpected --help output\n\ |
| 183 | EXPECTED OUTPUT:\n\ |
| 184 | {expected_msg}\n\ |
| 185 | ACTUAL OUTPUT:\n\ |
| 186 | {actual_msg}" |
| 187 | ); |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | #[test] |
| 191 | fn test_here_file() -> anyhow::Result<()> { |
| 192 | let tmpdir = tempdir()?; |
| 193 | let tmpfile = tmpdir.path().join("herefile"); |
Lukasz Anforowicz | 5bf4943 | 2022-12-12 12:17:24 -0800 | [diff] [blame^] | 194 | let file_lines = vec![ |
| 195 | "--h-out=foo.h", |
| 196 | "--rs-out=foo_impl.rs", |
| 197 | "--clang-format-exe-path=clang-format.exe", |
| 198 | "--rustfmt-exe-path=rustfmt.exe", |
| 199 | "--", |
| 200 | "test.rs", |
| 201 | "--crate-type=lib", |
| 202 | ]; |
| 203 | std::fs::write(&tmpfile, file_lines.as_slice().join("\n"))?; |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 204 | |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 205 | let flag_file_arg = format!("@{}", tmpfile.display()); |
| 206 | let cmdline = new_cmdline([flag_file_arg.as_str()]).expect("No errors expected"); |
| 207 | assert_eq!(Path::new("foo.h"), cmdline.h_out); |
Lukasz Anforowicz | e7a2500 | 2022-11-10 06:21:42 -0800 | [diff] [blame] | 208 | assert_eq!(Path::new("foo_impl.rs"), cmdline.rs_out); |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 209 | let rustc_args = &cmdline.rustc_args; |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 210 | assert!( |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 211 | itertools::equal( |
| 212 | ["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"], |
| 213 | rustc_args), |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 214 | "rustc_args = {:?}", |
Lukasz Anforowicz | a18aab8 | 2022-09-29 11:18:06 -0700 | [diff] [blame] | 215 | rustc_args, |
Lukasz Anforowicz | 581fd75 | 2022-09-21 11:30:15 -0700 | [diff] [blame] | 216 | ); |
| 217 | Ok(()) |
| 218 | } |
| 219 | } |