cxx_build/
lib.rs

1//! The CXX code generator for constructing and compiling C++ code.
2//!
3//! This is intended to be used from Cargo build scripts to execute CXX's
4//! C++ code generator, set up any additional compiler flags depending on
5//! the use case, and make the C++ compiler invocation.
6//!
7//! <br>
8//!
9//! # Example
10//!
11//! Example of a canonical Cargo build script that builds a CXX bridge:
12//!
13//! ```no_run
14//! // build.rs
15//!
16//! fn main() {
17//!     cxx_build::bridge("src/main.rs")
18//!         .file("src/demo.cc")
19//!         .std("c++11")
20//!         .compile("cxxbridge-demo");
21//!
22//!     println!("cargo:rerun-if-changed=src/main.rs");
23//!     println!("cargo:rerun-if-changed=src/demo.cc");
24//!     println!("cargo:rerun-if-changed=include/demo.h");
25//! }
26//! ```
27//!
28//! A runnable working setup with this build script is shown in the *demo*
29//! directory of [https://github.com/dtolnay/cxx].
30//!
31//! [https://github.com/dtolnay/cxx]: https://github.com/dtolnay/cxx
32//!
33//! <br>
34//!
35//! # Alternatives
36//!
37//! For use in non-Cargo builds like Bazel or Buck, CXX provides an
38//! alternate way of invoking the C++ code generator as a standalone command
39//! line tool. The tool is packaged as the `cxxbridge-cmd` crate.
40//!
41//! ```bash
42//! $ cargo install cxxbridge-cmd  # or build it from the repo
43//!
44//! $ cxxbridge src/main.rs --header > path/to/mybridge.h
45//! $ cxxbridge src/main.rs > path/to/mybridge.cc
46//! ```
47
48#![doc(html_root_url = "https://docs.rs/cxx-build/1.0.135")]
49#![cfg_attr(not(check_cfg), allow(unexpected_cfgs))]
50#![allow(
51    clippy::cast_sign_loss,
52    clippy::default_trait_access,
53    clippy::derive_partial_eq_without_eq,
54    clippy::doc_markdown,
55    clippy::enum_glob_use,
56    clippy::explicit_auto_deref,
57    clippy::if_same_then_else,
58    clippy::inherent_to_string,
59    clippy::into_iter_without_iter,
60    clippy::items_after_statements,
61    clippy::match_bool,
62    clippy::match_on_vec_items,
63    clippy::match_same_arms,
64    clippy::module_name_repetitions,
65    clippy::needless_doctest_main,
66    clippy::needless_lifetimes,
67    clippy::needless_pass_by_value,
68    clippy::new_without_default,
69    clippy::nonminimal_bool,
70    clippy::or_fun_call,
71    clippy::redundant_else,
72    clippy::ref_option,
73    clippy::shadow_unrelated,
74    clippy::significant_drop_in_scrutinee,
75    clippy::similar_names,
76    clippy::single_match_else,
77    clippy::struct_excessive_bools,
78    clippy::struct_field_names,
79    clippy::too_many_arguments,
80    clippy::too_many_lines,
81    clippy::toplevel_ref_arg,
82    clippy::unconditional_recursion, // clippy bug: https://github.com/rust-lang/rust-clippy/issues/12133
83    clippy::uninlined_format_args,
84    clippy::upper_case_acronyms,
85)]
86
87mod cargo;
88mod cfg;
89mod deps;
90mod error;
91mod gen;
92mod intern;
93mod out;
94mod paths;
95mod syntax;
96mod target;
97mod vec;
98
99use crate::cargo::CargoEnvCfgEvaluator;
100use crate::deps::{Crate, HeaderDir};
101use crate::error::{Error, Result};
102use crate::gen::error::report;
103use crate::gen::Opt;
104use crate::paths::PathExt;
105use crate::syntax::map::{Entry, UnorderedMap};
106use crate::target::TargetDir;
107use cc::Build;
108use std::collections::BTreeSet;
109use std::env;
110use std::ffi::{OsStr, OsString};
111use std::io::{self, Write};
112use std::iter;
113use std::path::{Path, PathBuf};
114use std::process;
115
116pub use crate::cfg::{Cfg, CFG};
117
118/// This returns a [`cc::Build`] on which you should continue to set up any
119/// additional source files or compiler flags, and lastly call its [`compile`]
120/// method to execute the C++ build.
121///
122/// [`compile`]: https://docs.rs/cc/1.0.49/cc/struct.Build.html#method.compile
123#[must_use]
124pub fn bridge(rust_source_file: impl AsRef<Path>) -> Build {
125    bridges(iter::once(rust_source_file))
126}
127
128/// `cxx_build::bridge` but for when more than one file contains a
129/// #\[cxx::bridge\] module.
130///
131/// ```no_run
132/// let source_files = vec!["src/main.rs", "src/path/to/other.rs"];
133/// cxx_build::bridges(source_files)
134///     .file("src/demo.cc")
135///     .std("c++11")
136///     .compile("cxxbridge-demo");
137/// ```
138#[must_use]
139pub fn bridges(rust_source_files: impl IntoIterator<Item = impl AsRef<Path>>) -> Build {
140    let ref mut rust_source_files = rust_source_files.into_iter();
141    build(rust_source_files).unwrap_or_else(|err| {
142        let _ = writeln!(io::stderr(), "\n\ncxxbridge error: {}\n\n", report(err));
143        process::exit(1);
144    })
145}
146
147struct Project {
148    include_prefix: PathBuf,
149    manifest_dir: PathBuf,
150    // The `links = "..."` value from Cargo.toml.
151    links_attribute: Option<OsString>,
152    // Output directory as received from Cargo.
153    out_dir: PathBuf,
154    // Directory into which to symlink all generated code.
155    //
156    // This is *not* used for an #include path, only as a debugging convenience.
157    // Normally available at target/cxxbridge/ if we are able to know where the
158    // target dir is, otherwise under a common scratch dir.
159    //
160    // The reason this isn't the #include dir is that we do not want builds to
161    // have access to headers from arbitrary other parts of the dependency
162    // graph. Using a global directory for all builds would be both a race
163    // condition depending on what order Cargo randomly executes the build
164    // scripts, as well as semantically undesirable for builds not to have to
165    // declare their real dependencies.
166    shared_dir: PathBuf,
167}
168
169impl Project {
170    fn init() -> Result<Self> {
171        let include_prefix = Path::new(CFG.include_prefix);
172        assert!(include_prefix.is_relative());
173        let include_prefix = include_prefix.components().collect();
174
175        let links_attribute = env::var_os("CARGO_MANIFEST_LINKS");
176
177        let manifest_dir = paths::manifest_dir()?;
178        let out_dir = paths::out_dir()?;
179
180        let shared_dir = match target::find_target_dir(&out_dir) {
181            TargetDir::Path(target_dir) => target_dir.join("cxxbridge"),
182            TargetDir::Unknown => scratch::path("cxxbridge"),
183        };
184
185        Ok(Project {
186            include_prefix,
187            manifest_dir,
188            links_attribute,
189            out_dir,
190            shared_dir,
191        })
192    }
193}
194
195// We lay out the OUT_DIR as follows. Everything is namespaced under a cxxbridge
196// subdirectory to avoid stomping on other things that the caller's build script
197// might be doing inside OUT_DIR.
198//
199//     $OUT_DIR/
200//        cxxbridge/
201//           crate/
202//              $CARGO_PKG_NAME -> $CARGO_MANIFEST_DIR
203//           include/
204//              rust/
205//                 cxx.h
206//              $CARGO_PKG_NAME/
207//                 .../
208//                    lib.rs.h
209//           sources/
210//              $CARGO_PKG_NAME/
211//                 .../
212//                    lib.rs.cc
213//
214// The crate/ and include/ directories are placed on the #include path for the
215// current build as well as for downstream builds that have a direct dependency
216// on the current crate.
217fn build(rust_source_files: &mut dyn Iterator<Item = impl AsRef<Path>>) -> Result<Build> {
218    let ref prj = Project::init()?;
219    validate_cfg(prj)?;
220    let this_crate = make_this_crate(prj)?;
221
222    let mut build = Build::new();
223    build.cpp(true);
224    build.cpp_link_stdlib(None); // linked via link-cplusplus crate
225
226    for path in rust_source_files {
227        generate_bridge(prj, &mut build, path.as_ref())?;
228    }
229
230    this_crate.print_to_cargo();
231    eprintln!("\nCXX include path:");
232    for header_dir in this_crate.header_dirs {
233        build.include(&header_dir.path);
234        if header_dir.exported {
235            eprintln!("  {}", header_dir.path.display());
236        } else {
237            eprintln!("  {} (private)", header_dir.path.display());
238        }
239    }
240
241    Ok(build)
242}
243
244fn validate_cfg(prj: &Project) -> Result<()> {
245    for exported_dir in &CFG.exported_header_dirs {
246        if !exported_dir.is_absolute() {
247            return Err(Error::ExportedDirNotAbsolute(exported_dir));
248        }
249    }
250
251    for prefix in &CFG.exported_header_prefixes {
252        if prefix.is_empty() {
253            return Err(Error::ExportedEmptyPrefix);
254        }
255    }
256
257    if prj.links_attribute.is_none() {
258        if !CFG.exported_header_dirs.is_empty() {
259            return Err(Error::ExportedDirsWithoutLinks);
260        }
261        if !CFG.exported_header_prefixes.is_empty() {
262            return Err(Error::ExportedPrefixesWithoutLinks);
263        }
264        if !CFG.exported_header_links.is_empty() {
265            return Err(Error::ExportedLinksWithoutLinks);
266        }
267    }
268
269    Ok(())
270}
271
272fn make_this_crate(prj: &Project) -> Result<Crate> {
273    let crate_dir = make_crate_dir(prj);
274    let include_dir = make_include_dir(prj)?;
275
276    let mut this_crate = Crate {
277        include_prefix: Some(prj.include_prefix.clone()),
278        links: prj.links_attribute.clone(),
279        header_dirs: Vec::new(),
280    };
281
282    // The generated code directory (include_dir) is placed in front of
283    // crate_dir on the include line so that `#include "path/to/file.rs"` from
284    // C++ "magically" works and refers to the API generated from that Rust
285    // source file.
286    this_crate.header_dirs.push(HeaderDir {
287        exported: true,
288        path: include_dir,
289    });
290
291    this_crate.header_dirs.push(HeaderDir {
292        exported: true,
293        path: crate_dir,
294    });
295
296    for exported_dir in &CFG.exported_header_dirs {
297        this_crate.header_dirs.push(HeaderDir {
298            exported: true,
299            path: PathBuf::from(exported_dir),
300        });
301    }
302
303    let mut header_dirs_index = UnorderedMap::new();
304    let mut used_header_links = BTreeSet::new();
305    let mut used_header_prefixes = BTreeSet::new();
306    for krate in deps::direct_dependencies() {
307        let mut is_link_exported = || match &krate.links {
308            None => false,
309            Some(links_attribute) => CFG.exported_header_links.iter().any(|&exported| {
310                let matches = links_attribute == exported;
311                if matches {
312                    used_header_links.insert(exported);
313                }
314                matches
315            }),
316        };
317
318        let mut is_prefix_exported = || match &krate.include_prefix {
319            None => false,
320            Some(include_prefix) => CFG.exported_header_prefixes.iter().any(|&exported| {
321                let matches = include_prefix.starts_with(exported);
322                if matches {
323                    used_header_prefixes.insert(exported);
324                }
325                matches
326            }),
327        };
328
329        let exported = is_link_exported() || is_prefix_exported();
330
331        for dir in krate.header_dirs {
332            // Deduplicate dirs reachable via multiple transitive dependencies.
333            match header_dirs_index.entry(dir.path.clone()) {
334                Entry::Vacant(entry) => {
335                    entry.insert(this_crate.header_dirs.len());
336                    this_crate.header_dirs.push(HeaderDir {
337                        exported,
338                        path: dir.path,
339                    });
340                }
341                Entry::Occupied(entry) => {
342                    let index = *entry.get();
343                    this_crate.header_dirs[index].exported |= exported;
344                }
345            }
346        }
347    }
348
349    if let Some(unused) = CFG
350        .exported_header_links
351        .iter()
352        .find(|&exported| !used_header_links.contains(exported))
353    {
354        return Err(Error::UnusedExportedLinks(unused));
355    }
356
357    if let Some(unused) = CFG
358        .exported_header_prefixes
359        .iter()
360        .find(|&exported| !used_header_prefixes.contains(exported))
361    {
362        return Err(Error::UnusedExportedPrefix(unused));
363    }
364
365    Ok(this_crate)
366}
367
368fn make_crate_dir(prj: &Project) -> PathBuf {
369    if prj.include_prefix.as_os_str().is_empty() {
370        return prj.manifest_dir.clone();
371    }
372    let crate_dir = prj.out_dir.join("cxxbridge").join("crate");
373    let ref link = crate_dir.join(&prj.include_prefix);
374    let ref manifest_dir = prj.manifest_dir;
375    if out::relative_symlink_dir(manifest_dir, link).is_err() && cfg!(not(unix)) {
376        let cachedir_tag = "\
377        Signature: 8a477f597d28d172789f06886806bc55\n\
378        # This file is a cache directory tag created by cxx.\n\
379        # For information about cache directory tags see https://bford.info/cachedir/\n";
380        let _ = out::write(crate_dir.join("CACHEDIR.TAG"), cachedir_tag.as_bytes());
381        let max_depth = 6;
382        best_effort_copy_headers(manifest_dir, link, max_depth);
383    }
384    crate_dir
385}
386
387fn make_include_dir(prj: &Project) -> Result<PathBuf> {
388    let include_dir = prj.out_dir.join("cxxbridge").join("include");
389    let cxx_h = include_dir.join("rust").join("cxx.h");
390    let ref shared_cxx_h = prj.shared_dir.join("rust").join("cxx.h");
391    if let Some(ref original) = env::var_os("DEP_CXXBRIDGE1_HEADER") {
392        out::absolute_symlink_file(original, cxx_h)?;
393        out::absolute_symlink_file(original, shared_cxx_h)?;
394    } else {
395        out::write(shared_cxx_h, gen::include::HEADER.as_bytes())?;
396        out::relative_symlink_file(shared_cxx_h, cxx_h)?;
397    }
398    Ok(include_dir)
399}
400
401fn generate_bridge(prj: &Project, build: &mut Build, rust_source_file: &Path) -> Result<()> {
402    let opt = Opt {
403        allow_dot_includes: false,
404        cfg_evaluator: Box::new(CargoEnvCfgEvaluator),
405        doxygen: CFG.doxygen,
406        ..Opt::default()
407    };
408    let generated = gen::generate_from_path(rust_source_file, &opt);
409    let ref rel_path = paths::local_relative_path(rust_source_file);
410
411    let cxxbridge = prj.out_dir.join("cxxbridge");
412    let include_dir = cxxbridge.join("include").join(&prj.include_prefix);
413    let sources_dir = cxxbridge.join("sources").join(&prj.include_prefix);
414
415    let ref rel_path_h = rel_path.with_appended_extension(".h");
416    let ref header_path = include_dir.join(rel_path_h);
417    out::write(header_path, &generated.header)?;
418
419    let ref link_path = include_dir.join(rel_path);
420    let _ = out::relative_symlink_file(header_path, link_path);
421
422    let ref rel_path_cc = rel_path.with_appended_extension(".cc");
423    let ref implementation_path = sources_dir.join(rel_path_cc);
424    out::write(implementation_path, &generated.implementation)?;
425    build.file(implementation_path);
426
427    let shared_h = prj.shared_dir.join(&prj.include_prefix).join(rel_path_h);
428    let shared_cc = prj.shared_dir.join(&prj.include_prefix).join(rel_path_cc);
429    let _ = out::relative_symlink_file(header_path, shared_h);
430    let _ = out::relative_symlink_file(implementation_path, shared_cc);
431    Ok(())
432}
433
434fn best_effort_copy_headers(src: &Path, dst: &Path, max_depth: usize) {
435    // Not using crate::gen::fs because we aren't reporting the errors.
436    use std::fs;
437
438    let mut dst_created = false;
439    let Ok(mut entries) = fs::read_dir(src) else {
440        return;
441    };
442
443    while let Some(Ok(entry)) = entries.next() {
444        let file_name = entry.file_name();
445        if file_name.to_string_lossy().starts_with('.') {
446            continue;
447        }
448        match entry.file_type() {
449            Ok(file_type) if file_type.is_dir() && max_depth > 0 => {
450                let src = entry.path();
451                if src.join("Cargo.toml").exists() || src.join("CACHEDIR.TAG").exists() {
452                    continue;
453                }
454                let dst = dst.join(file_name);
455                best_effort_copy_headers(&src, &dst, max_depth - 1);
456            }
457            Ok(file_type) if file_type.is_file() => {
458                let src = entry.path();
459                match src.extension().and_then(OsStr::to_str) {
460                    Some("h" | "hh" | "hpp") => {}
461                    _ => continue,
462                }
463                if !dst_created && fs::create_dir_all(dst).is_err() {
464                    return;
465                }
466                dst_created = true;
467                let dst = dst.join(file_name);
468                let _ = fs::remove_file(&dst);
469                let _ = fs::copy(src, dst);
470            }
471            _ => {}
472        }
473    }
474}
475
476fn env_os(key: impl AsRef<OsStr>) -> Result<OsString> {
477    let key = key.as_ref();
478    env::var_os(key).ok_or_else(|| Error::NoEnv(key.to_owned()))
479}