blob: 1bddaab96afae94c6d736365201852e7950fa4a0 [file] [log] [blame]
Lukasz Anforowicz581fd752022-09-21 11:30:15 -07001// 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 Anforowicza18aab82022-09-29 11:18:06 -07005use anyhow::Result;
6use clap::Parser;
7use std::path::PathBuf;
Lukasz Anforowicz581fd752022-09-21 11:30:15 -07008
Lukasz Anforowicza18aab82022-09-29 11:18:06 -07009#[derive(Debug, Parser)]
10#[clap(name = "cc_bindings_from_rs")]
11#[clap(about = "Generates C++ bindings for a Rust crate", long_about = None)]
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070012pub struct Cmdline {
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070013 /// Output path for C++ header file with bindings.
14 #[clap(long, value_parser, value_name = "FILE")]
15 pub h_out: PathBuf,
16
Lukasz Anforowicze7a25002022-11-10 06:21:42 -080017 /// Output path for Rust implementation of the bindings.
18 #[clap(long, value_parser, value_name = "FILE")]
19 pub rs_out: PathBuf,
20
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -080021 /// 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 Anforowicz5c081b22022-11-11 08:40:17 -080026 /// Path to a rustfmt executable that will be used to format the
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -080027 /// Rust source files generated by the tool.
Lukasz Anforowicz5c081b22022-11-11 08:40:17 -080028 #[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 Anforowicza18aab82022-09-29 11:18:06 -070036 /// Command line arguments of the Rust compiler.
37 #[clap(last = true, value_parser)]
38 pub rustc_args: Vec<String>,
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070039}
40
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070041impl Cmdline {
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070042 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 Anforowicz581fd752022-09-21 11:30:15 -070050 // Ensure that `@file` expansion also covers *our* args.
Lukasz Anforowicz13794df2022-10-21 07:56:34 -070051 //
52 // TODO(b/254688847): Decide whether to replace this with a `clap`-declared,
53 // `--help`-exposed `--flagfile <path>`.
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070054 let args = rustc_driver::args::arg_expand_all(args);
55
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070056 // Parse `args` using the parser `derive`d by the `clap` crate.
57 let mut cmdline = Self::try_parse_from(args)?;
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070058
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070059 // 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 Anforowicz581fd752022-09-21 11:30:15 -070065
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070066 Ok(cmdline)
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070067 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 use itertools::Itertools;
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070075 use std::path::Path;
Lukasz Anforowicz581fd752022-09-21 11:30:15 -070076 use tempfile::tempdir;
77
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070078 fn new_cmdline<'a>(args: impl IntoIterator<Item = &'a str>) -> Result<Cmdline> {
Lukasz Anforowiczf018e4d2022-09-28 07:35:59 -070079 // 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 Anforowicz581fd752022-09-21 11:30:15 -070085 Cmdline::new(&args)
86 }
87
88 #[test]
Lukasz Anforowicze7a25002022-11-10 06:21:42 -080089 fn test_happy_path() {
Lukasz Anforowicz5c081b22022-11-11 08:40:17 -080090 let cmdline = new_cmdline([
91 "--h-out=foo.h",
92 "--rs-out=foo_impl.rs",
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -080093 "--clang-format-exe-path=clang-format.exe",
Lukasz Anforowicz5c081b22022-11-11 08:40:17 -080094 "--rustfmt-exe-path=rustfmt.exe",
95 ])
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -080096 .unwrap();
97
Lukasz Anforowicza18aab82022-09-29 11:18:06 -070098 assert_eq!(Path::new("foo.h"), cmdline.h_out);
Lukasz Anforowicze7a25002022-11-10 06:21:42 -080099 assert_eq!(Path::new("foo_impl.rs"), cmdline.rs_out);
Lukasz Anforowicz5c081b22022-11-11 08:40:17 -0800100 assert_eq!(Path::new("rustfmt.exe"), cmdline.rustfmt_exe_path);
101 assert!(cmdline.rustfmt_config_path.is_none());
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700102 }
103
104 #[test]
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700105 fn test_rustc_args_happy_path() {
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700106 // Note that this test would fail without the `--` separator.
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -0800107 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 Anforowicza18aab82022-09-29 11:18:06 -0700118 let rustc_args = &cmdline.rustc_args;
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700119 assert!(
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700120 itertools::equal(
121 ["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"],
122 rustc_args
123 ),
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700124 "rustc_args = {:?}",
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700125 rustc_args,
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700126 );
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700127 }
128
129 #[test]
130 fn test_help() {
131 // This test has multiple purposes:
132 // - Direct/obvious purpose: testing that `--help` works
Lukasz Anforowiczca87e472022-10-14 14:58:30 -0700133 // - 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 Anforowicza18aab82022-09-29 11:18:06 -0700137 // - 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 Anforowicze7a25002022-11-10 06:21:42 -0800141 //
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 Anforowicza18aab82022-09-29 11:18:06 -0700146 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
149Generates C++ bindings for a Rust crate
150
151USAGE:
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -0800152 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 Anforowicza18aab82022-09-29 11:18:06 -0700153
154ARGS:
155 <RUSTC_ARGS>... Command line arguments of the Rust compiler
156
157OPTIONS:
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -0800158 --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 Anforowicza18aab82022-09-29 11:18:06 -0700178"#;
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -0800179 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 Anforowicz581fd752022-09-21 11:30:15 -0700188 }
189
190 #[test]
191 fn test_here_file() -> anyhow::Result<()> {
192 let tmpdir = tempdir()?;
193 let tmpfile = tmpdir.path().join("herefile");
Lukasz Anforowicz5bf49432022-12-12 12:17:24 -0800194 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 Anforowicz581fd752022-09-21 11:30:15 -0700204
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700205 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 Anforowicze7a25002022-11-10 06:21:42 -0800208 assert_eq!(Path::new("foo_impl.rs"), cmdline.rs_out);
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700209 let rustc_args = &cmdline.rustc_args;
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700210 assert!(
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700211 itertools::equal(
212 ["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"],
213 rustc_args),
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700214 "rustc_args = {:?}",
Lukasz Anforowicza18aab82022-09-29 11:18:06 -0700215 rustc_args,
Lukasz Anforowicz581fd752022-09-21 11:30:15 -0700216 );
217 Ok(())
218 }
219}