qt_build_utils/
lib.rs

1// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Be Wilson <be.wilson@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6#![deny(missing_docs)]
7
8//! This crate provides information about the Qt installation and can invoke Qt's
9//! [moc](https://doc.qt.io/qt-6/moc.html) code generator. This crate does not build
10//! any C++ code on its own. It is intended to be used in [build.rs scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
11//! together with
12//! [cc](https://docs.rs/cc/latest/cc/),
13//! [cxx_build](https://docs.rs/cxx-build/latest/cxx_build/), or
14//! [cpp_build](https://docs.rs/cpp_build/latest/cpp_build/).
15//!
16//! ⚠️ THIS CRATE IS UNSTABLE!
17//! It is used internally by [cxx-qt-build](https://crates.io/crates/cxx-qt-build) and may be
18//! stabilized in the future. For now, prefer use [cxx-qt-build] directly.
19
20#![allow(clippy::too_many_arguments)]
21
22mod cfg;
23pub use cfg::CfgGenerator;
24
25mod error;
26pub use error::QtBuildError;
27
28mod initializer;
29pub use initializer::Initializer;
30
31mod installation;
32pub use installation::QtInstallation;
33
34#[cfg(feature = "qmake")]
35pub use installation::qmake::QtInstallationQMake;
36
37#[cfg(feature = "qmake")]
38mod parse_cflags;
39
40mod platform;
41pub use platform::QtPlatformLinker;
42
43mod qml;
44pub use qml::{PluginType, QmlDirBuilder, QmlFile, QmlLsIniBuilder, QmlPluginCppBuilder, QmlUri};
45
46mod qrc;
47pub use qrc::{QResource, QResourceFile, QResources};
48
49mod tool;
50pub use tool::{
51    MocArguments, MocProducts, QmlCacheArguments, QmlCacheProducts, QtTool, QtToolMoc,
52    QtToolQmlCacheGen, QtToolQmlTypeRegistrar, QtToolRcc,
53};
54
55mod utils;
56
57use std::{
58    env,
59    ffi::{OsStr, OsString},
60    fs::File,
61    path::{Path, PathBuf},
62};
63
64use semver::Version;
65
66/// Paths to C++ files generated by [QtBuild::register_qml_module]
67pub struct QmlModuleRegistrationFiles {
68    /// File generated by [rcc](https://doc.qt.io/qt-6/rcc.html) for the QML plugin. The compiled static library
69    /// must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
70    /// or the linker will discard the generated static variables because they are not referenced from `main`.
71    pub rcc: PathBuf,
72    /// Files generated by [qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html). Must be linked with `+whole-archive`.
73    pub qmlcachegen: Vec<PathBuf>,
74    /// File generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
75    pub qmltyperegistrar: Option<PathBuf>,
76    /// The .qmltypes file generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
77    /// Mostly used for IDE support (e.g. qmllint/qmlls).
78    pub qmltypes: PathBuf,
79    /// qmldir file path.
80    /// Mostly used for better qmllint/qmlls support.
81    pub qmldir: PathBuf,
82    /// File with generated [QQmlEngineExtensionPlugin](https://doc.qt.io/qt-6/qqmlengineextensionplugin.html) that calls the function generated by qmltyperegistrar.
83    pub plugin: PathBuf,
84    /// Initializer that automatically registers the QQmlExtensionPlugin at startup.
85    pub plugin_init: Initializer,
86    /// An optional include path that should be included
87    pub include_path: Option<PathBuf>,
88    /// The original QML files defined in the QML module
89    pub qml_files: Vec<PathBuf>,
90}
91
92/// Helper for build.rs scripts using Qt
93/// ```
94/// let qt_modules = vec!["Core", "Gui"]
95///     .iter()
96///     .map(|m| String::from(*m))
97///     .collect();
98/// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
99/// ```
100pub struct QtBuild {
101    qt_installation: Box<dyn QtInstallation>,
102    qt_modules: Vec<String>,
103    autorcc_options: Vec<OsString>,
104}
105
106impl QtBuild {
107    /// Create a [QtBuild] using the default [QtInstallation] (currently uses [QtInstallationQMake])
108    /// and specify which Qt modules you are linking, ommitting the `Qt` prefix (`"Core"`
109    /// rather than `"QtCore"`).
110    ///
111    /// Currently this function is only available when the `qmake` feature is enabled.
112    /// Use [Self::with_installation] to create a [QtBuild] with a custom [QtInstallation].
113    #[cfg(feature = "qmake")]
114    pub fn new(qt_modules: Vec<String>) -> anyhow::Result<Self> {
115        let qt_installation = Box::new(QtInstallationQMake::new()?);
116        Ok(Self::with_installation(qt_installation, qt_modules))
117    }
118
119    /// Create a [QtBuild] using the given [QtInstallation] and specify which
120    /// Qt modules you are linking, ommitting the `Qt` prefix (`"Core"` rather than `"QtCore"`).
121    pub fn with_installation(
122        qt_installation: Box<dyn QtInstallation>,
123        mut qt_modules: Vec<String>,
124    ) -> Self {
125        if qt_modules.is_empty() {
126            qt_modules.push("Core".to_owned());
127        }
128
129        Self {
130            qt_installation,
131            qt_modules,
132            autorcc_options: Vec::new(),
133        }
134    }
135
136    /// Add custom arguments to be passed to the end of the rcc invocation when converting qrc files.
137    pub fn autorcc_options(mut self, options: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Self {
138        self.autorcc_options = options
139            .into_iter()
140            .map(|s| s.as_ref().to_os_string())
141            .collect();
142        self
143    }
144
145    /// Tell Cargo to link each Qt module.
146    pub fn cargo_link_libraries(&self, builder: &mut cc::Build) {
147        self.qt_installation.link_modules(builder, &self.qt_modules);
148    }
149
150    /// Get the frmaework paths for Qt. This is intended to be passed to whichever tool
151    /// you are using to invoke the C++ compiler.
152    pub fn framework_paths(&self) -> Vec<PathBuf> {
153        self.qt_installation.framework_paths(&self.qt_modules)
154    }
155
156    /// Get the include paths for Qt, including Qt module subdirectories. This is intended
157    /// to be passed to whichever tool you are using to invoke the C++ compiler.
158    pub fn include_paths(&self) -> Vec<PathBuf> {
159        self.qt_installation.include_paths(&self.qt_modules)
160    }
161
162    /// Version of the detected Qt installation
163    pub fn version(&self) -> Version {
164        self.qt_installation.version()
165    }
166
167    /// Create a [QtToolMoc] for this [QtBuild]
168    ///
169    /// This allows for using [moc](https://doc.qt.io/qt-6/moc.html)
170    pub fn moc(&mut self) -> QtToolMoc {
171        QtToolMoc::new(self.qt_installation.as_ref(), &self.qt_modules)
172    }
173
174    /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
175    ///
176    /// This generates a [qmldir file](https://doc.qt.io/qt-6/qtqml-modules-qmldir.html) for the QML module.
177    /// The `qml_files` and `qrc_files` are registered with the [Qt Resource System](https://doc.qt.io/qt-6/resources.html) in
178    /// the [default QML import path](https://doc.qt.io/qt-6/qtqml-syntax-imports.html#qml-import-path) `qrc:/qt/qml/uri/of/module/`.
179    ///
180    /// When using Qt 6, this will [run qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html) to compile the specified .qml files ahead-of-time.
181    pub fn register_qml_module(
182        &mut self,
183        metatypes_json: &[impl AsRef<Path>],
184        uri: &QmlUri,
185        version_major: usize,
186        version_minor: usize,
187        plugin_name: &str,
188        qml_files: &[QmlFile],
189        depends: impl IntoIterator<Item = impl Into<QmlUri>>,
190        plugin_type: PluginType,
191    ) -> QmlModuleRegistrationFiles {
192        let qml_uri_dirs = uri.as_dirs();
193        let qml_uri_underscores = uri.as_underscores();
194        let plugin_type_info = "plugin.qmltypes";
195        let plugin_class_name = format!("{}_plugin", qml_uri_underscores);
196
197        let out_dir = env::var("OUT_DIR").unwrap();
198        let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils"));
199        std::fs::create_dir_all(&qt_build_utils_dir).expect("Could not create qt_build_utils dir");
200
201        let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs);
202        std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");
203
204        let qmltypes_path = qml_module_dir.join(plugin_type_info);
205
206        // Generate qmldir file
207        let qmldir_file_path = qml_module_dir.join("qmldir");
208        {
209            let qml_type_files = qml_files
210                .iter()
211                .filter(|file| {
212                    // Qt by default only includes uppercase files in the qmldir file.
213                    // Mirror this behavior.
214                    file.get_path()
215                        .file_name()
216                        .and_then(OsStr::to_str)
217                        .and_then(|file_name| file_name.chars().next())
218                        .map(char::is_uppercase)
219                        .unwrap_or_default()
220                })
221                .cloned();
222            let mut file = File::create(&qmldir_file_path).expect("Could not create qmldir file");
223            QmlDirBuilder::new(uri.clone())
224                .depends(depends)
225                .plugin(plugin_name, true)
226                .class_name(&plugin_class_name)
227                .type_info(plugin_type_info)
228                .qml_files(qml_type_files)
229                .write(&mut file)
230                .expect("Could not write qmldir file");
231        }
232
233        // Generate .qrc file and run rcc on it
234        // TODO: Replace with an equivalent of [qt_add_resources](https://doc.qt.io/qt-6/qt-add-resources.html)
235        let qrc_path =
236            qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc"));
237        {
238            let qml_module_dir_str = qml_module_dir.to_str().unwrap();
239            let qml_uri_dirs_prefix = format!("/qt/qml/{qml_uri_dirs}");
240            let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
241            QResources::new()
242                .resource(QResource::new().prefix("/".to_string()).file(
243                    QResourceFile::new(qml_module_dir_str).alias(qml_uri_dirs_prefix.clone()),
244                ))
245                .resource({
246                    let mut resource = QResource::new().prefix(qml_uri_dirs_prefix.clone()).file(
247                        QResourceFile::new(format!("{qml_module_dir_str}/qmldir"))
248                            .alias("qmldir".to_string()),
249                    );
250
251                    fn resource_add_path(resource: QResource, path: &Path) -> QResource {
252                        let resolved = std::fs::canonicalize(path)
253                            .unwrap_or_else(|_| {
254                                panic!("Could not canonicalize path {}", path.display())
255                            })
256                            .display()
257                            .to_string();
258                        resource
259                            .file(QResourceFile::new(resolved).alias(path.display().to_string()))
260                    }
261
262                    for file in qml_files {
263                        resource = resource_add_path(resource, file.get_path());
264                    }
265                    resource
266                })
267                .write(&mut qrc)
268                .expect("Could note write qrc file");
269        }
270
271        // Run qmlcachegen
272        // qmlcachegen needs to be run once for each .qml file with --resource-path,
273        // then once for the module with --resource-name.
274        let mut qmlcachegen_file_paths = Vec::new();
275
276        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
277        if self.qt_installation.version().major >= 6 {
278            let qml_cache_args = QmlCacheArguments {
279                uri: uri.clone(),
280                qmldir_path: qmldir_file_path.clone(),
281                qmldir_qrc_path: qrc_path.clone(),
282            };
283            let mut qml_resource_paths = Vec::new();
284            for file in qml_files {
285                let result = QtToolQmlCacheGen::new(self.qt_installation.as_ref())
286                    .compile(qml_cache_args.clone(), file.get_path());
287                qmlcachegen_file_paths.push(result.qml_cache_path);
288                qml_resource_paths.push(result.qml_resource_path);
289            }
290
291            // If there are no QML files there is nothing for qmlcachegen to run with
292            if !qml_files.is_empty() {
293                qmlcachegen_file_paths.push(
294                    QtToolQmlCacheGen::new(self.qt_installation.as_ref())
295                        .compile_loader(qml_cache_args.clone(), &qml_resource_paths),
296                );
297            }
298        }
299
300        let qml_plugin_dir = PathBuf::from(format!("{out_dir}/qt-build-utils/qml_plugin"));
301        std::fs::create_dir_all(&qml_plugin_dir).expect("Could not create qml_plugin dir");
302
303        // Run qmltyperegistrar over the meta types
304        let qmltyperegistrar_path = self.qmltyperegistrar().compile(
305            metatypes_json,
306            &qmltypes_path,
307            uri,
308            Version::new(version_major as u64, version_minor as u64, 0),
309        );
310
311        // Generate QQmlEngineExtensionPlugin
312        let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
313        let include_path;
314        {
315            let mut file = File::create(&qml_plugin_cpp_path)
316                .expect("Could not create plugin definition file");
317            let plugin_init = QmlPluginCppBuilder::new(uri.clone(), plugin_class_name.clone())
318                .qml_cache(!qml_files.is_empty() && !qmlcachegen_file_paths.is_empty())
319                .plugin_type(plugin_type)
320                .write(&mut file)
321                .expect("Failed to write plugin definition");
322
323            let moc_product = self.moc().compile(
324                &qml_plugin_cpp_path,
325                MocArguments::default().uri(uri.to_owned()),
326            );
327            // Pass the include directory of the moc file to the caller
328            include_path = moc_product.cpp.parent().map(Path::to_path_buf);
329
330            let qml_files = qml_files
331                .iter()
332                .map(|qml_file| qml_file.get_path().to_path_buf())
333                .collect();
334
335            let rcc = self.rcc().compile(&qrc_path);
336            QmlModuleRegistrationFiles {
337                // The rcc file is automatically initialized when importing the plugin.
338                // so we don't need to treat it like an initializer here.
339                rcc: rcc.file.unwrap(),
340                qmlcachegen: qmlcachegen_file_paths,
341                qmltyperegistrar: qmltyperegistrar_path,
342                qmltypes: qmltypes_path,
343                qmldir: qmldir_file_path,
344                plugin: qml_plugin_cpp_path,
345                plugin_init,
346                include_path,
347                qml_files,
348            }
349        }
350    }
351
352    /// Create a [QtToolRcc] for this [QtBuild]
353    ///
354    /// This allows for using [rcc](https://doc.qt.io/qt-6/resources.html)
355    pub fn rcc(&self) -> QtToolRcc {
356        QtToolRcc::new(self.qt_installation.as_ref()).custom_args(&self.autorcc_options)
357    }
358
359    /// Create a [QtToolQmlTypeRegistrar] for this [QtBuild]
360    pub fn qmltyperegistrar(&self) -> QtToolQmlTypeRegistrar {
361        QtToolQmlTypeRegistrar::new(self.qt_installation.as_ref())
362    }
363}