blob: d0b519918ef873ea9abc43edc0964af00c4140d7 [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
9use autocxx_parser::file_locations::FileLocationStrategy;
10use miette::Diagnostic;
11use thiserror::Error;
12
13use crate::generate_rs_single;
14use crate::{strip_system_headers, CppCodegenOptions, ParseError, RebuildDependencyRecorder};
15use std::ffi::OsStr;
16use std::ffi::OsString;
17use std::fs::File;
18use std::io::Write;
19use std::marker::PhantomData;
20use std::path::{Path, PathBuf};
21
22/// Errors returned during creation of a [`cc::Build`] from an include_cxx
23/// macro.
24#[derive(Error, Diagnostic, Debug)]
25#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
26pub enum BuilderError {
27 #[error("cxx couldn't handle our generated bindings - could be a bug in autocxx: {0}")]
28 InvalidCxx(cxx_gen::Error),
29 #[error(transparent)]
30 #[diagnostic(transparent)]
31 ParseError(ParseError),
32 #[error("we couldn't write the generated code to disk at {1}: {0}")]
33 FileWriteFail(std::io::Error, PathBuf),
34 #[error("no include_cpp! macro was found")]
35 NoIncludeCxxMacrosFound,
36 #[error("could not create a directory {1}: {0}")]
37 UnableToCreateDirectory(std::io::Error, PathBuf),
38}
39
40#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
41pub type BuilderBuild = cc::Build;
42
43/// For test purposes only, a [`cc::Build`] and lists of Rust and C++
44/// files generated.
45#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
46pub struct BuilderSuccess(pub BuilderBuild, pub Vec<PathBuf>, pub Vec<PathBuf>);
47
48/// Results of a build.
49#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
50pub type BuilderResult = Result<BuilderSuccess, BuilderError>;
51
52/// The context in which a builder object lives. Callbacks for various
53/// purposes.
54#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
55pub trait BuilderContext {
56 /// Perform any initialization specific to the context in which this
57 /// builder lives.
58 fn setup() {}
59
60 /// Create a dependency recorder, if any.
61 fn get_dependency_recorder() -> Option<Box<dyn RebuildDependencyRecorder>>;
62}
63
64/// An object to allow building of bindings from a `build.rs` file.
65///
66/// It would be unusual to use this directly - see the `autocxx_build` or
67/// `autocxx_gen` crates.
68#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
69pub struct Builder<'a, BuilderContext> {
70 rs_file: PathBuf,
71 autocxx_incs: Vec<OsString>,
72 extra_clang_args: Vec<String>,
73 dependency_recorder: Option<Box<dyn RebuildDependencyRecorder>>,
74 custom_gendir: Option<PathBuf>,
75 auto_allowlist: bool,
76 cpp_codegen_options: CppCodegenOptions<'a>,
77 // This member is to ensure that this type is parameterized
78 // by a BuilderContext. The goal is to balance three needs:
79 // (1) have most of the functionality over in autocxx_engine,
80 // (2) expose this type to users of autocxx_build and to
81 // make it easy for callers simply to call Builder::new,
82 // (3) ensure that such a Builder does a few tasks specific to its use
83 // in a cargo environment.
84 ctx: PhantomData<BuilderContext>,
85}
86
87impl<CTX: BuilderContext> Builder<'_, CTX> {
88 /// Create a new Builder object. You'll need to pass in the Rust file
89 /// which contains the bindings (typically an `include_cpp!` macro
90 /// though `autocxx` can also handle manually-crafted `cxx::bridge`
91 /// bindings), and a list of include directories which should be searched
92 /// by autocxx as it tries to hunt for the include files specified
93 /// within the `include_cpp!` macro.
94 ///
95 /// Usually after this you'd call [`build`].
96 pub fn new(
97 rs_file: impl AsRef<Path>,
98 autocxx_incs: impl IntoIterator<Item = impl AsRef<OsStr>>,
99 ) -> Self {
100 CTX::setup();
101 Self {
102 rs_file: rs_file.as_ref().to_path_buf(),
103 autocxx_incs: autocxx_incs
104 .into_iter()
105 .map(|s| s.as_ref().to_os_string())
106 .collect(),
107 extra_clang_args: Vec::new(),
108 dependency_recorder: CTX::get_dependency_recorder(),
109 custom_gendir: None,
110 auto_allowlist: false,
111 cpp_codegen_options: CppCodegenOptions::default(),
112 ctx: PhantomData,
113 }
114 }
115
116 /// Specify extra arguments for clang.
117 pub fn extra_clang_args(mut self, extra_clang_args: &[&str]) -> Self {
118 self.extra_clang_args = extra_clang_args.iter().map(|s| s.to_string()).collect();
119 self
120 }
121
122 /// Where to generate the code.
123 pub fn custom_gendir(mut self, custom_gendir: PathBuf) -> Self {
124 self.custom_gendir = Some(custom_gendir);
125 self
126 }
127
128 /// Update C++ code generation options. See [`CppCodegenOptions`] for details.
129 pub fn cpp_codegen_options<F>(mut self, modifier: F) -> Self
130 where
131 F: FnOnce(&mut CppCodegenOptions),
132 {
133 modifier(&mut self.cpp_codegen_options);
134 self
135 }
136
137 /// Automatically discover uses of the C++ `ffi` mod and generate the allowlist
138 /// from that.
139 /// This is a highly experimental option, not currently recommended.
140 /// It doesn't work in the following cases:
141 /// * Static function calls on types within the FFI mod.
142 /// * Anything inside a macro invocation.
143 /// * You're using a different name for your `ffi` mod
144 /// * You're using multiple FFI mods
145 /// * You've got usages scattered across files beyond that with the
146 /// `include_cpp` invocation
147 /// * You're using `use` statements to rename mods or items. If this
148 /// proves to be a promising or helpful direction, autocxx would be happy
149 /// to accept pull requests to remove some of these limitations.
150 pub fn auto_allowlist(mut self, do_it: bool) -> Self {
151 self.auto_allowlist = do_it;
152 self
153 }
154
155 /// Whether to suppress inclusion of system headers (`memory`, `string` etc.)
156 /// from generated C++ bindings code. This should not normally be used,
157 /// but can occasionally be useful if you're reducing a test case and you
158 /// have a preprocessed header file which already contains absolutely everything
159 /// that the bindings could ever need.
160 pub fn suppress_system_headers(mut self, do_it: bool) -> Self {
161 self.cpp_codegen_options.suppress_system_headers = do_it;
162 self
163 }
164
165 /// An annotation optionally to include on each C++ function.
166 /// For example to export the symbol from a library.
167 pub fn cxx_impl_annotations(mut self, cxx_impl_annotations: Option<String>) -> Self {
168 self.cpp_codegen_options.cxx_impl_annotations = cxx_impl_annotations;
169 self
170 }
171
172 /// Build autocxx C++ files and return a [`cc::Build`] you can use to build
173 /// more from a build.rs file.
174 ///
175 /// The error type returned by this function supports [`miette::Diagnostic`],
176 /// so if you use the `miette` crate and its `fancy` feature, then simply
177 /// return a `miette::Result` from your main function, you should get nicely
178 /// printed diagnostics.
179 pub fn build(self) -> Result<BuilderBuild, BuilderError> {
180 self.build_listing_files().map(|r| r.0)
181 }
182
183 /// For use in tests only, this does the build and returns additional information
184 /// about the files generated which can subsequently be examined for correctness.
185 /// In production, please use simply [`build`].
186 pub fn build_listing_files(self) -> Result<BuilderSuccess, BuilderError> {
187 let clang_args = &self
188 .extra_clang_args
189 .iter()
190 .map(|s| &s[..])
191 .collect::<Vec<_>>();
192 rust_version_check();
193 let gen_location_strategy = match self.custom_gendir {
194 None => FileLocationStrategy::new(),
195 Some(custom_dir) => FileLocationStrategy::Custom(custom_dir),
196 };
197 let incdir = gen_location_strategy.get_include_dir();
198 ensure_created(&incdir)?;
199 let cxxdir = gen_location_strategy.get_cxx_dir();
200 ensure_created(&cxxdir)?;
201 let rsdir = gen_location_strategy.get_rs_dir();
202 ensure_created(&rsdir)?;
203 // We are incredibly unsophisticated in our directory arrangement here
204 // compared to cxx. I have no doubt that we will need to replicate just
205 // about everything cxx does, in due course...
206 // Write cxx.h to that location, as it may be needed by
207 // some of our generated code.
208 write_to_file(
209 &incdir,
210 "cxx.h",
211 &Self::get_cxx_header_bytes(self.cpp_codegen_options.suppress_system_headers),
212 )?;
213
214 let autocxx_inc = build_autocxx_inc(self.autocxx_incs, &incdir);
215 gen_location_strategy.set_cargo_env_vars_for_build();
216
217 let mut parsed_file = crate::parse_file(self.rs_file, self.auto_allowlist)
218 .map_err(BuilderError::ParseError)?;
219 parsed_file
220 .resolve_all(
221 autocxx_inc,
222 clang_args,
223 self.dependency_recorder,
224 &self.cpp_codegen_options,
225 )
226 .map_err(BuilderError::ParseError)?;
227 let mut counter = 0;
228 let mut builder = cc::Build::new();
229 builder.cpp(true);
230 if std::env::var_os("AUTOCXX_ASAN").is_some() {
231 builder.flag_if_supported("-fsanitize=address");
232 }
233 let mut generated_rs = Vec::new();
234 let mut generated_cpp = Vec::new();
235 builder.includes(parsed_file.include_dirs());
236 for include_cpp in parsed_file.get_cpp_buildables() {
237 let generated_code = include_cpp
238 .generate_h_and_cxx(&self.cpp_codegen_options)
239 .map_err(BuilderError::InvalidCxx)?;
240 for filepair in generated_code.0 {
241 let fname = format!("gen{}.cxx", counter);
242 counter += 1;
243 if let Some(implementation) = &filepair.implementation {
244 let gen_cxx_path = write_to_file(&cxxdir, &fname, implementation)?;
245 builder.file(&gen_cxx_path);
246 generated_cpp.push(gen_cxx_path);
247 }
248 write_to_file(&incdir, &filepair.header_name, &filepair.header)?;
249 generated_cpp.push(incdir.join(filepair.header_name));
250 }
251 }
252
253 for rs_output in parsed_file.get_rs_outputs() {
254 let rs = generate_rs_single(rs_output);
255 generated_rs.push(write_to_file(&rsdir, &rs.filename, rs.code.as_bytes())?);
256 }
257 if counter == 0 {
258 Err(BuilderError::NoIncludeCxxMacrosFound)
259 } else {
260 Ok(BuilderSuccess(builder, generated_rs, generated_cpp))
261 }
262 }
263
264 fn get_cxx_header_bytes(suppress_system_headers: bool) -> Vec<u8> {
265 strip_system_headers(crate::HEADER.as_bytes().to_vec(), suppress_system_headers)
266 }
267}
268
269fn ensure_created(dir: &Path) -> Result<(), BuilderError> {
270 std::fs::create_dir_all(dir)
271 .map_err(|e| BuilderError::UnableToCreateDirectory(e, dir.to_path_buf()))
272}
273
274fn build_autocxx_inc<I, T>(paths: I, extra_path: &Path) -> Vec<PathBuf>
275where
276 I: IntoIterator<Item = T>,
277 T: AsRef<OsStr>,
278{
279 paths
280 .into_iter()
281 .map(|p| PathBuf::from(p.as_ref()))
282 .chain(std::iter::once(extra_path.to_path_buf()))
283 .collect()
284}
285
286fn write_to_file(dir: &Path, filename: &str, content: &[u8]) -> Result<PathBuf, BuilderError> {
287 let path = dir.join(filename);
288 try_write_to_file(&path, content).map_err(|e| BuilderError::FileWriteFail(e, path.clone()))?;
289 Ok(path)
290}
291
292fn try_write_to_file(path: &Path, content: &[u8]) -> std::io::Result<()> {
293 let mut f = File::create(path)?;
294 f.write_all(content)
295}
296
297fn rust_version_check() {
298 if !version_check::is_min_version("1.54.0").unwrap_or(false) {
299 panic!("Rust 1.54 or later is required.")
300 }
301}