Skip to main content

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)]
21mod cfg;
22pub use cfg::CfgGenerator;
23
24mod error;
25pub use error::QtBuildError;
26
27mod initializer;
28pub use initializer::Initializer;
29
30mod installation;
31pub use installation::QtInstallation;
32
33#[cfg(feature = "qmake")]
34pub use installation::qmake::QtInstallationQMake;
35
36#[cfg(feature = "qt_minimal")]
37pub use installation::qt_minimal::QtInstallationQtMinimal;
38
39#[cfg(feature = "qmake")]
40mod parse_cflags;
41
42mod platform;
43pub use platform::QtPlatformLinker;
44
45mod qml;
46pub use qml::{PluginType, QmlDirBuilder, QmlFile, QmlLsIniBuilder, QmlPluginCppBuilder, QmlUri};
47
48mod qrc;
49pub use qrc::{QResource, QResourceFile, QResources};
50
51mod tool;
52pub use tool::{
53    MocArguments, MocProducts, QmlCacheArguments, QmlCacheProducts, QtPathsQueryArguments, QtTool,
54    QtToolMoc, QtToolQmlCacheGen, QtToolQmlTypeRegistrar, QtToolQtPaths, QtToolRcc,
55};
56
57mod utils;
58
59use std::{
60    env,
61    ffi::{OsStr, OsString},
62    fs::File,
63    path::{Path, PathBuf},
64};
65
66use semver::Version;
67
68/// Paths to C++ files generated by [QtBuild::register_qml_module]
69pub struct QmlModuleRegistrationFiles {
70    /// File generated by [rcc](https://doc.qt.io/qt-6/rcc.html) for the QML plugin. The compiled static library
71    /// must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
72    /// or the linker will discard the generated static variables because they are not referenced from `main`.
73    pub rcc: PathBuf,
74    /// Files generated by [qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html). Must be linked with `+whole-archive`.
75    pub qmlcachegen: Vec<PathBuf>,
76    /// File generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
77    pub qmltyperegistrar: Option<PathBuf>,
78    /// The .qmltypes file generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
79    /// Mostly used for IDE support (e.g. qmllint/qmlls).
80    pub qmltypes: PathBuf,
81    /// qmldir file path.
82    /// Mostly used for better qmllint/qmlls support.
83    pub qmldir: PathBuf,
84    /// File with generated [QQmlEngineExtensionPlugin](https://doc.qt.io/qt-6/qqmlengineextensionplugin.html) that calls the function generated by qmltyperegistrar.
85    pub plugin: PathBuf,
86    /// Initializer that automatically registers the QQmlExtensionPlugin at startup.
87    pub plugin_init: Initializer,
88    /// An optional include path that should be included
89    pub include_path: Option<PathBuf>,
90    /// The original QML files defined in the QML module
91    pub qml_files: Vec<PathBuf>,
92}
93
94/// Helper for build.rs scripts using Qt
95/// ```
96/// let qt_modules = vec!["Core", "Gui"]
97///     .iter()
98///     .map(|m| String::from(*m))
99///     .collect();
100/// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
101/// ```
102pub struct QtBuild {
103    qt_installation: Box<dyn QtInstallation>,
104    qt_modules: Vec<String>,
105    autorcc_options: Vec<OsString>,
106}
107
108impl QtBuild {
109    /// Create a [QtBuild] automatically determining the [QtInstallation] depending on enable features
110    /// and specify which Qt modules you are linking, ommitting the `Qt` prefix (`"Core"`
111    /// rather than `"QtCore"`).
112    pub fn new(qt_modules: Vec<String>) -> anyhow::Result<Self> {
113        let find_qt_installation = || -> anyhow::Result<Box<dyn QtInstallation>> {
114            // If QMAKE env var is set then try this first
115            //
116            // NOTE: if the env is set but qmake doesn't exist we fail
117            #[cfg(feature = "qmake")]
118            {
119                if let Some(result) = QtInstallationQMake::try_from_qmake_env() {
120                    return result
121                        .map(|installation| -> Box<dyn QtInstallation> { Box::new(installation) });
122                }
123            }
124
125            #[cfg(feature = "qt_version")]
126            let mut local_versions = std::collections::BTreeSet::new();
127
128            // Auto determining Qt version from crates is enabled
129            #[cfg(feature = "qt_version")]
130            {
131                let versions = qt_version::qt_versions();
132
133                // Check for a qmake install in PATH
134                #[cfg(feature = "qmake")]
135                {
136                    // See if qmake matches the Qt version range
137                    if let Ok(qt_installation) = QtInstallationQMake::try_from_path() {
138                        if versions.contains(&qt_installation.version()) {
139                            return Ok(Box::new(qt_installation));
140                        }
141
142                        local_versions.insert(qt_installation.version());
143                    }
144                }
145
146                // Check for a qt_minimal install
147                #[cfg(feature = "qt_minimal")]
148                {
149                    // Search existing installed qt_minimal versions
150                    if let Ok(local_artifacts) = QtInstallationQtMinimal::local_artifacts() {
151                        // Find artifacts with the version range for our OS and arch
152                        let artifacts = QtInstallationQtMinimal::match_artifact_requirements(
153                            local_artifacts.clone(),
154                            &versions,
155                        );
156
157                        // Merge artifacts into combined bin/ and include/
158                        let mut artifacts = QtInstallationQtMinimal::group_artifacts(artifacts);
159
160                        // Sort the artifacts by version
161                        artifacts.sort_by_key(|artifact| artifact.version.clone());
162
163                        // Try all the local available Qt minimal installs
164                        // starting with the largest Qt version
165                        for artifact in artifacts.into_iter().rev() {
166                            // Try building a Qt installation from the url
167                            if let Ok(qt_installation) =
168                                QtInstallationQtMinimal::try_from(PathBuf::from(artifact.url))
169                            {
170                                return Ok(Box::new(qt_installation));
171                            }
172                        }
173
174                        local_versions
175                            .extend(local_artifacts.into_iter().map(|artifact| artifact.version));
176                    }
177
178                    // Download from Qt artifacts
179                    //
180                    // NOTE: we assume the last version is the newest and
181                    // try each version in case there is a mismatch between
182                    // qt_artifacts and qt_versions
183                    for version in versions.into_iter().rev() {
184                        if let Ok(qt_installation) = QtInstallationQtMinimal::try_from(version) {
185                            return Ok(Box::new(qt_installation));
186                        }
187                    }
188                }
189            }
190
191            #[cfg(not(feature = "qt_version"))]
192            {
193                // Check for a qmake install
194                #[cfg(feature = "qmake")]
195                {
196                    // See if qmake matches the Qt version range
197                    if let Ok(qt_installation) = QtInstallationQMake::try_from_path() {
198                        return Ok(Box::new(qt_installation));
199                    }
200                }
201
202                // NOTE: qt_minimal feature implies qt_version feature
203                // so we do not need to check for qt_minimal
204            }
205
206            #[cfg(feature = "qt_version")]
207            {
208                Err(QtBuildError::QtMissingVersion {
209                    available_versions: local_versions.into_iter().collect(),
210                    requested_versions: qt_version::qt_versions(),
211                }
212                .into())
213            }
214
215            #[cfg(not(feature = "qt_version"))]
216            Err(QtBuildError::QtMissing.into())
217        };
218
219        Ok(Self::with_installation(find_qt_installation()?, qt_modules))
220    }
221
222    /// Create a [QtBuild] using the given [QtInstallation] and specify which
223    /// Qt modules you are linking, ommitting the `Qt` prefix (`"Core"` rather than `"QtCore"`).
224    pub fn with_installation(
225        qt_installation: Box<dyn QtInstallation>,
226        mut qt_modules: Vec<String>,
227    ) -> Self {
228        if qt_modules.is_empty() {
229            qt_modules.push("Core".to_owned());
230        }
231
232        Self {
233            qt_installation,
234            qt_modules,
235            autorcc_options: Vec::new(),
236        }
237    }
238
239    /// Add custom arguments to be passed to the end of the rcc invocation when converting qrc files.
240    pub fn autorcc_options(mut self, options: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Self {
241        self.autorcc_options = options
242            .into_iter()
243            .map(|s| s.as_ref().to_os_string())
244            .collect();
245        self
246    }
247
248    /// Tell Cargo to link each Qt module.
249    pub fn cargo_link_libraries(&self, builder: &mut cc::Build) {
250        self.qt_installation.link_modules(builder, &self.qt_modules);
251    }
252
253    /// Get the frmaework paths for Qt. This is intended to be passed to whichever tool
254    /// you are using to invoke the C++ compiler.
255    pub fn framework_paths(&self) -> Vec<PathBuf> {
256        self.qt_installation.framework_paths(&self.qt_modules)
257    }
258
259    /// Get the include paths for Qt, including Qt module subdirectories. This is intended
260    /// to be passed to whichever tool you are using to invoke the C++ compiler.
261    pub fn include_paths(&self) -> Vec<PathBuf> {
262        self.qt_installation.include_paths(&self.qt_modules)
263    }
264
265    /// Get the inner [QtInstallation] implementation
266    pub fn installation(&self) -> &dyn QtInstallation {
267        self.qt_installation.as_ref()
268    }
269
270    /// Version of the detected Qt installation
271    pub fn version(&self) -> Version {
272        self.qt_installation.version()
273    }
274
275    /// Create a [QtToolMoc] for this [QtBuild]
276    ///
277    /// This allows for using [moc](https://doc.qt.io/qt-6/moc.html)
278    pub fn moc(&mut self) -> QtToolMoc {
279        QtToolMoc::new(self.qt_installation.as_ref(), &self.qt_modules)
280    }
281
282    /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
283    ///
284    /// This generates a [qmldir file](https://doc.qt.io/qt-6/qtqml-modules-qmldir.html) for the QML module.
285    /// The `qml_files` and `qrc_files` are registered with the [Qt Resource System](https://doc.qt.io/qt-6/resources.html) in
286    /// the [default QML import path](https://doc.qt.io/qt-6/qtqml-syntax-imports.html#qml-import-path) `qrc:/qt/qml/uri/of/module/`.
287    ///
288    /// 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.
289    pub fn register_qml_module(
290        &mut self,
291        metatypes_json: &[impl AsRef<Path>],
292        uri: &QmlUri,
293        version_major: usize,
294        version_minor: usize,
295        plugin_name: &str,
296        qml_files: &[QmlFile],
297        depends: impl IntoIterator<Item = impl Into<QmlUri>>,
298        plugin_type: PluginType,
299    ) -> QmlModuleRegistrationFiles {
300        let qml_uri_dirs = uri.as_dirs();
301        let qml_uri_underscores = uri.as_underscores();
302        let plugin_type_info = "plugin.qmltypes";
303        let plugin_class_name = format!("{}_plugin", qml_uri_underscores);
304
305        let out_dir = env::var("OUT_DIR").unwrap();
306        let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils"));
307        std::fs::create_dir_all(&qt_build_utils_dir).expect("Could not create qt_build_utils dir");
308
309        let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs);
310        std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");
311
312        let qmltypes_path = qml_module_dir.join(plugin_type_info);
313
314        // Generate qmldir file
315        let qmldir_file_path = qml_module_dir.join("qmldir");
316        {
317            let qml_type_files = qml_files
318                .iter()
319                .filter(|file| {
320                    // Qt by default only includes uppercase files in the qmldir file.
321                    // Mirror this behavior.
322                    file.get_path()
323                        .file_name()
324                        .and_then(OsStr::to_str)
325                        .and_then(|file_name| file_name.chars().next())
326                        .map(char::is_uppercase)
327                        .unwrap_or_default()
328                })
329                .cloned();
330            let mut file = File::create(&qmldir_file_path).expect("Could not create qmldir file");
331            QmlDirBuilder::new(uri.clone())
332                .depends(depends)
333                .plugin(plugin_name, true)
334                .class_name(&plugin_class_name)
335                .type_info(plugin_type_info)
336                .qml_files(qml_type_files)
337                .write(&mut file)
338                .expect("Could not write qmldir file");
339        }
340
341        // Generate .qrc file and run rcc on it
342        // TODO: Replace with an equivalent of [qt_add_resources](https://doc.qt.io/qt-6/qt-add-resources.html)
343        let qrc_path =
344            qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc"));
345        {
346            let qml_module_dir_str = qml_module_dir.to_str().unwrap();
347            let qml_uri_dirs_prefix = format!("/qt/qml/{qml_uri_dirs}");
348            let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
349            QResources::new()
350                .resource({
351                    let mut resource = QResource::new().prefix(qml_uri_dirs_prefix.clone()).file(
352                        QResourceFile::new(format!("{qml_module_dir_str}/qmldir"))
353                            .alias("qmldir".to_string()),
354                    );
355
356                    fn resource_add_path(resource: QResource, path: &Path) -> QResource {
357                        let resolved = std::fs::canonicalize(path)
358                            .unwrap_or_else(|_| {
359                                panic!("Could not canonicalize path {}", path.display())
360                            })
361                            .display()
362                            .to_string();
363                        resource
364                            .file(QResourceFile::new(resolved).alias(path.display().to_string()))
365                    }
366
367                    for file in qml_files {
368                        resource = resource_add_path(resource, file.get_path());
369                    }
370                    resource
371                })
372                .write(&mut qrc)
373                .expect("Could note write qrc file");
374        }
375
376        // Run qmlcachegen
377        // qmlcachegen needs to be run once for each .qml file with --resource-path,
378        // then once for the module with --resource-name.
379        let mut qmlcachegen_file_paths = Vec::new();
380
381        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
382        if self.qt_installation.version().major >= 6 {
383            let qml_cache_args = QmlCacheArguments {
384                uri: uri.clone(),
385                qmldir_path: qmldir_file_path.clone(),
386                qmldir_qrc_path: qrc_path.clone(),
387            };
388            let mut qml_resource_paths = Vec::new();
389            for file in qml_files {
390                let result = QtToolQmlCacheGen::new(self.qt_installation.as_ref())
391                    .compile(qml_cache_args.clone(), file.get_path());
392                qmlcachegen_file_paths.push(result.qml_cache_path);
393                qml_resource_paths.push(result.qml_resource_path);
394            }
395
396            // If there are no QML files there is nothing for qmlcachegen to run with
397            if !qml_files.is_empty() {
398                qmlcachegen_file_paths.push(
399                    QtToolQmlCacheGen::new(self.qt_installation.as_ref())
400                        .compile_loader(qml_cache_args.clone(), &qml_resource_paths),
401                );
402            }
403        }
404
405        let qml_plugin_dir = PathBuf::from(format!("{out_dir}/qt-build-utils/qml_plugin"));
406        std::fs::create_dir_all(&qml_plugin_dir).expect("Could not create qml_plugin dir");
407
408        // Run qmltyperegistrar over the meta types
409        let qmltyperegistrar_path = self.qmltyperegistrar().compile(
410            metatypes_json,
411            &qmltypes_path,
412            uri,
413            Version::new(version_major as u64, version_minor as u64, 0),
414        );
415
416        // Generate QQmlEngineExtensionPlugin
417        let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
418        let include_path;
419        {
420            let mut file = File::create(&qml_plugin_cpp_path)
421                .expect("Could not create plugin definition file");
422            let plugin_init = QmlPluginCppBuilder::new(uri.clone(), plugin_class_name.clone())
423                .qml_cache(!qml_files.is_empty() && !qmlcachegen_file_paths.is_empty())
424                .plugin_type(plugin_type)
425                .write(&mut file)
426                .expect("Failed to write plugin definition");
427
428            let moc_product = self.moc().compile(
429                &qml_plugin_cpp_path,
430                MocArguments::default().uri(uri.to_owned()),
431            );
432            // Pass the include directory of the moc file to the caller
433            include_path = moc_product.cpp.parent().map(Path::to_path_buf);
434
435            let qml_files = qml_files
436                .iter()
437                .map(|qml_file| qml_file.get_path().to_path_buf())
438                .collect();
439
440            let rcc = self.rcc().compile(&qrc_path);
441            QmlModuleRegistrationFiles {
442                // The rcc file is automatically initialized when importing the plugin.
443                // so we don't need to treat it like an initializer here.
444                rcc: rcc.file.unwrap(),
445                qmlcachegen: qmlcachegen_file_paths,
446                qmltyperegistrar: qmltyperegistrar_path,
447                qmltypes: qmltypes_path,
448                qmldir: qmldir_file_path,
449                plugin: qml_plugin_cpp_path,
450                plugin_init,
451                include_path,
452                qml_files,
453            }
454        }
455    }
456
457    /// Create a [QtToolRcc] for this [QtBuild]
458    ///
459    /// This allows for using [rcc](https://doc.qt.io/qt-6/resources.html)
460    pub fn rcc(&self) -> QtToolRcc {
461        QtToolRcc::new(self.qt_installation.as_ref()).custom_args(&self.autorcc_options)
462    }
463
464    /// Create a [QtToolQmlTypeRegistrar] for this [QtBuild]
465    pub fn qmltyperegistrar(&self) -> QtToolQmlTypeRegistrar {
466        QtToolQmlTypeRegistrar::new(self.qt_installation.as_ref())
467    }
468
469    /// Create a [QtToolQtPaths] for this [QtBuild]
470    pub fn qtpaths(&self) -> QtToolQtPaths {
471        QtToolQtPaths::new(self.qt_installation.as_ref())
472    }
473}