blob: 8b109c302c2d042c110f4d3869f607d1cea078c1 [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::{
14 generate_rs_archive, generate_rs_single, parse_file, AutocxxgenHeaderNamer, CxxgenHeaderNamer,
15 RebuildDependencyRecorder,
16};
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.
77The output filename is named gen.rs.json.
78
79This teaches rustc (and the autocxx macro) that all the different Rust bindings
80for multiple different autocxx macros have been archived into this single file.
81";
82
83fn 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 }
Brian Silverman4e662aa2022-05-11 23:10:19 -0700319 if matches.is_present("gen-rs-include") {
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700320 if !matches.is_present("fix-rs-include-name") && desired_number.is_some() {
321 return Err(miette::Report::msg(
322 "gen-rs-include and generate-exact requires fix-rs-include-name.",
323 ));
324 }
325 let mut counter = 0usize;
Brian Silverman4e662aa2022-05-11 23:10:19 -0700326 let rust_buildables = parsed_files
327 .iter()
328 .flat_map(|parsed_file| parsed_file.get_rs_outputs());
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700329 for include_cxx in rust_buildables {
Brian Silverman4e662aa2022-05-11 23:10:19 -0700330 let rs_code = generate_rs_single(include_cxx);
331 let fname = if matches.is_present("fix-rs-include-name") {
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700332 name_include_rs(counter)
Brian Silverman4e662aa2022-05-11 23:10:19 -0700333 } else {
334 rs_code.filename
335 };
336 writer.write_to_file(fname, rs_code.code.as_bytes())?;
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700337 counter += 1;
Brian Silverman4e662aa2022-05-11 23:10:19 -0700338 }
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700339 writer.write_placeholders(counter, desired_number, name_include_rs)?;
Brian Silverman4e662aa2022-05-11 23:10:19 -0700340 }
341 if matches.is_present("gen-rs-archive") {
342 let rust_buildables = parsed_files
343 .iter()
344 .flat_map(|parsed_file| parsed_file.get_rs_outputs());
345 let json = generate_rs_archive(rust_buildables);
Brian Silverman4e662aa2022-05-11 23:10:19 -0700346 writer.write_to_file("gen.rs.json".into(), json.as_bytes())?;
347 }
348 if let Some(depfile) = depfile {
349 depfile.borrow_mut().write().into_diagnostic()?;
350 }
351 Ok(())
352}
353
354fn name_autocxxgen_h(counter: usize) -> String {
355 format!("autocxxgen{}.h", counter)
356}
357
358fn name_cxxgen_h(counter: usize) -> String {
359 format!("gen{}.h", counter)
360}
361
Brian Silvermanf3ec38b2022-07-06 20:43:36 -0700362fn name_include_rs(counter: usize) -> String {
363 format!("gen{}.include.rs", counter)
364}
365
Brian Silverman4e662aa2022-05-11 23:10:19 -0700366fn get_dependency_recorder(depfile: Rc<RefCell<Depfile>>) -> Box<dyn RebuildDependencyRecorder> {
367 Box::new(RecordIntoDepfile(depfile))
368}
369
370fn get_option_string(option: &str, matches: &clap::ArgMatches) -> Option<String> {
371 let cxx_impl_annotations = matches.value_of(option).map(|s| s.to_string());
372 cxx_impl_annotations
373}
374
375struct FileWriter<'a> {
376 depfile: &'a Option<Rc<RefCell<Depfile>>>,
377 outdir: &'a Path,
378 written: IndexSet<String>,
379}
380
381impl<'a> FileWriter<'a> {
382 fn write_placeholders<F: FnOnce(usize) -> String + Copy>(
383 &mut self,
384 mut counter: usize,
385 desired_number: Option<usize>,
386 filename: F,
387 ) -> miette::Result<()> {
388 if let Some(desired_number) = desired_number {
389 if counter > desired_number {
390 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."));
391 }
392 while counter < desired_number {
393 let fname = filename(counter);
394 self.write_to_file(fname, BLANK.as_bytes())?;
395 counter += 1;
396 }
397 }
398 Ok(())
399 }
400
401 fn write_to_file(&mut self, filename: String, content: &[u8]) -> miette::Result<()> {
402 let path = self.outdir.join(&filename);
403 if let Some(depfile) = self.depfile {
404 depfile.borrow_mut().add_output(&path);
405 }
406 {
407 let f = File::open(&path);
408 if let Ok(mut f) = f {
409 let mut existing_content = Vec::new();
410 let r = f.read_to_end(&mut existing_content);
411 if r.is_ok() && existing_content == content {
412 return Ok(()); // don't change timestamp on existing file unnecessarily
413 }
414 }
415 }
416 let mut f = File::create(&path).into_diagnostic()?;
417 f.write_all(content).into_diagnostic()?;
418 if self.written.contains(&filename) {
419 return Err(miette::Report::msg(format!("autocxx_gen would write two files entitled '{}' which would have conflicting contents. Consider using --generate-exact.", filename)));
420 }
421 self.written.insert(filename);
422 Ok(())
423 }
424}
425
426struct RecordIntoDepfile(Rc<RefCell<Depfile>>);
427
428impl RebuildDependencyRecorder for RecordIntoDepfile {
429 fn record_header_file_dependency(&self, filename: &str) {
430 self.0.borrow_mut().add_dependency(&PathBuf::from(filename))
431 }
432}
433
434impl std::fmt::Debug for RecordIntoDepfile {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 write!(f, "<depfile>")
437 }
438}