cxx_qt_build/
lib.rs

1// SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3// SPDX-FileContributor: Be Wilson <be.wilson@kdab.com>
4// SPDX-FileContributor: Gerhard de Clercq <gerhard.declercq@kdab.com>
5//
6// SPDX-License-Identifier: MIT OR Apache-2.0
7
8#![deny(missing_docs)]
9
10//! This crate provides a builder which parses given Rust source code to search
11//! for CXX-Qt or CXX macros and generate any resulting C++ code. It also builds
12//! the C++ code into a binary with any cxx-qt-lib code and Qt linked.
13
14mod cfg_evaluator;
15
16mod diagnostics;
17use diagnostics::{Diagnostic, GeneratedError};
18
19pub mod dir;
20use dir::INCLUDE_VERB;
21
22mod dependencies;
23pub use dependencies::Interface;
24use dependencies::{Dependency, Manifest};
25
26mod opts;
27pub use opts::CxxQtBuildersOpts;
28pub use opts::QObjectHeaderOpts;
29
30mod qml_modules;
31use qml_modules::OwningQmlModule;
32pub use qml_modules::QmlModule;
33
34pub use qt_build_utils::MocArguments;
35use qt_build_utils::SemVer;
36use quote::ToTokens;
37use std::{
38    collections::HashSet,
39    env,
40    fs::File,
41    io::Write,
42    path::{Path, PathBuf},
43};
44
45use cxx_qt_gen::{
46    parse_qt_file, write_cpp, write_rust, CppFragment, CxxQtItem, GeneratedCppBlocks,
47    GeneratedRustBlocks, Parser,
48};
49
50// TODO: we need to eventually support having multiple modules defined in a single file. This
51// is currently an issue because we are using the Rust file name to derive the cpp file name
52// and are blindly re-writing files.
53//
54// As we use struct names for the QObject files, we should actually be able to support multiple
55// QObject macros and at most one "raw CXX" macro per file already. For now this remains a TODO
56// as to keep things simpler. We also want to able to warn users about duplicate names eventually.
57
58struct GeneratedCppFilePaths {
59    plain_cpp: PathBuf,
60    qobject: Option<PathBuf>,
61    qobject_header: Option<PathBuf>,
62}
63
64struct GeneratedCpp {
65    cxx_qt: Option<CppFragment>,
66    cxx: cxx_gen::GeneratedCode,
67    file_ident: String,
68}
69
70impl GeneratedCpp {
71    /// Generate QObject and cxx header/source C++ file contents
72    pub fn new(
73        rust_file_path: impl AsRef<Path>,
74        relative_path: impl AsRef<Path>,
75        include_prefix: &str,
76    ) -> Result<Self, Diagnostic> {
77        let to_diagnostic = |err| Diagnostic::new(rust_file_path.as_ref().to_owned(), err);
78
79        let rust_file_path = rust_file_path.as_ref();
80
81        let file = parse_qt_file(rust_file_path)
82            .map_err(GeneratedError::from)
83            .map_err(to_diagnostic)?;
84
85        let mut cxx_qt = None;
86        let mut tokens = proc_macro2::TokenStream::new();
87
88        // Add any attributes in the file into the tokenstream
89        for attr in &file.attrs {
90            tokens.extend(attr.into_token_stream());
91        }
92
93        // Match upstream where they use the file name and folders as the ident
94        //
95        // We need the relative path here as we want the folders
96        let file_ident = relative_path
97            .as_ref()
98            // Remove the .rs extension
99            .with_extension("")
100            .to_string_lossy()
101            .to_string();
102
103        // The include path we inject needs any prefix (eg the crate name) too
104        let include_ident = format!("{include_prefix}/{file_ident}");
105
106        // Loop through the items looking for any CXX or CXX-Qt blocks
107        let mut found_bridge = false;
108        for item in &file.items {
109            match item {
110                CxxQtItem::Cxx(m) => {
111                    // TODO: later we will allow for multiple CXX or CXX-Qt blocks in one file
112                    if found_bridge {
113                        panic!(
114                            "Unfortunately only files with either a single cxx or a single cxx_qt module are currently supported.
115                            The file {} has more than one of these.",
116                            rust_file_path.display());
117                    }
118                    found_bridge = true;
119
120                    tokens.extend(m.into_token_stream());
121                }
122                CxxQtItem::CxxQt(m) => {
123                    // TODO: later we will allow for multiple CXX or CXX-Qt blocks in one file
124                    if found_bridge {
125                        panic!(
126                            "Unfortunately only files with either a single cxx or a single cxx_qt module are currently supported.
127                            The file {} has more than one of these.",
128                            rust_file_path.display());
129                    }
130                    found_bridge = true;
131
132                    let parser = Parser::from(m.clone())
133                        .map_err(GeneratedError::from)
134                        .map_err(to_diagnostic)?;
135                    let generated_cpp = GeneratedCppBlocks::from(&parser)
136                        .map_err(GeneratedError::from)
137                        .map_err(to_diagnostic)?;
138                    let generated_rust = GeneratedRustBlocks::from(&parser)
139                        .map_err(GeneratedError::from)
140                        .map_err(to_diagnostic)?;
141
142                    // TODO: we'll have to extend the C++ data here rather than overwriting
143                    // assuming we share the same file
144                    cxx_qt = Some(write_cpp(&generated_cpp, &include_ident));
145                    let rust_tokens = write_rust(&generated_rust, Some(&include_ident));
146
147                    // We need to do this and can't rely on the macro, as we need to generate the
148                    // CXX bridge Rust code that is then fed into the cxx_gen generation.
149                    tokens.extend(rust_tokens);
150                }
151                CxxQtItem::Item(item) => {
152                    tokens.extend(item.into_token_stream());
153                }
154            }
155        }
156
157        let mut opt = cxx_gen::Opt::default();
158        opt.cfg_evaluator = Box::new(cfg_evaluator::CargoEnvCfgEvaluator);
159        let cxx = cxx_gen::generate_header_and_cc(tokens, &opt)
160            .map_err(GeneratedError::from)
161            .map_err(to_diagnostic)?;
162
163        Ok(GeneratedCpp {
164            cxx_qt,
165            cxx,
166            file_ident,
167        })
168    }
169
170    /// Write generated .cpp and .h files to specified directories. Returns the paths of all files written.
171    pub fn write_to_directories(
172        self,
173        cpp_directory: impl AsRef<Path>,
174        header_directory: impl AsRef<Path>,
175    ) -> GeneratedCppFilePaths {
176        let cpp_directory = cpp_directory.as_ref();
177        let header_directory = header_directory.as_ref();
178
179        let mut cpp_file_paths = GeneratedCppFilePaths {
180            plain_cpp: PathBuf::new(),
181            qobject: None,
182            qobject_header: None,
183        };
184        if let Some(cxx_qt_generated) = &self.cxx_qt {
185            let header_path = PathBuf::from(format!(
186                "{}/{}.cxxqt.h",
187                header_directory.display(),
188                self.file_ident
189            ));
190            if let Some(directory) = header_path.parent() {
191                std::fs::create_dir_all(directory)
192                    .expect("Could not create directory to write cxx-qt generated files");
193            }
194            let mut header =
195                File::create(&header_path).expect("Could not create cxx-qt header file");
196            let header_generated = match cxx_qt_generated {
197                CppFragment::Pair { header, source: _ } => header,
198                CppFragment::Header(header) => header,
199                CppFragment::Source(_) => panic!("Unexpected call for source fragment."),
200            };
201            header
202                .write_all(header_generated.as_bytes())
203                .expect("Could not write cxx-qt header file");
204            cpp_file_paths.qobject_header = Some(header_path);
205
206            let cpp_path = PathBuf::from(format!(
207                "{}/{}.cxxqt.cpp",
208                cpp_directory.display(),
209                self.file_ident
210            ));
211            if let Some(directory) = cpp_path.parent() {
212                std::fs::create_dir_all(directory)
213                    .expect("Could not create directory to write cxx-qt generated files");
214            }
215            let mut cpp = File::create(&cpp_path).expect("Could not create cxx-qt source file");
216            let source_generated = match cxx_qt_generated {
217                CppFragment::Pair { header: _, source } => source,
218                CppFragment::Header(_) => panic!("Unexpected call for header fragment."),
219                CppFragment::Source(source) => source,
220            };
221            cpp.write_all(source_generated.as_bytes())
222                .expect("Could not write cxx-qt source file");
223            cpp_file_paths.qobject = Some(cpp_path);
224        }
225
226        let header_path = PathBuf::from(format!(
227            "{}/{}.cxx.h",
228            header_directory.display(),
229            self.file_ident
230        ));
231        if let Some(directory) = header_path.parent() {
232            std::fs::create_dir_all(directory)
233                .expect("Could not create directory to write cxx-qt generated header files");
234        }
235        let mut header = File::create(header_path).expect("Could not create cxx header file");
236        header
237            .write_all(&self.cxx.header)
238            .expect("Could not write cxx header file");
239
240        let cpp_path = PathBuf::from(format!(
241            "{}/{}.cxx.cpp",
242            cpp_directory.display(),
243            self.file_ident
244        ));
245        if let Some(directory) = cpp_path.parent() {
246            std::fs::create_dir_all(directory)
247                .expect("Could not create directory to write cxx-qt generated source files");
248        }
249        let mut cpp = File::create(&cpp_path).expect("Could not create cxx source file");
250        cpp.write_all(&self.cxx.implementation)
251            .expect("Could not write cxx source file");
252        cpp_file_paths.plain_cpp = cpp_path;
253
254        cpp_file_paths
255    }
256}
257
258/// Generate C++ files from a given list of Rust files, returning the generated paths
259fn generate_cxxqt_cpp_files(
260    rs_source: &[impl AsRef<Path>],
261    header_dir: impl AsRef<Path>,
262    include_prefix: &str,
263) -> Vec<GeneratedCppFilePaths> {
264    let cxx_qt_dir = dir::gen();
265    std::fs::create_dir_all(&cxx_qt_dir).expect("Failed to create cxx-qt-gen directory!");
266    std::fs::write(cxx_qt_dir.join("include-prefix.txt"), include_prefix).expect("");
267
268    let header_dir = header_dir.as_ref().join(include_prefix);
269    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
270
271    let mut generated_file_paths: Vec<GeneratedCppFilePaths> = Vec::with_capacity(rs_source.len());
272    for rs_path in rs_source {
273        let path = manifest_dir.join(rs_path);
274        println!("cargo::rerun-if-changed={}", path.to_string_lossy());
275
276        let generated_code = match GeneratedCpp::new(&path, rs_path, include_prefix) {
277            Ok(v) => v,
278            Err(diagnostic) => {
279                diagnostic.report();
280                std::process::exit(1);
281            }
282        };
283        generated_file_paths.push(generated_code.write_to_directories(&cxx_qt_dir, &header_dir));
284    }
285
286    generated_file_paths
287}
288
289pub(crate) fn module_name_from_uri(module_uri: &str) -> String {
290    // Note: We need to make sure this matches the conversion done in CMake!
291    module_uri.replace('.', "_")
292}
293
294pub(crate) fn crate_name() -> String {
295    env::var("CARGO_PKG_NAME").unwrap()
296}
297
298pub(crate) fn link_name() -> Option<String> {
299    env::var("CARGO_MANIFEST_LINKS").ok()
300}
301
302fn qt_modules_import() -> Option<String> {
303    env::var("CXX_QT_QT_MODULES").ok()
304}
305
306fn static_lib_name() -> String {
307    format!("{}-cxxqt-generated", crate_name())
308}
309
310fn panic_duplicate_file_and_qml_module(
311    path: impl AsRef<Path>,
312    uri: &str,
313    version_major: usize,
314    version_minor: usize,
315) {
316    panic!("CXX-Qt bridge Rust file {} specified in QML module {uri} (version {version_major}.{version_minor}), but also specified via CxxQtBuilder::file. Bridge files must be specified via CxxQtBuilder::file or CxxQtBuilder::qml_module, but not both.", path.as_ref().display());
317}
318
319/// Run cxx-qt's C++ code generator on Rust modules marked with the `cxx_qt::bridge` macro, compile
320/// the code, and link to Qt. This is the complement of the `cxx_qt::bridge` macro, which the Rust
321/// compiler uses to generate the corresponding Rust code. No dependencies besides Qt, a C++17 compiler,
322/// and Rust toolchain are required.
323///
324/// For example, if your `cxx_qt::bridge` module is in a file called `src/lib.rs` within your crate,
325/// put this in your [build.rs](https://doc.rust-lang.org/cargo/reference/build-scripts.html):
326///
327/// ```no_run
328/// use cxx_qt_build::CxxQtBuilder;
329///
330/// CxxQtBuilder::new()
331///     .file("src/lib.rs")
332///     .build();
333/// ```
334///
335/// If you have multiple major versions of Qt installed (for example, 5 and 6), you can tell
336/// [CxxQtBuilder] which one to use by setting the `QT_VERSION_MAJOR` environment variable to when
337/// running `cargo build`. Otherwise [CxxQtBuilder] prefers the newer version by default.
338///
339/// To use [CxxQtBuilder] for a library to link with a C++ application, specify a directory to output
340/// cxx-qt's autogenerated headers by having the C++ build system set the `CXX_QT_EXPORT_DIR`
341/// environment variable before calling `cargo build`. Then, add the same directory path to the C++
342/// include paths. Also, set the `QMAKE` environment variable to the path of the `qmake` executable
343/// for the Qt installation found by the C++ build system. This ensures that the C++ build system and
344/// [CxxQtBuilder] link to the same installation of Qt.
345///
346/// Under the hood, [CxxQtBuilder] uses [cc::Build], which allows compiling aditional C++ files as well.
347/// Refer to [CxxQtBuilder::cc_builder] for details.
348///
349/// In addition to autogenerating and building QObject C++ subclasses, manually written QObject
350/// subclasses can be parsed by moc and built using [CxxQtBuilder::qobject_header].
351#[derive(Default)]
352pub struct CxxQtBuilder {
353    rust_sources: Vec<PathBuf>,
354    qobject_headers: Vec<QObjectHeaderOpts>,
355    qrc_files: Vec<PathBuf>,
356    qt_modules: HashSet<String>,
357    qml_modules: Vec<OwningQmlModule>,
358    cc_builder: cc::Build,
359    public_interface: Option<Interface>,
360    include_prefix: String,
361    initializers: Vec<String>,
362}
363
364impl CxxQtBuilder {
365    /// Create a new builder
366    pub fn new() -> Self {
367        let mut qt_modules = HashSet::new();
368
369        // Add any Qt modules from CMake
370        if let Some(modules) = qt_modules_import() {
371            qt_modules.extend(
372                modules
373                    // Each module is split by a comma
374                    .split(",")
375                    // Each module could be Qt::Core or Qt6::Core etc
376                    // we only want the last part
377                    .map(|module| {
378                        if let Some((_, end)) = module.rsplit_once("::") {
379                            end
380                        } else {
381                            module
382                        }
383                    })
384                    .map(str::to_owned),
385            );
386        } else {
387            // When building with Cargo we implicitly add Qt Core
388            // for CMake this must be set in CMake
389            qt_modules.insert("Core".to_owned());
390        }
391
392        Self {
393            rust_sources: vec![],
394            qobject_headers: vec![],
395            qrc_files: vec![],
396            qt_modules,
397            qml_modules: vec![],
398            cc_builder: cc::Build::new(),
399            initializers: vec![],
400            public_interface: None,
401            include_prefix: crate_name(),
402        }
403    }
404
405    /// Create a new builder that is set up to create a library crate that is meant to be
406    /// included by later dependencies.
407    ///
408    /// The headers generated for this crate will be specified
409    pub fn library(interface_definition: Interface) -> Self {
410        let mut this = Self::new();
411        this.public_interface = Some(interface_definition);
412
413        if link_name().is_none() {
414            panic!("Building a Cxx-Qt based library requires setting a `links` field in the Cargo.toml file.\nConsider adding:\n\tlinks = \"{}\"\nto your Cargo.toml\n", crate_name());
415        }
416
417        this
418    }
419
420    /// Specify rust file paths to parse through the cxx-qt marco
421    /// Relative paths are treated as relative to the path of your crate's Cargo.toml file
422    pub fn file(mut self, rust_source: impl AsRef<Path>) -> Self {
423        let rust_source = rust_source.as_ref().to_path_buf();
424        for qml_module in &self.qml_modules {
425            if qml_module.rust_files.contains(&rust_source) {
426                panic_duplicate_file_and_qml_module(
427                    &rust_source,
428                    &qml_module.uri,
429                    qml_module.version_major,
430                    qml_module.version_minor,
431                );
432            }
433        }
434        println!("cargo::rerun-if-changed={}", rust_source.display());
435        self.rust_sources.push(rust_source);
436        self
437    }
438
439    /// Include files listed in a .qrc file into the binary
440    /// with [Qt's resource system](https://doc.qt.io/qt-6/resources.html).
441    /// ```no_run
442    /// # use cxx_qt_build::CxxQtBuilder;
443    /// CxxQtBuilder::new()
444    ///     .file("src/cxxqt_module.rs")
445    ///     .qrc("src/my_resources.qrc")
446    ///     .build();
447    /// ```
448    ///
449    /// ⚠️  In CMake projects, the .qrc file is typically added to the `SOURCES` of the target.
450    /// Prefer this to adding the qrc file through cxx-qt-build.
451    /// When using CMake, the qrc file will **not** be built by cxx-qt-build!
452    pub fn qrc(mut self, qrc_file: impl AsRef<Path>) -> Self {
453        let qrc_file = qrc_file.as_ref();
454        self.qrc_files.push(qrc_file.to_path_buf());
455        println!("cargo::rerun-if-changed={}", qrc_file.display());
456        self
457    }
458
459    /// Link additional [Qt modules](https://doc.qt.io/qt-6/qtmodules.html).
460    /// Specify their names without the `Qt` prefix, for example `"Widgets"`.
461    /// The `Core` module and any modules from dependencies are linked automatically; there is no need to specify them.
462    ///
463    /// Note that any qt_module you specify here will be enabled for all downstream
464    /// dependencies as well if this crate is built as a library with [CxxQtBuilder::library].
465    /// It is therefore best practice to specify features on your crate that allow downstream users
466    /// to disable any qt modules that are optional.
467    pub fn qt_module(mut self, module: &str) -> Self {
468        // Ensure that CMake and Cargo build.rs are not out of sync
469        if qt_modules_import().is_some() && !self.qt_modules.contains(module) {
470            panic!("Qt module mismatch between cxx-qt-build and CMake!\n\
471                    Qt module '{module}' was not specified in CMake!\n\
472                    When building with CMake, all Qt modules must be specified with the QT_MODULES argument in cxx_qt_import_crate");
473        }
474
475        self.qt_modules.insert(module.to_owned());
476        self
477    }
478
479    /// Instead of generating files under the crate name, generate files under the given prefix.
480    pub fn include_prefix(mut self, prefix: &str) -> Self {
481        prefix.clone_into(&mut self.include_prefix);
482        self
483    }
484
485    /// Register a QML module at build time. The `rust_files` of the [QmlModule] struct
486    /// should contain `#[cxx_qt::bridge]` modules with QObject types annotated with `#[qml_element]`.
487    ///
488    /// The QmlModule struct's `qml_files` are registered with the [Qt Resource System](https://doc.qt.io/qt-6/resources.html) in
489    /// the [default QML import path](https://doc.qt.io/qt-6/qtqml-syntax-imports.html#qml-import-path) `qrc:/qt/qml/uri/of/module/`.
490    /// Additional resources such as images can be added to the Qt resources for the QML module by specifying
491    /// the `qrc_files` field.
492    ///
493    /// When using Qt 6, this will [run qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html)
494    /// to compile the specified `.qml` files ahead-of-time.
495    ///
496    /// ```no_run
497    /// use cxx_qt_build::{CxxQtBuilder, QmlModule};
498    ///
499    /// CxxQtBuilder::new()
500    ///     .qml_module(QmlModule {
501    ///         uri: "com.kdab.cxx_qt.demo",
502    ///         rust_files: &["src/cxxqt_object.rs"],
503    ///         qml_files: &["qml/main.qml"],
504    ///         ..Default::default()
505    ///     })
506    ///     .build();
507    /// ```
508    pub fn qml_module<A: AsRef<Path>, B: AsRef<Path>>(
509        mut self,
510        qml_module: QmlModule<A, B>,
511    ) -> CxxQtBuilder {
512        let qml_module = OwningQmlModule::from(qml_module);
513        for path in &qml_module.rust_files {
514            if self.rust_sources.contains(path) {
515                panic_duplicate_file_and_qml_module(
516                    path,
517                    &qml_module.uri,
518                    qml_module.version_major,
519                    qml_module.version_minor,
520                );
521            }
522        }
523        self.qml_modules.push(qml_module);
524        self
525    }
526
527    /// Specify a C++ header containing a Q_OBJECT macro to run [moc](https://doc.qt.io/qt-6/moc.html) on.
528    /// This allows building QObject C++ subclasses besides the ones autogenerated by cxx-qt.
529    pub fn qobject_header(mut self, opts: impl Into<QObjectHeaderOpts>) -> Self {
530        let opts = opts.into();
531        println!("cargo::rerun-if-changed={}", opts.path.display());
532        self.qobject_headers.push(opts);
533        self
534    }
535
536    /// Use a closure to run additional customization on [CxxQtBuilder]'s internal [cc::Build]
537    /// before calling [CxxQtBuilder::build]. This allows to add extra include paths, compiler flags,
538    /// or anything else available via [cc::Build]'s API. For example, to add an include path for
539    /// manually written C++ headers located in a directory called `include` within your crate:
540    ///
541    /// ```no_run
542    /// # use cxx_qt_build::CxxQtBuilder;
543    ///
544    /// CxxQtBuilder::new()
545    ///     .file("src/lib.rs")
546    ///     .cc_builder(|cc| {
547    ///         cc.include("include");
548    ///     })
549    ///     .build();
550    /// ```
551    pub fn cc_builder(mut self, mut callback: impl FnMut(&mut cc::Build)) -> Self {
552        callback(&mut self.cc_builder);
553        self
554    }
555
556    fn define_cfg_variable(key: String, value: Option<&str>) {
557        if let Some(value) = value {
558            println!("cargo::rustc-cfg={key}=\"{value}\"");
559        } else {
560            println!("cargo::rustc-cfg={key}");
561        }
562        let variable_cargo = format!("CARGO_CFG_{}", key);
563        env::set_var(variable_cargo, value.unwrap_or("true"));
564    }
565
566    fn define_cfg_check_variable(key: String, values: Option<Vec<&str>>) {
567        if let Some(values) = values {
568            let values = values
569                .iter()
570                // Escape and add quotes
571                .map(|value| format!("\"{}\"", value.escape_default()))
572                .collect::<Vec<_>>()
573                .join(", ");
574            println!("cargo::rustc-check-cfg=cfg({key}, values({values}))");
575        } else {
576            println!("cargo::rustc-check-cfg=cfg({key})");
577        }
578    }
579
580    fn define_qt_version_cfg_variables(version: &SemVer) {
581        // Allow for Qt 5 or Qt 6 as valid values
582        CxxQtBuilder::define_cfg_check_variable(
583            "cxxqt_qt_version_major".to_owned(),
584            Some(vec!["5", "6"]),
585        );
586        // Find the Qt version and tell the Rust compiler
587        // this allows us to have conditional Rust code
588        CxxQtBuilder::define_cfg_variable(
589            "cxxqt_qt_version_major".to_string(),
590            Some(version.major.to_string().as_str()),
591        );
592
593        // Seed all values from Qt 5.0 through to Qt 7.99
594        for major in 5..=7 {
595            CxxQtBuilder::define_cfg_check_variable(
596                format!("cxxqt_qt_version_at_least_{major}"),
597                None,
598            );
599
600            for minor in 0..=99 {
601                CxxQtBuilder::define_cfg_check_variable(
602                    format!("cxxqt_qt_version_at_least_{major}_{minor}"),
603                    None,
604                );
605            }
606        }
607
608        for minor in 0..=version.minor {
609            let qt_version_at_least =
610                format!("cxxqt_qt_version_at_least_{}_{}", version.major, minor);
611            CxxQtBuilder::define_cfg_variable(qt_version_at_least.to_string(), None);
612        }
613
614        // We don't support Qt < 5
615        for major in 5..=version.major {
616            let at_least_qt_major_version = format!("cxxqt_qt_version_at_least_{}", major);
617            CxxQtBuilder::define_cfg_variable(at_least_qt_major_version, None);
618        }
619    }
620
621    fn write_common_headers() {
622        let header_root = dir::header_root();
623        // Write cxx headers
624        std::fs::create_dir_all(header_root.join("rust"))
625            .expect("Could not create cxx header directory");
626        let h_path = header_root.join("rust").join("cxx.h");
627        // Wrap the File in a block scope so the file is closed before the compiler is run.
628        // Otherwise MSVC fails to open cxx.h because the process for this build script already has it open.
629        {
630            std::fs::write(h_path, cxx_gen::HEADER).expect("Failed to write cxx.h");
631        }
632    }
633
634    // A dependency can specify which of its own include paths it wants to export.
635    // Set up each of these exported include paths as symlinks in our own include directory,
636    // or deep copy the files if the platform does not support symlinks.
637    fn include_dependency(&mut self, dependency: &Dependency) {
638        let header_root = dir::header_root();
639        let dependency_root = dependency.path.join("include");
640        for include_prefix in &dependency.manifest.exported_include_prefixes {
641            // setup include directory
642            let source = dependency_root.join(include_prefix);
643            let dest = header_root.join(include_prefix);
644
645            match dir::symlink_or_copy_directory(source, dest) {
646                Ok(true) => (),
647                Ok(false) => {
648                    panic!(
649                        "Conflicting include_prefixes for {include_prefix}!\nDependency {dep_name} conflicts with existing include path",
650                        dep_name = dependency.manifest.name,
651                    );
652                }
653                Err(e) => {
654                    panic!("Could not {INCLUDE_VERB} for include_prefix {include_prefix}: {e:?}");
655                }
656            }
657        }
658    }
659
660    fn setup_cc_builder(
661        builder: &mut cc::Build,
662        include_paths: &[impl AsRef<Path>],
663        defines: &[(String, Option<String>)],
664    ) {
665        // Note, ensure our settings stay in sync across cxx-qt and cxx-qt-lib
666        builder.cpp(true);
667        builder.std("c++17");
668        // MSVC
669        builder.flag_if_supported("/Zc:__cplusplus");
670        builder.flag_if_supported("/permissive-");
671        builder.flag_if_supported("/bigobj");
672        // MinGW requires big-obj otherwise debug builds fail
673        builder.flag_if_supported("-Wa,-mbig-obj");
674
675        // Enable any extra defines
676        for (variable, value) in defines {
677            builder.define(variable, value.as_deref());
678        }
679
680        for include_path in include_paths {
681            builder.include(include_path);
682        }
683    }
684
685    fn moc_qobject_headers(&mut self, qtbuild: &mut qt_build_utils::QtBuild) {
686        for QObjectHeaderOpts {
687            path,
688            moc_arguments,
689        } in &self.qobject_headers
690        {
691            let moc_products = qtbuild.moc(path, moc_arguments.clone());
692            // Include the moc folder
693            if let Some(dir) = moc_products.cpp.parent() {
694                self.cc_builder.include(dir);
695            }
696            self.cc_builder.file(moc_products.cpp);
697        }
698    }
699
700    fn generate_cpp_files_from_cxxqt_bridges(
701        &mut self,
702        header_dir: impl AsRef<Path>,
703        include_prefix: &str,
704    ) {
705        for files in generate_cxxqt_cpp_files(&self.rust_sources, &header_dir, include_prefix) {
706            self.cc_builder.file(files.plain_cpp);
707            if let (Some(qobject), Some(qobject_header)) = (files.qobject, files.qobject_header) {
708                self.cc_builder.file(&qobject);
709                self.qobject_headers.push(qobject_header.into());
710            }
711        }
712    }
713
714    fn build_object_file(builder: &cc::Build, file_path: impl AsRef<Path>, object_path: PathBuf) {
715        let mut obj_builder = builder.clone();
716        obj_builder.file(file_path);
717        let obj_files = obj_builder.compile_intermediates();
718
719        // We only expect a single file, so destructure the vec.
720        // If there's 0 or > 1 file, we panic in the `else` branch, because then the builder is
721        // probably not correctly configured.
722        if let [obj_file] = &obj_files[..] {
723            if dir::is_exporting() {
724                if let Some(directory) = object_path.parent() {
725                    std::fs::create_dir_all(directory).unwrap_or_else(|_| {
726                        panic!(
727                            "Could not create directory for object file: {}",
728                            object_path.to_string_lossy()
729                        )
730                    });
731                }
732                std::fs::copy(obj_file, &object_path).unwrap_or_else(|_| {
733                    panic!(
734                        "Failed to move object file to {}!",
735                        object_path.to_string_lossy()
736                    )
737                });
738            } else {
739                println!("cargo::rustc-link-arg={}", obj_file.to_string_lossy());
740            }
741        } else {
742            panic!(
743                "CXX-Qt internal error: Expected only one object file out of cc::Build! Got {}",
744                obj_files.len()
745            );
746        }
747    }
748
749    fn build_qml_modules(
750        &mut self,
751        init_builder: &cc::Build,
752        qtbuild: &mut qt_build_utils::QtBuild,
753        generated_header_dir: impl AsRef<Path>,
754        header_prefix: &str,
755    ) {
756        for qml_module in &self.qml_modules {
757            dir::clean(dir::module_target(&qml_module.uri))
758                .expect("Failed to clean qml module export directory!");
759
760            let mut qml_metatypes_json = Vec::new();
761
762            // Check that all rust files are within the same directory
763            //
764            // Note we need to do this as moc generates an inputFile which only
765            // includes the file name, qmltyperegistrar then uses this for the
766            // include path (and doesn't consider any prefix).
767            //
768            // This can also be observed when using qt_add_qml_module, if a class
769            // has a QML_ELEMENT the file must be in the same directory as the
770            // CMakeLists and cannot be a relative path to a sub directory.
771            let dirs = qml_module
772                .rust_files
773                .iter()
774                .map(|file| {
775                    if let Some(parent) = file.parent() {
776                        parent.to_string_lossy().to_string()
777                    } else {
778                        // Fallback to an empty string if there is no parent path
779                        String::new()
780                    }
781                })
782                .collect::<HashSet<String>>();
783            if dirs.len() > 1 {
784                panic!(
785                    "Only one directory is supported per QmlModule for rust_files.\n\
786                    This is due to Qt bug https://bugreports.qt.io/browse/QTBUG-93443\n\
787                    Found directories: {dirs:?}"
788                );
789            }
790
791            // TODO: for now we use the global CxxQtBuilder cc_builder
792            // this means that any includes/files etc on these are in this builder
793            // but we cannot have separate builds until we can configure includes,
794            // qt modules, files, cc_builder options etc in the QmlModule itself
795            let cc_builder = &mut self.cc_builder;
796            qtbuild.cargo_link_libraries(cc_builder);
797
798            let mut moc_include_paths = HashSet::new();
799            for files in generate_cxxqt_cpp_files(
800                &qml_module.rust_files,
801                &generated_header_dir,
802                header_prefix,
803            ) {
804                cc_builder.file(files.plain_cpp);
805                if let (Some(qobject), Some(qobject_header)) = (files.qobject, files.qobject_header)
806                {
807                    // Ensure that the generated QObject header is in the include path
808                    // so that qmltyperegistar can include them later
809                    if let Some(dir) = qobject_header.parent() {
810                        moc_include_paths.insert(dir.to_path_buf());
811                    }
812
813                    cc_builder.file(&qobject);
814                    let moc_products = qtbuild.moc(
815                        qobject_header,
816                        MocArguments::default().uri(qml_module.uri.clone()),
817                    );
818                    // Include the moc folder
819                    if let Some(dir) = moc_products.cpp.parent() {
820                        moc_include_paths.insert(dir.to_path_buf());
821                    }
822                    cc_builder.file(moc_products.cpp);
823                    qml_metatypes_json.push(moc_products.metatypes_json);
824                }
825            }
826
827            let qml_module_registration_files = qtbuild.register_qml_module(
828                &qml_metatypes_json,
829                &qml_module.uri,
830                qml_module.version_major,
831                qml_module.version_minor,
832                // TODO: This will be passed to the `optional plugin ...` part of the qmldir
833                // We don't load any shared libraries, so the name shouldn't matter
834                // But make sure it still works
835                &module_name_from_uri(&qml_module.uri),
836                &qml_module.qml_files,
837                &qml_module.qrc_files,
838            );
839            cc_builder
840                .file(qml_module_registration_files.qmltyperegistrar)
841                .file(qml_module_registration_files.plugin)
842                // In comparison to the other RCC files, we don't need to link this with whole-archive or
843                // anything like that.
844                // The plugin_init file already takes care of loading the resources associated with this
845                // RCC file.
846                .file(qml_module_registration_files.rcc);
847
848            // Add any include paths the qml module registration needs
849            // this is most likely the moc folder for the plugin
850            if let Some(include_path) = qml_module_registration_files.include_path {
851                moc_include_paths.insert(include_path);
852            }
853
854            // Ensure that all include paths from moc folders that are required
855            for include_path in &moc_include_paths {
856                cc_builder.include(include_path);
857            }
858
859            for qmlcachegen_file in qml_module_registration_files.qmlcachegen {
860                cc_builder.file(qmlcachegen_file);
861            }
862            // This is required, as described here: plugin_builder
863            cc_builder.define("QT_STATICPLUGIN", None);
864
865            // If any of the files inside the qml module change, then trigger a rerun
866            for path in qml_module.qml_files.iter().chain(
867                qml_module
868                    .rust_files
869                    .iter()
870                    .chain(qml_module.qrc_files.iter()),
871            ) {
872                println!("cargo::rerun-if-changed={}", path.display());
873            }
874
875            // Now all necessary symbols should be included in the cc_builder.
876            // However, the plugin needs to be initialized at runtime.
877            // This is done through the plugin_init file.
878            // It needs to be linked as an object file, to ensure that the linker doesn't throw away
879            // the static initializers in this file.
880            // For CMake builds, we export this file to then later include it as an object library in
881            // CMake.
882            // In cargo builds, add the object file as a direct argument to the linker.
883            Self::build_object_file(
884                init_builder,
885                &qml_module_registration_files.plugin_init,
886                dir::module_target(&qml_module.uri).join("plugin_init.o"),
887            );
888        }
889    }
890
891    fn setup_qt5_compatibility(&mut self, qtbuild: &qt_build_utils::QtBuild) {
892        // If we are using Qt 5 then write the std_types source
893        // This registers std numbers as a type for use in QML
894        //
895        // Note that we need this to be compiled into an object file
896        // as they are stored in statics in the source.
897        //
898        // TODO: Can we move this into cxx-qt so that it's only built
899        // once rather than for every cxx-qt-build? When we do this
900        // ensure that in a multi project that numbers work everywhere.
901        //
902        // Also then it should be possible to use CARGO_MANIFEST_DIR/src/std_types_qt5.cpp
903        // as path for cc::Build rather than copying the .cpp file
904        //
905        // https://github.com/rust-lang/rust/issues/108081
906        // https://github.com/KDAB/cxx-qt/pull/598
907        if qtbuild.version().major == 5 {
908            self.initializers
909                .push(include_str!("std_types_qt5.cpp").to_owned());
910        }
911    }
912
913    fn generate_init_code(&self, initializers: &HashSet<PathBuf>) -> String {
914        initializers
915            .iter()
916            .map(|path| std::fs::read_to_string(path).expect("Could not read initializer file!"))
917            .chain(self.initializers.iter().cloned())
918            .collect::<Vec<_>>()
919            .join("\n")
920    }
921
922    fn build_initializers(&mut self, init_builder: &cc::Build, initializers: &HashSet<PathBuf>) {
923        let initializers_path = dir::out().join("cxx-qt-build").join("initializers");
924        std::fs::create_dir_all(&initializers_path).expect("Failed to create initializers path!");
925
926        let initializers_path = initializers_path.join(format!("{}.cpp", crate_name()));
927        std::fs::write(&initializers_path, self.generate_init_code(initializers))
928            .expect("Could not write initializers file");
929        Self::build_object_file(
930            init_builder,
931            initializers_path,
932            dir::crate_target().join("initializers.o"),
933        );
934    }
935
936    fn generate_cpp_from_qrc_files(
937        &mut self,
938        qtbuild: &mut qt_build_utils::QtBuild,
939    ) -> HashSet<PathBuf> {
940        self.qrc_files
941            .iter()
942            .map(|qrc_file| {
943                // Also ensure that each of the files in the qrc can cause a change
944                for qrc_inner_file in qtbuild.qrc_list(&qrc_file) {
945                    println!("cargo::rerun-if-changed={}", qrc_inner_file.display());
946                }
947                // We need to link this using an object file or +whole-achive, the static initializer of
948                // the qrc file isn't lost.
949                qtbuild.qrc(&qrc_file)
950            })
951            .collect()
952    }
953
954    fn write_manifest(
955        &self,
956        dependencies: &[Dependency],
957        qt_modules: HashSet<String>,
958        initializers: HashSet<PathBuf>,
959    ) {
960        if let Some(interface) = &self.public_interface {
961            // We automatically reexport all qt_modules and initializers from downstream dependencies
962            // as they will always need to be enabled in the final binary.
963            // However, we only reexport the headers and compile-time definitions of libraries that
964            // are marked as re-export.
965            let dependencies = dependencies::reexported_dependencies(interface, dependencies);
966
967            let initializers = initializers.into_iter().collect();
968            let exported_include_prefixes =
969                dependencies::all_include_prefixes(interface, &dependencies);
970            let defines = dependencies::all_compile_definitions(Some(interface), &dependencies);
971
972            let manifest = Manifest {
973                name: crate_name(),
974                link_name: link_name()
975                    .expect("The links key must be set when creating a library with CXX-Qt-build!"),
976                defines,
977                initializers,
978                qt_modules: qt_modules.into_iter().collect(),
979                exported_include_prefixes,
980            };
981
982            let manifest_path = dir::crate_target().join("manifest.json");
983            let manifest_json = serde_json::to_string_pretty(&manifest)
984                .expect("Failed to convert Manifest to JSON!");
985            std::fs::write(&manifest_path, manifest_json).expect("Failed to write manifest.json!");
986            println!(
987                "cargo::metadata=CXX_QT_MANIFEST_PATH={}",
988                manifest_path.to_string_lossy()
989            );
990        }
991    }
992
993    fn qt_modules(&self, dependencies: &[Dependency]) -> HashSet<String> {
994        let mut qt_modules = self.qt_modules.clone();
995        for dependency in dependencies {
996            qt_modules.extend(dependency.manifest.qt_modules.iter().cloned());
997        }
998        qt_modules
999    }
1000
1001    fn write_interface_include_dirs(&self) {
1002        let Some(interface) = &self.public_interface else {
1003            return;
1004        };
1005        let header_root = dir::header_root();
1006        for (header_dir, dest) in &interface.exported_include_directories {
1007            let dest_dir = header_root.join(dest);
1008            if let Err(e) = dir::symlink_or_copy_directory(header_dir, dest_dir) {
1009                panic!(
1010                        "Failed to {INCLUDE_VERB} `{dest}` for export_include_directory `{dir_name}`: {e:?}",
1011                        dir_name = header_dir.to_string_lossy()
1012                    )
1013            };
1014        }
1015    }
1016
1017    /// Generate and compile cxx-qt C++ code, as well as compile any additional files from
1018    /// [CxxQtBuilder::qobject_header] and [CxxQtBuilder::cc_builder].
1019    pub fn build(mut self) {
1020        dir::clean(dir::crate_target()).expect("Failed to clean crate export directory!");
1021
1022        // We will do these two steps first, as setting up the dependencies can modify flags we
1023        // need further down the line
1024        // Also write the common headers first, to make sure they don't conflict with any
1025        // dependencies
1026        Self::write_common_headers();
1027        self.write_interface_include_dirs();
1028        let dependencies = Dependency::find_all();
1029        for dependency in &dependencies {
1030            self.include_dependency(dependency);
1031        }
1032        let qt_modules = self.qt_modules(&dependencies);
1033
1034        // Ensure that the linker is setup correctly for Cargo builds
1035        qt_build_utils::setup_linker();
1036
1037        let header_root = dir::header_root();
1038
1039        let mut qtbuild = qt_build_utils::QtBuild::new(qt_modules.iter().cloned().collect())
1040            .expect("Could not find Qt installation");
1041        qtbuild.cargo_link_libraries(&mut self.cc_builder);
1042        Self::define_qt_version_cfg_variables(qtbuild.version());
1043
1044        // Setup compilers
1045        // Static QML plugin and Qt resource initializers need to be linked as their own separate
1046        // object files because they use static variables which need to be initialized before main
1047        // (regardless of whether main is in Rust or C++). Normally linkers only copy symbols referenced
1048        // from within main when static linking, which would result in discarding those static variables.
1049        // Use a separate cc::Build for the little amount of code that needs to be built & linked this way.
1050        let mut init_builder = cc::Build::new();
1051        // Ensure that Qt modules and apple framework are linked and searched correctly
1052        qtbuild.cargo_link_libraries(&mut init_builder);
1053        let mut include_paths = qtbuild.include_paths();
1054        include_paths.push(header_root.clone());
1055        // TODO: Some of the code generated by qmltyperegistrar doesn't add the include_prefix to
1056        // the #include directives.
1057        // We therefore need to push the full header directory including the prefix as an include path.
1058        // This is not ideal and should be removed in future as it allows user code direct access
1059        // to the generated files without any namespacing.
1060        include_paths.push(header_root.join(&self.include_prefix));
1061
1062        let compile_definitions =
1063            dependencies::all_compile_definitions(self.public_interface.as_ref(), &dependencies);
1064        Self::setup_cc_builder(&mut self.cc_builder, &include_paths, &compile_definitions);
1065
1066        Self::setup_cc_builder(&mut init_builder, &include_paths, &compile_definitions);
1067        // Note: From now on the init_builder is correctly configured.
1068        // When building object files with this builder, we always need to copy it first.
1069        // So remove `mut` to ensure that we can't accidentally change the configuration or add
1070        // files.
1071        let init_builder = init_builder;
1072
1073        // Generate files
1074        self.generate_cpp_files_from_cxxqt_bridges(&header_root, &self.include_prefix.clone());
1075
1076        self.moc_qobject_headers(&mut qtbuild);
1077
1078        // Bridges for QML modules are handled separately because
1079        // the metatypes_json generated by moc needs to be passed to qmltyperegistrar
1080        self.build_qml_modules(
1081            &init_builder,
1082            &mut qtbuild,
1083            &header_root,
1084            &self.include_prefix.clone(),
1085        );
1086
1087        let mut initializers = self.generate_cpp_from_qrc_files(&mut qtbuild);
1088        initializers.extend(dependencies::initializer_paths(
1089            self.public_interface.as_ref(),
1090            &dependencies,
1091        ));
1092
1093        self.setup_qt5_compatibility(&qtbuild);
1094
1095        self.build_initializers(&init_builder, &initializers);
1096
1097        // Only compile if we have added files to the builder
1098        // otherwise we end up with no static library but ask cargo to link to it which causes an error
1099        if self.cc_builder.get_files().count() > 0 {
1100            // The linker argument order matters!
1101            // We need to link the object file first, then link the static library.
1102            // Otherwise, the linker will be unable to find the symbols in the static library file.
1103            // See also: https://stackoverflow.com/questions/45135/why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc
1104            if !dir::is_exporting() {
1105                println!("cargo::rustc-link-arg=-l{}", static_lib_name());
1106            }
1107
1108            self.cc_builder.compile(&static_lib_name());
1109        }
1110
1111        self.write_manifest(&dependencies, qt_modules, initializers);
1112    }
1113}