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