blob: 20f278b3a5df4189452be2c50a686c3167fc3f3c [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 }
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
348fn name_autocxxgen_h(counter: usize) -> String {
349 format!("autocxxgen{}.h", counter)
350}
351
352fn name_cxxgen_h(counter: usize) -> String {
353 format!("gen{}.h", counter)
354}
355
356fn get_dependency_recorder(depfile: Rc<RefCell<Depfile>>) -> Box<dyn RebuildDependencyRecorder> {
357 Box::new(RecordIntoDepfile(depfile))
358}
359
360fn 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
365struct FileWriter<'a> {
366 depfile: &'a Option<Rc<RefCell<Depfile>>>,
367 outdir: &'a Path,
368 written: IndexSet<String>,
369}
370
371impl<'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
416struct RecordIntoDepfile(Rc<RefCell<Depfile>>);
417
418impl 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
424impl std::fmt::Debug for RecordIntoDepfile {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 write!(f, "<depfile>")
427 }
428}