blob: 4d533ff147c1085238749f1082473d760b7efe81 [file] [log] [blame]
Brian Silverman4e662aa2022-05-11 23:10:19 -07001// 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
11mod depfile;
12
13use autocxx_engine::{
Austin Schuh6ea9bfa2023-08-06 19:05:10 -070014 generate_rs_archive, generate_rs_single, get_cxx_header_bytes, parse_file,
15 AutocxxgenHeaderNamer, CxxgenHeaderNamer, RebuildDependencyRecorder,
Brian Silverman4e662aa2022-05-11 23:10:19 -070016};
17use clap::{crate_authors, crate_version, Arg, ArgGroup, Command};
18use depfile::Depfile;
19use indexmap::IndexSet;
20use miette::IntoDiagnostic;
21use std::cell::RefCell;
22use std::io::{Read, Write};
23use std::path::PathBuf;
24use std::rc::Rc;
25use std::{cell::Cell, fs::File, path::Path};
26
27pub(crate) static BLANK: &str = "// Blank autocxx placeholder";
28
29static LONG_HELP: &str = "
30Command line utility to expand the Rust 'autocxx' include_cpp! directive.
31
32This tool can generate both the C++ and Rust side binding code for
33a Rust file containing an include_cpp! directive.
34
35If you're using cargo, don't use this: use autocxx_build instead,
36which is much easier to include in build.rs build scripts. You'd likely
37use this tool only if you're using some non-Cargo build system. If
38that's you, read on.
39
40This tool has three modes: generate the C++; or generate
41a Rust file which can be included by the autocxx_macro; or generate an archive
42containing multiple Rust files to be expanded by different autocxx macros.
43You may specify multiple modes, or of course, invoke the tool multiple times.
44
45In any mode, you'll need to pass the source Rust file name and the C++
46include path. You may pass multiple Rust files, each of which may contain
47multiple include_cpp! or cxx::bridge macros.
48
49There are three basic ways to use this tool, depending on the flexibility
50of your build system.
51
52Does your build system require fixed output filenames, or can it enumerate
53whatever files are generated?
54
55If it's flexible, then use
56 --gen-rs-include --gen-cpp
57An arbitrary number of .h, .cc and .rs files will be generated, depending
58on how many cxx::bridge and include_cpp macros are encountered and their contents.
59When building the rust code, simply ensure that AUTOCXX_RS or OUT_DIR is set to
60teach rustc where to find these .rs files.
61
62If your build system needs to be told exactly what C++ files are generated,
63additionally use --generate-exact <N> You are then guaranteed to get
64exactly 'n' files as follows:
65 gen<n>.h
66 autocxxgen<n>.h
67 gen<n>.cc
68Some of them may be blank. If the tool finds too many include_cpp or cxx::bridge
69macros to fit within that allowance, the build will fail.
70
71If your build system additionally requires that Rust files have fixed
72filenames, then you should use
73 --gen-rs-archive
74instead of
75 --gen-rs-include
76and you will need to give AUTOCXX_RS_JSON_ARCHIVE when building the Rust code.
Austin Schuh6ea9bfa2023-08-06 19:05:10 -070077The output filename is named gen.rs.json. AUTOCXX_RS_JSON_ARCHIVE should be set
78to the path to gen.rs.json. It may optionally have multiple paths separated the
79way as the PATH environment variable for the current platform, see
80[`std::env::split_paths`] for details. The first path which is successfully
81opened will be used.
Brian Silverman4e662aa2022-05-11 23:10:19 -070082
83This teaches rustc (and the autocxx macro) that all the different Rust bindings
84for multiple different autocxx macros have been archived into this single file.
85";
86
87fn main() -> miette::Result<()> {
88 let matches = Command::new("autocxx-gen")
89 .version(crate_version!())
90 .author(crate_authors!())
91 .about("Generates bindings files from Rust files that contain include_cpp! macros")
92 .long_about(LONG_HELP)
93 .arg(
94 Arg::new("INPUT")
95 .help("Sets the input .rs files to use")
96 .required(true)
97 .multiple_occurrences(true)
98 )
99 .arg(
100 Arg::new("outdir")
101 .short('o')
102 .long("outdir")
103 .allow_invalid_utf8(true)
104 .value_name("PATH")
105 .help("output directory path")
106 .takes_value(true)
107 .required(true),
108 )
109 .arg(
110 Arg::new("inc")
111 .short('I')
112 .long("inc")
113 .multiple_occurrences(true)
114 .number_of_values(1)
115 .value_name("INCLUDE DIRS")
116 .help("include path")
117 .takes_value(true),
118 )
119 .arg(
120 Arg::new("cpp-extension")
121 .long("cpp-extension")
122 .value_name("EXTENSION")
123 .default_value("cc")
124 .help("C++ filename extension")
125 .takes_value(true),
126 )
127 .arg(
128 Arg::new("gen-cpp")
129 .long("gen-cpp")
130 .help("whether to generate C++ implementation and header files")
131 )
132 .arg(
133 Arg::new("gen-rs-include")
134 .long("gen-rs-include")
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700135 .help("whether to generate Rust files for inclusion using autocxx_macro")
Brian Silverman4e662aa2022-05-11 23:10:19 -0700136 )
137 .arg(
138 Arg::new("gen-rs-archive")
139 .long("gen-rs-archive")
140 .help("whether to generate an archive of multiple sets of Rust bindings for use by autocxx_macro (suffix will be .rs.json)")
141 )
142 .group(ArgGroup::new("mode")
143 .required(true)
144 .multiple(true)
145 .arg("gen-cpp")
146 .arg("gen-rs-include")
147 .arg("gen-rs-archive")
148 )
149 .arg(
150 Arg::new("generate-exact")
151 .long("generate-exact")
152 .value_name("NUM")
153 .help("assume and ensure there are exactly NUM bridge blocks in the file. Only applies for --gen-cpp or --gen-rs-include")
154 .takes_value(true),
155 )
156 .arg(
157 Arg::new("fix-rs-include-name")
158 .long("fix-rs-include-name")
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700159 .help("Make the name of the .rs file predictable (suffix will be .include.rs). You must set AUTOCXX_RS_FILE during Rust build time to educate autocxx_macro about your choice.")
Brian Silverman4e662aa2022-05-11 23:10:19 -0700160 .requires("gen-rs-include")
161 )
162 .arg(
163 Arg::new("auto-allowlist")
164 .long("auto-allowlist")
165 .help("Dynamically construct allowlist from real uses of APIs.")
166 )
167 .arg(
168 Arg::new("suppress-system-headers")
169 .long("suppress-system-headers")
170 .help("Do not refer to any system headers from generated code. May be useful for minimization.")
171 )
172 .arg(
173 Arg::new("cxx-impl-annotations")
174 .long("cxx-impl-annotations")
175 .value_name("ANNOTATION")
176 .help("prefix for symbols to be exported from C++ bindings, e.g. __attribute__ ((visibility (\"default\")))")
177 .takes_value(true),
178 )
179 .arg(
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700180 Arg::new("generate-cxx-h")
181 .long("generate-cxx-h")
182 .help("whether to generate cxx.h header file. If you already knew where to find cxx.h, consider using --cxx-h-path")
183 )
184 .arg(
Brian Silverman4e662aa2022-05-11 23:10:19 -0700185 Arg::new("cxx-h-path")
186 .long("cxx-h-path")
187 .value_name("PREFIX")
188 .help("prefix for path to cxx.h (from the cxx crate) within #include statements. Must end in /")
189 .takes_value(true),
190 )
191 .arg(
192 Arg::new("cxxgen-h-path")
193 .long("cxxgen-h-path")
194 .value_name("PREFIX")
195 .help("prefix for path to cxxgen.h (which we generate into the output directory) within #include statements. Must end in /")
196 .takes_value(true),
197 )
198 .arg(
199 Arg::new("depfile")
200 .long("depfile")
201 .value_name("DEPFILE")
202 .help("A .d file to write")
203 .takes_value(true),
204 )
205 .arg(
206 Arg::new("clang-args")
207 .last(true)
208 .multiple_occurrences(true)
209 .help("Extra arguments to pass to Clang"),
210 )
211 .get_matches();
212
213 env_logger::builder().init();
214 let incs = matches
215 .values_of("inc")
216 .unwrap_or_default()
217 .map(PathBuf::from)
218 .collect::<Vec<_>>();
219 let extra_clang_args: Vec<_> = matches
220 .values_of("clang-args")
221 .unwrap_or_default()
222 .collect();
223 let suppress_system_headers = matches.is_present("suppress-system-headers");
224 let desired_number = matches
225 .value_of("generate-exact")
226 .map(|s| s.parse::<usize>().unwrap());
227 let autocxxgen_header_counter = Cell::new(0);
228 let autocxxgen_header_namer = if desired_number.is_some() {
229 AutocxxgenHeaderNamer(Box::new(|_| {
230 let r = name_autocxxgen_h(autocxxgen_header_counter.get());
231 autocxxgen_header_counter.set(autocxxgen_header_counter.get() + 1);
232 r
233 }))
234 } else {
235 Default::default()
236 };
237 let cxxgen_header_counter = Cell::new(0);
238 let cxxgen_header_namer = if desired_number.is_some() {
239 CxxgenHeaderNamer(Box::new(|| {
240 let r = name_cxxgen_h(cxxgen_header_counter.get());
241 cxxgen_header_counter.set(cxxgen_header_counter.get() + 1);
242 r
243 }))
244 } else {
245 Default::default()
246 };
247 let cpp_codegen_options = autocxx_engine::CppCodegenOptions {
248 suppress_system_headers,
249 cxx_impl_annotations: get_option_string("cxx-impl-annotations", &matches),
250 path_to_cxx_h: get_option_string("cxx-h-path", &matches),
251 path_to_cxxgen_h: get_option_string("cxxgen-h-path", &matches),
252 autocxxgen_header_namer,
253 cxxgen_header_namer,
254 };
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700255 let codegen_options = autocxx_engine::CodegenOptions {
256 cpp_codegen_options,
257 ..Default::default()
258 };
Brian Silverman4e662aa2022-05-11 23:10:19 -0700259 let depfile = match matches.value_of("depfile") {
260 None => None,
261 Some(depfile_path) => {
262 let depfile_path = PathBuf::from(depfile_path);
263 Some(Rc::new(RefCell::new(
264 Depfile::new(&depfile_path).into_diagnostic()?,
265 )))
266 }
267 };
268 let auto_allowlist = matches.is_present("auto-allowlist");
269
270 let mut parsed_files = Vec::new();
271 for input in matches.values_of("INPUT").expect("No INPUT was provided") {
272 // Parse all the .rs files we're asked to process, first.
273 // Spot any fundamental parsing or command line problems before we start
274 // to do the complex processing.
275 let parsed_file = parse_file(input, auto_allowlist)?;
276 parsed_files.push(parsed_file);
277 }
278
279 for parsed_file in parsed_files.iter_mut() {
280 // Now actually handle all the include_cpp directives we found,
281 // which is the complex bit where we interpret all the C+.
282 let dep_recorder: Option<Box<dyn RebuildDependencyRecorder>> = depfile
283 .as_ref()
284 .map(|rc| get_dependency_recorder(rc.clone()));
285 parsed_file.resolve_all(
286 incs.clone(),
287 &extra_clang_args,
288 dep_recorder,
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700289 &codegen_options,
Brian Silverman4e662aa2022-05-11 23:10:19 -0700290 )?;
291 }
292
293 // Finally start to write the C++ and Rust out.
294 let outdir: PathBuf = matches.value_of_os("outdir").unwrap().into();
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700295
296 if !outdir.exists() {
297 use miette::WrapErr as _;
298 std::fs::create_dir_all(&outdir)
299 .into_diagnostic()
300 .wrap_err_with(|| format!("Failed to create `outdir` '{}'", outdir.display()))?;
301 }
302
Brian Silverman4e662aa2022-05-11 23:10:19 -0700303 let mut writer = FileWriter {
304 depfile: &depfile,
305 outdir: &outdir,
306 written: IndexSet::new(),
307 };
308 if matches.is_present("gen-cpp") {
309 let cpp = matches.value_of("cpp-extension").unwrap();
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700310 let name_cc_file = |counter| format!("gen{counter}.{cpp}");
Brian Silverman4e662aa2022-05-11 23:10:19 -0700311 let mut counter = 0usize;
312 for include_cxx in parsed_files
313 .iter()
314 .flat_map(|file| file.get_cpp_buildables())
315 {
316 let generations = include_cxx
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700317 .generate_h_and_cxx(&codegen_options.cpp_codegen_options)
Brian Silverman4e662aa2022-05-11 23:10:19 -0700318 .expect("Unable to generate header and C++ code");
319 for pair in generations.0 {
320 let cppname = name_cc_file(counter);
321 writer.write_to_file(cppname, &pair.implementation.unwrap_or_default())?;
322 writer.write_to_file(pair.header_name, &pair.header)?;
323 counter += 1;
324 }
325 }
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700326 drop(codegen_options);
Brian Silverman4e662aa2022-05-11 23:10:19 -0700327 // Write placeholders to ensure we always make exactly 'n' of each file type.
328 writer.write_placeholders(counter, desired_number, name_cc_file)?;
329 writer.write_placeholders(
330 cxxgen_header_counter.into_inner(),
331 desired_number,
332 name_cxxgen_h,
333 )?;
334 writer.write_placeholders(
335 autocxxgen_header_counter.into_inner(),
336 desired_number,
337 name_autocxxgen_h,
338 )?;
339 }
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700340
341 if matches.is_present("generate-cxx-h") {
342 writer.write_to_file(
343 "cxx.h".to_string(),
344 &get_cxx_header_bytes(suppress_system_headers),
345 )?;
346 }
347
Brian Silverman4e662aa2022-05-11 23:10:19 -0700348 if matches.is_present("gen-rs-include") {
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700349 if !matches.is_present("fix-rs-include-name") && desired_number.is_some() {
350 return Err(miette::Report::msg(
351 "gen-rs-include and generate-exact requires fix-rs-include-name.",
352 ));
353 }
354 let mut counter = 0usize;
Brian Silverman4e662aa2022-05-11 23:10:19 -0700355 let rust_buildables = parsed_files
356 .iter()
357 .flat_map(|parsed_file| parsed_file.get_rs_outputs());
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700358 for include_cxx in rust_buildables {
Brian Silverman4e662aa2022-05-11 23:10:19 -0700359 let rs_code = generate_rs_single(include_cxx);
360 let fname = if matches.is_present("fix-rs-include-name") {
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700361 name_include_rs(counter)
Brian Silverman4e662aa2022-05-11 23:10:19 -0700362 } else {
363 rs_code.filename
364 };
365 writer.write_to_file(fname, rs_code.code.as_bytes())?;
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700366 counter += 1;
Brian Silverman4e662aa2022-05-11 23:10:19 -0700367 }
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700368 writer.write_placeholders(counter, desired_number, name_include_rs)?;
Brian Silverman4e662aa2022-05-11 23:10:19 -0700369 }
370 if matches.is_present("gen-rs-archive") {
371 let rust_buildables = parsed_files
372 .iter()
373 .flat_map(|parsed_file| parsed_file.get_rs_outputs());
374 let json = generate_rs_archive(rust_buildables);
Brian Silverman4e662aa2022-05-11 23:10:19 -0700375 writer.write_to_file("gen.rs.json".into(), json.as_bytes())?;
376 }
377 if let Some(depfile) = depfile {
378 depfile.borrow_mut().write().into_diagnostic()?;
379 }
380 Ok(())
381}
382
383fn name_autocxxgen_h(counter: usize) -> String {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700384 format!("autocxxgen{counter}.h")
Brian Silverman4e662aa2022-05-11 23:10:19 -0700385}
386
387fn name_cxxgen_h(counter: usize) -> String {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700388 format!("gen{counter}.h")
Brian Silverman4e662aa2022-05-11 23:10:19 -0700389}
390
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700391fn name_include_rs(counter: usize) -> String {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700392 format!("gen{counter}.include.rs")
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700393}
394
Brian Silverman4e662aa2022-05-11 23:10:19 -0700395fn get_dependency_recorder(depfile: Rc<RefCell<Depfile>>) -> Box<dyn RebuildDependencyRecorder> {
396 Box::new(RecordIntoDepfile(depfile))
397}
398
399fn get_option_string(option: &str, matches: &clap::ArgMatches) -> Option<String> {
400 let cxx_impl_annotations = matches.value_of(option).map(|s| s.to_string());
401 cxx_impl_annotations
402}
403
404struct FileWriter<'a> {
405 depfile: &'a Option<Rc<RefCell<Depfile>>>,
406 outdir: &'a Path,
407 written: IndexSet<String>,
408}
409
410impl<'a> FileWriter<'a> {
411 fn write_placeholders<F: FnOnce(usize) -> String + Copy>(
412 &mut self,
413 mut counter: usize,
414 desired_number: Option<usize>,
415 filename: F,
416 ) -> miette::Result<()> {
417 if let Some(desired_number) = desired_number {
418 if counter > desired_number {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700419 return Err(miette::Report::msg(format!("{counter} files were generated. Increase the value passed to --generate-exact or reduce the number of include_cpp! sections.")));
Brian Silverman4e662aa2022-05-11 23:10:19 -0700420 }
421 while counter < desired_number {
422 let fname = filename(counter);
423 self.write_to_file(fname, BLANK.as_bytes())?;
424 counter += 1;
425 }
426 }
427 Ok(())
428 }
429
430 fn write_to_file(&mut self, filename: String, content: &[u8]) -> miette::Result<()> {
431 let path = self.outdir.join(&filename);
432 if let Some(depfile) = self.depfile {
433 depfile.borrow_mut().add_output(&path);
434 }
435 {
436 let f = File::open(&path);
437 if let Ok(mut f) = f {
438 let mut existing_content = Vec::new();
439 let r = f.read_to_end(&mut existing_content);
440 if r.is_ok() && existing_content == content {
441 return Ok(()); // don't change timestamp on existing file unnecessarily
442 }
443 }
444 }
445 let mut f = File::create(&path).into_diagnostic()?;
446 f.write_all(content).into_diagnostic()?;
447 if self.written.contains(&filename) {
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700448 return Err(miette::Report::msg(format!("autocxx_gen would write two files entitled '{filename}' which would have conflicting contents. Consider using --generate-exact.")));
Brian Silverman4e662aa2022-05-11 23:10:19 -0700449 }
450 self.written.insert(filename);
451 Ok(())
452 }
453}
454
455struct RecordIntoDepfile(Rc<RefCell<Depfile>>);
456
457impl RebuildDependencyRecorder for RecordIntoDepfile {
458 fn record_header_file_dependency(&self, filename: &str) {
459 self.0.borrow_mut().add_dependency(&PathBuf::from(filename))
460 }
461}
462
463impl std::fmt::Debug for RecordIntoDepfile {
464 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465 write!(f, "<depfile>")
466 }
467}