Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame^] | 1 | // Copyright 2020 Google LLC |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| 4 | // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| 5 | // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
| 6 | // option. This file may not be copied, modified, or distributed |
| 7 | // except according to those terms. |
| 8 | |
| 9 | #![forbid(unsafe_code)] |
| 10 | |
| 11 | mod depfile; |
| 12 | |
| 13 | use autocxx_engine::{ |
| 14 | generate_rs_archive, generate_rs_single, parse_file, AutocxxgenHeaderNamer, CxxgenHeaderNamer, |
| 15 | RebuildDependencyRecorder, |
| 16 | }; |
| 17 | use clap::{crate_authors, crate_version, Arg, ArgGroup, Command}; |
| 18 | use depfile::Depfile; |
| 19 | use indexmap::IndexSet; |
| 20 | use miette::IntoDiagnostic; |
| 21 | use std::cell::RefCell; |
| 22 | use std::io::{Read, Write}; |
| 23 | use std::path::PathBuf; |
| 24 | use std::rc::Rc; |
| 25 | use std::{cell::Cell, fs::File, path::Path}; |
| 26 | |
| 27 | pub(crate) static BLANK: &str = "// Blank autocxx placeholder"; |
| 28 | |
| 29 | static LONG_HELP: &str = " |
| 30 | Command line utility to expand the Rust 'autocxx' include_cpp! directive. |
| 31 | |
| 32 | This tool can generate both the C++ and Rust side binding code for |
| 33 | a Rust file containing an include_cpp! directive. |
| 34 | |
| 35 | If you're using cargo, don't use this: use autocxx_build instead, |
| 36 | which is much easier to include in build.rs build scripts. You'd likely |
| 37 | use this tool only if you're using some non-Cargo build system. If |
| 38 | that's you, read on. |
| 39 | |
| 40 | This tool has three modes: generate the C++; or generate |
| 41 | a Rust file which can be included by the autocxx_macro; or generate an archive |
| 42 | containing multiple Rust files to be expanded by different autocxx macros. |
| 43 | You may specify multiple modes, or of course, invoke the tool multiple times. |
| 44 | |
| 45 | In any mode, you'll need to pass the source Rust file name and the C++ |
| 46 | include path. You may pass multiple Rust files, each of which may contain |
| 47 | multiple include_cpp! or cxx::bridge macros. |
| 48 | |
| 49 | There are three basic ways to use this tool, depending on the flexibility |
| 50 | of your build system. |
| 51 | |
| 52 | Does your build system require fixed output filenames, or can it enumerate |
| 53 | whatever files are generated? |
| 54 | |
| 55 | If it's flexible, then use |
| 56 | --gen-rs-include --gen-cpp |
| 57 | An arbitrary number of .h, .cc and .rs files will be generated, depending |
| 58 | on how many cxx::bridge and include_cpp macros are encountered and their contents. |
| 59 | When building the rust code, simply ensure that AUTOCXX_RS or OUT_DIR is set to |
| 60 | teach rustc where to find these .rs files. |
| 61 | |
| 62 | If your build system needs to be told exactly what C++ files are generated, |
| 63 | additionally use --generate-exact <N> You are then guaranteed to get |
| 64 | exactly 'n' files as follows: |
| 65 | gen<n>.h |
| 66 | autocxxgen<n>.h |
| 67 | gen<n>.cc |
| 68 | Some of them may be blank. If the tool finds too many include_cpp or cxx::bridge |
| 69 | macros to fit within that allowance, the build will fail. |
| 70 | |
| 71 | If your build system additionally requires that Rust files have fixed |
| 72 | filenames, then you should use |
| 73 | --gen-rs-archive |
| 74 | instead of |
| 75 | --gen-rs-include |
| 76 | and you will need to give AUTOCXX_RS_JSON_ARCHIVE when building the Rust code. |
| 77 | The output filename is named gen.rs.json. |
| 78 | |
| 79 | This teaches rustc (and the autocxx macro) that all the different Rust bindings |
| 80 | for multiple different autocxx macros have been archived into this single file. |
| 81 | "; |
| 82 | |
| 83 | fn main() -> miette::Result<()> { |
| 84 | let matches = Command::new("autocxx-gen") |
| 85 | .version(crate_version!()) |
| 86 | .author(crate_authors!()) |
| 87 | .about("Generates bindings files from Rust files that contain include_cpp! macros") |
| 88 | .long_about(LONG_HELP) |
| 89 | .arg( |
| 90 | Arg::new("INPUT") |
| 91 | .help("Sets the input .rs files to use") |
| 92 | .required(true) |
| 93 | .multiple_occurrences(true) |
| 94 | ) |
| 95 | .arg( |
| 96 | Arg::new("outdir") |
| 97 | .short('o') |
| 98 | .long("outdir") |
| 99 | .allow_invalid_utf8(true) |
| 100 | .value_name("PATH") |
| 101 | .help("output directory path") |
| 102 | .takes_value(true) |
| 103 | .required(true), |
| 104 | ) |
| 105 | .arg( |
| 106 | Arg::new("inc") |
| 107 | .short('I') |
| 108 | .long("inc") |
| 109 | .multiple_occurrences(true) |
| 110 | .number_of_values(1) |
| 111 | .value_name("INCLUDE DIRS") |
| 112 | .help("include path") |
| 113 | .takes_value(true), |
| 114 | ) |
| 115 | .arg( |
| 116 | Arg::new("cpp-extension") |
| 117 | .long("cpp-extension") |
| 118 | .value_name("EXTENSION") |
| 119 | .default_value("cc") |
| 120 | .help("C++ filename extension") |
| 121 | .takes_value(true), |
| 122 | ) |
| 123 | .arg( |
| 124 | Arg::new("gen-cpp") |
| 125 | .long("gen-cpp") |
| 126 | .help("whether to generate C++ implementation and header files") |
| 127 | ) |
| 128 | .arg( |
| 129 | Arg::new("gen-rs-include") |
| 130 | .long("gen-rs-include") |
| 131 | .help("whether to generate Rust files for inclusion using autocxx_macro (suffix will be .include.rs)") |
| 132 | ) |
| 133 | .arg( |
| 134 | Arg::new("gen-rs-archive") |
| 135 | .long("gen-rs-archive") |
| 136 | .help("whether to generate an archive of multiple sets of Rust bindings for use by autocxx_macro (suffix will be .rs.json)") |
| 137 | ) |
| 138 | .group(ArgGroup::new("mode") |
| 139 | .required(true) |
| 140 | .multiple(true) |
| 141 | .arg("gen-cpp") |
| 142 | .arg("gen-rs-include") |
| 143 | .arg("gen-rs-archive") |
| 144 | ) |
| 145 | .arg( |
| 146 | Arg::new("generate-exact") |
| 147 | .long("generate-exact") |
| 148 | .value_name("NUM") |
| 149 | .help("assume and ensure there are exactly NUM bridge blocks in the file. Only applies for --gen-cpp or --gen-rs-include") |
| 150 | .takes_value(true), |
| 151 | ) |
| 152 | .arg( |
| 153 | Arg::new("fix-rs-include-name") |
| 154 | .long("fix-rs-include-name") |
| 155 | .help("Make the name of the .rs file predictable. You must set AUTOCXX_RS_FILE during Rust build time to educate autocxx_macro about your choice.") |
| 156 | .requires("gen-rs-include") |
| 157 | ) |
| 158 | .arg( |
| 159 | Arg::new("auto-allowlist") |
| 160 | .long("auto-allowlist") |
| 161 | .help("Dynamically construct allowlist from real uses of APIs.") |
| 162 | ) |
| 163 | .arg( |
| 164 | Arg::new("suppress-system-headers") |
| 165 | .long("suppress-system-headers") |
| 166 | .help("Do not refer to any system headers from generated code. May be useful for minimization.") |
| 167 | ) |
| 168 | .arg( |
| 169 | Arg::new("cxx-impl-annotations") |
| 170 | .long("cxx-impl-annotations") |
| 171 | .value_name("ANNOTATION") |
| 172 | .help("prefix for symbols to be exported from C++ bindings, e.g. __attribute__ ((visibility (\"default\")))") |
| 173 | .takes_value(true), |
| 174 | ) |
| 175 | .arg( |
| 176 | Arg::new("cxx-h-path") |
| 177 | .long("cxx-h-path") |
| 178 | .value_name("PREFIX") |
| 179 | .help("prefix for path to cxx.h (from the cxx crate) within #include statements. Must end in /") |
| 180 | .takes_value(true), |
| 181 | ) |
| 182 | .arg( |
| 183 | Arg::new("cxxgen-h-path") |
| 184 | .long("cxxgen-h-path") |
| 185 | .value_name("PREFIX") |
| 186 | .help("prefix for path to cxxgen.h (which we generate into the output directory) within #include statements. Must end in /") |
| 187 | .takes_value(true), |
| 188 | ) |
| 189 | .arg( |
| 190 | Arg::new("depfile") |
| 191 | .long("depfile") |
| 192 | .value_name("DEPFILE") |
| 193 | .help("A .d file to write") |
| 194 | .takes_value(true), |
| 195 | ) |
| 196 | .arg( |
| 197 | Arg::new("clang-args") |
| 198 | .last(true) |
| 199 | .multiple_occurrences(true) |
| 200 | .help("Extra arguments to pass to Clang"), |
| 201 | ) |
| 202 | .get_matches(); |
| 203 | |
| 204 | env_logger::builder().init(); |
| 205 | let incs = matches |
| 206 | .values_of("inc") |
| 207 | .unwrap_or_default() |
| 208 | .map(PathBuf::from) |
| 209 | .collect::<Vec<_>>(); |
| 210 | let extra_clang_args: Vec<_> = matches |
| 211 | .values_of("clang-args") |
| 212 | .unwrap_or_default() |
| 213 | .collect(); |
| 214 | let suppress_system_headers = matches.is_present("suppress-system-headers"); |
| 215 | let desired_number = matches |
| 216 | .value_of("generate-exact") |
| 217 | .map(|s| s.parse::<usize>().unwrap()); |
| 218 | let autocxxgen_header_counter = Cell::new(0); |
| 219 | let autocxxgen_header_namer = if desired_number.is_some() { |
| 220 | AutocxxgenHeaderNamer(Box::new(|_| { |
| 221 | let r = name_autocxxgen_h(autocxxgen_header_counter.get()); |
| 222 | autocxxgen_header_counter.set(autocxxgen_header_counter.get() + 1); |
| 223 | r |
| 224 | })) |
| 225 | } else { |
| 226 | Default::default() |
| 227 | }; |
| 228 | let cxxgen_header_counter = Cell::new(0); |
| 229 | let cxxgen_header_namer = if desired_number.is_some() { |
| 230 | CxxgenHeaderNamer(Box::new(|| { |
| 231 | let r = name_cxxgen_h(cxxgen_header_counter.get()); |
| 232 | cxxgen_header_counter.set(cxxgen_header_counter.get() + 1); |
| 233 | r |
| 234 | })) |
| 235 | } else { |
| 236 | Default::default() |
| 237 | }; |
| 238 | let cpp_codegen_options = autocxx_engine::CppCodegenOptions { |
| 239 | suppress_system_headers, |
| 240 | cxx_impl_annotations: get_option_string("cxx-impl-annotations", &matches), |
| 241 | path_to_cxx_h: get_option_string("cxx-h-path", &matches), |
| 242 | path_to_cxxgen_h: get_option_string("cxxgen-h-path", &matches), |
| 243 | autocxxgen_header_namer, |
| 244 | cxxgen_header_namer, |
| 245 | }; |
| 246 | let depfile = match matches.value_of("depfile") { |
| 247 | None => None, |
| 248 | Some(depfile_path) => { |
| 249 | let depfile_path = PathBuf::from(depfile_path); |
| 250 | Some(Rc::new(RefCell::new( |
| 251 | Depfile::new(&depfile_path).into_diagnostic()?, |
| 252 | ))) |
| 253 | } |
| 254 | }; |
| 255 | let auto_allowlist = matches.is_present("auto-allowlist"); |
| 256 | |
| 257 | let mut parsed_files = Vec::new(); |
| 258 | for input in matches.values_of("INPUT").expect("No INPUT was provided") { |
| 259 | // Parse all the .rs files we're asked to process, first. |
| 260 | // Spot any fundamental parsing or command line problems before we start |
| 261 | // to do the complex processing. |
| 262 | let parsed_file = parse_file(input, auto_allowlist)?; |
| 263 | parsed_files.push(parsed_file); |
| 264 | } |
| 265 | |
| 266 | for parsed_file in parsed_files.iter_mut() { |
| 267 | // Now actually handle all the include_cpp directives we found, |
| 268 | // which is the complex bit where we interpret all the C+. |
| 269 | let dep_recorder: Option<Box<dyn RebuildDependencyRecorder>> = depfile |
| 270 | .as_ref() |
| 271 | .map(|rc| get_dependency_recorder(rc.clone())); |
| 272 | parsed_file.resolve_all( |
| 273 | incs.clone(), |
| 274 | &extra_clang_args, |
| 275 | dep_recorder, |
| 276 | &cpp_codegen_options, |
| 277 | )?; |
| 278 | } |
| 279 | |
| 280 | // Finally start to write the C++ and Rust out. |
| 281 | let outdir: PathBuf = matches.value_of_os("outdir").unwrap().into(); |
| 282 | let mut writer = FileWriter { |
| 283 | depfile: &depfile, |
| 284 | outdir: &outdir, |
| 285 | written: IndexSet::new(), |
| 286 | }; |
| 287 | if matches.is_present("gen-cpp") { |
| 288 | let cpp = matches.value_of("cpp-extension").unwrap(); |
| 289 | let name_cc_file = |counter| format!("gen{}.{}", counter, cpp); |
| 290 | let mut counter = 0usize; |
| 291 | for include_cxx in parsed_files |
| 292 | .iter() |
| 293 | .flat_map(|file| file.get_cpp_buildables()) |
| 294 | { |
| 295 | let generations = include_cxx |
| 296 | .generate_h_and_cxx(&cpp_codegen_options) |
| 297 | .expect("Unable to generate header and C++ code"); |
| 298 | for pair in generations.0 { |
| 299 | let cppname = name_cc_file(counter); |
| 300 | writer.write_to_file(cppname, &pair.implementation.unwrap_or_default())?; |
| 301 | writer.write_to_file(pair.header_name, &pair.header)?; |
| 302 | counter += 1; |
| 303 | } |
| 304 | } |
| 305 | drop(cpp_codegen_options); |
| 306 | // Write placeholders to ensure we always make exactly 'n' of each file type. |
| 307 | writer.write_placeholders(counter, desired_number, name_cc_file)?; |
| 308 | writer.write_placeholders( |
| 309 | cxxgen_header_counter.into_inner(), |
| 310 | desired_number, |
| 311 | name_cxxgen_h, |
| 312 | )?; |
| 313 | writer.write_placeholders( |
| 314 | autocxxgen_header_counter.into_inner(), |
| 315 | desired_number, |
| 316 | name_autocxxgen_h, |
| 317 | )?; |
| 318 | } |
| 319 | //writer.write_placeholders(header_counter.into_inner(), desired_number, "h")?; |
| 320 | if matches.is_present("gen-rs-include") { |
| 321 | let rust_buildables = parsed_files |
| 322 | .iter() |
| 323 | .flat_map(|parsed_file| parsed_file.get_rs_outputs()); |
| 324 | for (counter, include_cxx) in rust_buildables.enumerate() { |
| 325 | let rs_code = generate_rs_single(include_cxx); |
| 326 | let fname = if matches.is_present("fix-rs-include-name") { |
| 327 | format!("gen{}.include.rs", counter) |
| 328 | } else { |
| 329 | rs_code.filename |
| 330 | }; |
| 331 | writer.write_to_file(fname, rs_code.code.as_bytes())?; |
| 332 | } |
| 333 | } |
| 334 | if matches.is_present("gen-rs-archive") { |
| 335 | let rust_buildables = parsed_files |
| 336 | .iter() |
| 337 | .flat_map(|parsed_file| parsed_file.get_rs_outputs()); |
| 338 | let json = generate_rs_archive(rust_buildables); |
| 339 | eprintln!("Writing to gen.rs.json in {:?}", outdir); |
| 340 | writer.write_to_file("gen.rs.json".into(), json.as_bytes())?; |
| 341 | } |
| 342 | if let Some(depfile) = depfile { |
| 343 | depfile.borrow_mut().write().into_diagnostic()?; |
| 344 | } |
| 345 | Ok(()) |
| 346 | } |
| 347 | |
| 348 | fn name_autocxxgen_h(counter: usize) -> String { |
| 349 | format!("autocxxgen{}.h", counter) |
| 350 | } |
| 351 | |
| 352 | fn name_cxxgen_h(counter: usize) -> String { |
| 353 | format!("gen{}.h", counter) |
| 354 | } |
| 355 | |
| 356 | fn get_dependency_recorder(depfile: Rc<RefCell<Depfile>>) -> Box<dyn RebuildDependencyRecorder> { |
| 357 | Box::new(RecordIntoDepfile(depfile)) |
| 358 | } |
| 359 | |
| 360 | fn get_option_string(option: &str, matches: &clap::ArgMatches) -> Option<String> { |
| 361 | let cxx_impl_annotations = matches.value_of(option).map(|s| s.to_string()); |
| 362 | cxx_impl_annotations |
| 363 | } |
| 364 | |
| 365 | struct FileWriter<'a> { |
| 366 | depfile: &'a Option<Rc<RefCell<Depfile>>>, |
| 367 | outdir: &'a Path, |
| 368 | written: IndexSet<String>, |
| 369 | } |
| 370 | |
| 371 | impl<'a> FileWriter<'a> { |
| 372 | fn write_placeholders<F: FnOnce(usize) -> String + Copy>( |
| 373 | &mut self, |
| 374 | mut counter: usize, |
| 375 | desired_number: Option<usize>, |
| 376 | filename: F, |
| 377 | ) -> miette::Result<()> { |
| 378 | if let Some(desired_number) = desired_number { |
| 379 | if counter > desired_number { |
| 380 | return Err(miette::Report::msg("More files were generated than expected. Increase the value passed to --generate-exact or reduce the number of include_cpp! sections.")); |
| 381 | } |
| 382 | while counter < desired_number { |
| 383 | let fname = filename(counter); |
| 384 | self.write_to_file(fname, BLANK.as_bytes())?; |
| 385 | counter += 1; |
| 386 | } |
| 387 | } |
| 388 | Ok(()) |
| 389 | } |
| 390 | |
| 391 | fn write_to_file(&mut self, filename: String, content: &[u8]) -> miette::Result<()> { |
| 392 | let path = self.outdir.join(&filename); |
| 393 | if let Some(depfile) = self.depfile { |
| 394 | depfile.borrow_mut().add_output(&path); |
| 395 | } |
| 396 | { |
| 397 | let f = File::open(&path); |
| 398 | if let Ok(mut f) = f { |
| 399 | let mut existing_content = Vec::new(); |
| 400 | let r = f.read_to_end(&mut existing_content); |
| 401 | if r.is_ok() && existing_content == content { |
| 402 | return Ok(()); // don't change timestamp on existing file unnecessarily |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | let mut f = File::create(&path).into_diagnostic()?; |
| 407 | f.write_all(content).into_diagnostic()?; |
| 408 | if self.written.contains(&filename) { |
| 409 | return Err(miette::Report::msg(format!("autocxx_gen would write two files entitled '{}' which would have conflicting contents. Consider using --generate-exact.", filename))); |
| 410 | } |
| 411 | self.written.insert(filename); |
| 412 | Ok(()) |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | struct RecordIntoDepfile(Rc<RefCell<Depfile>>); |
| 417 | |
| 418 | impl RebuildDependencyRecorder for RecordIntoDepfile { |
| 419 | fn record_header_file_dependency(&self, filename: &str) { |
| 420 | self.0.borrow_mut().add_dependency(&PathBuf::from(filename)) |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | impl std::fmt::Debug for RecordIntoDepfile { |
| 425 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 426 | write!(f, "<depfile>") |
| 427 | } |
| 428 | } |