autocxx_engine/
builder.rs

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
9use autocxx_parser::file_locations::FileLocationStrategy;
10use miette::Diagnostic;
11use thiserror::Error;
12
13use crate::{generate_rs_single, CodegenOptions};
14use crate::{get_cxx_header_bytes, 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 create this directly - see the `autocxx_build` or
67/// `autocxx_gen` crates.
68///
69/// Once you've got one of these objects, you may set some configuration
70/// options but then you're likely to want to call the [`build`] method.
71///
72/// # Setting C++ version
73///
74/// Ensure you use [`extra_clang_args`] as well as giving an appropriate
75/// option to the [`cc::Build`] which you receive from the [`build`] function.
76#[cfg_attr(feature = "nightly", doc(cfg(feature = "build")))]
77pub struct Builder<'a, BuilderContext> {
78    rs_file: PathBuf,
79    autocxx_incs: Vec<OsString>,
80    extra_clang_args: Vec<String>,
81    dependency_recorder: Option<Box<dyn RebuildDependencyRecorder>>,
82    custom_gendir: Option<PathBuf>,
83    auto_allowlist: bool,
84    codegen_options: CodegenOptions<'a>,
85    // This member is to ensure that this type is parameterized
86    // by a BuilderContext. The goal is to balance three needs:
87    // (1) have most of the functionality over in autocxx_engine,
88    // (2) expose this type to users of autocxx_build and to
89    //     make it easy for callers simply to call Builder::new,
90    // (3) ensure that such a Builder does a few tasks specific to its use
91    // in a cargo environment.
92    ctx: PhantomData<BuilderContext>,
93}
94
95impl<CTX: BuilderContext> Builder<'_, CTX> {
96    /// Create a new Builder object. You'll need to pass in the Rust file
97    /// which contains the bindings (typically an `include_cpp!` macro
98    /// though `autocxx` can also handle manually-crafted `cxx::bridge`
99    /// bindings), and a list of include directories which should be searched
100    /// by autocxx as it tries to hunt for the include files specified
101    /// within the `include_cpp!` macro.
102    ///
103    /// Usually after this you'd call [`build`].
104    pub fn new(
105        rs_file: impl AsRef<Path>,
106        autocxx_incs: impl IntoIterator<Item = impl AsRef<OsStr>>,
107    ) -> Self {
108        CTX::setup();
109        Self {
110            rs_file: rs_file.as_ref().to_path_buf(),
111            autocxx_incs: autocxx_incs
112                .into_iter()
113                .map(|s| s.as_ref().to_os_string())
114                .collect(),
115            extra_clang_args: Vec::new(),
116            dependency_recorder: CTX::get_dependency_recorder(),
117            custom_gendir: None,
118            auto_allowlist: false,
119            codegen_options: CodegenOptions::default(),
120            ctx: PhantomData,
121        }
122    }
123
124    /// Specify extra arguments for clang. These are used when parsing
125    /// C++ headers. For example, you might want to provide
126    /// `-std=c++17` to specify C++17.
127    pub fn extra_clang_args(mut self, extra_clang_args: &[&str]) -> Self {
128        self.extra_clang_args = extra_clang_args.iter().map(|s| s.to_string()).collect();
129        self
130    }
131
132    /// Where to generate the code.
133    pub fn custom_gendir(mut self, custom_gendir: PathBuf) -> Self {
134        self.custom_gendir = Some(custom_gendir);
135        self
136    }
137
138    /// Update C++ code generation options. See [`CppCodegenOptions`] for details.
139    pub fn cpp_codegen_options<F>(mut self, modifier: F) -> Self
140    where
141        F: FnOnce(&mut CppCodegenOptions),
142    {
143        modifier(&mut self.codegen_options.cpp_codegen_options);
144        self
145    }
146
147    /// Automatically discover uses of the C++ `ffi` mod and generate the allowlist
148    /// from that.
149    /// This is a highly experimental option, not currently recommended.
150    /// It doesn't work in the following cases:
151    /// * Static function calls on types within the FFI mod.
152    /// * Anything inside a macro invocation.
153    /// * You're using a different name for your `ffi` mod
154    /// * You're using multiple FFI mods
155    /// * You've got usages scattered across files beyond that with the
156    ///   `include_cpp` invocation
157    /// * You're using `use` statements to rename mods or items. If this
158    ///
159    /// proves to be a promising or helpful direction, autocxx would be happy
160    /// to accept pull requests to remove some of these limitations.
161    pub fn auto_allowlist(mut self, do_it: bool) -> Self {
162        self.auto_allowlist = do_it;
163        self
164    }
165
166    #[doc(hidden)]
167    /// Whether to force autocxx always to generate extra Rust and C++
168    /// side shims. This is only used by the integration test suite to
169    /// exercise more code paths - don't use it!
170    pub fn force_wrapper_generation(mut self, do_it: bool) -> Self {
171        self.codegen_options.force_wrapper_gen = do_it;
172        self
173    }
174
175    /// Whether to suppress inclusion of system headers (`memory`, `string` etc.)
176    /// from generated C++ bindings code. This should not normally be used,
177    /// but can occasionally be useful if you're reducing a test case and you
178    /// have a preprocessed header file which already contains absolutely everything
179    /// that the bindings could ever need.
180    pub fn suppress_system_headers(mut self, do_it: bool) -> Self {
181        self.codegen_options
182            .cpp_codegen_options
183            .suppress_system_headers = do_it;
184        self
185    }
186
187    /// An annotation optionally to include on each C++ function.
188    /// For example to export the symbol from a library.
189    pub fn cxx_impl_annotations(mut self, cxx_impl_annotations: Option<String>) -> Self {
190        self.codegen_options
191            .cpp_codegen_options
192            .cxx_impl_annotations = cxx_impl_annotations;
193        self
194    }
195
196    /// Build autocxx C++ files and return a [`cc::Build`] you can use to build
197    /// more from a build.rs file.
198    ///
199    /// The error type returned by this function supports [`miette::Diagnostic`],
200    /// so if you use the `miette` crate and its `fancy` feature, then simply
201    /// return a `miette::Result` from your main function, you should get nicely
202    /// printed diagnostics.
203    ///
204    /// As this is a [`cc::Build`] there are lots of options you can apply to
205    /// the resulting options, but please bear in mind that these only apply
206    /// to the build process for the generated code - such options will not
207    /// influence autocxx's process for parsing header files.
208    ///
209    /// For example, if you wish to set the C++ version to C++17, you might
210    /// be tempted to use [`cc::Build::flag_if_supported`] to add the
211    /// `-std=c++17` flag. However, this won't affect the header parsing which
212    /// autocxx does internally (by means of bindgen) so you _additionally_
213    /// should call [`extra_clang_args`] with that same option.
214    pub fn build(self) -> Result<BuilderBuild, BuilderError> {
215        self.build_listing_files().map(|r| r.0)
216    }
217
218    /// For use in tests only, this does the build and returns additional information
219    /// about the files generated which can subsequently be examined for correctness.
220    /// In production, please use simply [`build`].
221    pub fn build_listing_files(self) -> Result<BuilderSuccess, BuilderError> {
222        let clang_args = &self
223            .extra_clang_args
224            .iter()
225            .map(|s| &s[..])
226            .collect::<Vec<_>>();
227        rust_version_check();
228        let gen_location_strategy = match self.custom_gendir {
229            None => FileLocationStrategy::new(),
230            Some(custom_dir) => FileLocationStrategy::Custom(custom_dir),
231        };
232        let incdir = gen_location_strategy.get_include_dir();
233        ensure_created(&incdir)?;
234        let cxxdir = gen_location_strategy.get_cxx_dir();
235        ensure_created(&cxxdir)?;
236        let rsdir = gen_location_strategy.get_rs_dir();
237        ensure_created(&rsdir)?;
238        // We are incredibly unsophisticated in our directory arrangement here
239        // compared to cxx. I have no doubt that we will need to replicate just
240        // about everything cxx does, in due course...
241        // Write cxx.h to that location, as it may be needed by
242        // some of our generated code.
243        write_to_file(
244            &incdir,
245            "cxx.h",
246            &get_cxx_header_bytes(
247                self.codegen_options
248                    .cpp_codegen_options
249                    .suppress_system_headers,
250            ),
251        )?;
252
253        let autocxx_inc = build_autocxx_inc(self.autocxx_incs, &incdir);
254        gen_location_strategy.set_cargo_env_vars_for_build();
255
256        let mut parsed_file = crate::parse_file(self.rs_file, self.auto_allowlist)
257            .map_err(BuilderError::ParseError)?;
258        parsed_file
259            .resolve_all(
260                autocxx_inc,
261                clang_args,
262                self.dependency_recorder,
263                &self.codegen_options,
264            )
265            .map_err(BuilderError::ParseError)?;
266        let mut counter = 0;
267        let mut builder = cc::Build::new();
268        builder.cpp(true);
269        if std::env::var_os("AUTOCXX_ASAN").is_some() {
270            builder.flag_if_supported("-fsanitize=address");
271        }
272        let mut generated_rs = Vec::new();
273        let mut generated_cpp = Vec::new();
274        builder.includes(parsed_file.include_dirs());
275        for include_cpp in parsed_file.get_cpp_buildables() {
276            let generated_code = include_cpp
277                .generate_h_and_cxx(&self.codegen_options.cpp_codegen_options)
278                .map_err(BuilderError::InvalidCxx)?;
279            for filepair in generated_code.0 {
280                let fname = format!("gen{counter}.cxx");
281                counter += 1;
282                if let Some(implementation) = &filepair.implementation {
283                    let gen_cxx_path = write_to_file(&cxxdir, &fname, implementation)?;
284                    builder.file(&gen_cxx_path);
285                    generated_cpp.push(gen_cxx_path);
286                }
287                write_to_file(&incdir, &filepair.header_name, &filepair.header)?;
288                generated_cpp.push(incdir.join(filepair.header_name));
289            }
290        }
291
292        for rs_output in parsed_file.get_rs_outputs() {
293            let rs = generate_rs_single(rs_output);
294            generated_rs.push(write_to_file(&rsdir, &rs.filename, rs.code.as_bytes())?);
295        }
296        if counter == 0 {
297            Err(BuilderError::NoIncludeCxxMacrosFound)
298        } else {
299            Ok(BuilderSuccess(builder, generated_rs, generated_cpp))
300        }
301    }
302}
303
304fn ensure_created(dir: &Path) -> Result<(), BuilderError> {
305    std::fs::create_dir_all(dir)
306        .map_err(|e| BuilderError::UnableToCreateDirectory(e, dir.to_path_buf()))
307}
308
309fn build_autocxx_inc<I, T>(paths: I, extra_path: &Path) -> Vec<PathBuf>
310where
311    I: IntoIterator<Item = T>,
312    T: AsRef<OsStr>,
313{
314    paths
315        .into_iter()
316        .map(|p| PathBuf::from(p.as_ref()))
317        .chain(std::iter::once(extra_path.to_path_buf()))
318        .collect()
319}
320
321fn write_to_file(dir: &Path, filename: &str, content: &[u8]) -> Result<PathBuf, BuilderError> {
322    let path = dir.join(filename);
323    if let Ok(existing_contents) = std::fs::read(&path) {
324        // Avoid altering timestamps on disk if the file already exists,
325        // to stop downstream build steps recurring.
326        if existing_contents == content {
327            return Ok(path);
328        }
329    }
330    try_write_to_file(&path, content).map_err(|e| BuilderError::FileWriteFail(e, path.clone()))?;
331    Ok(path)
332}
333
334fn try_write_to_file(path: &Path, content: &[u8]) -> std::io::Result<()> {
335    let mut f = File::create(path)?;
336    f.write_all(content)
337}
338
339fn rust_version_check() {
340    if !version_check::is_min_version("1.54.0").unwrap_or(false) {
341        panic!("Rust 1.54 or later is required.")
342    }
343}