qt-build-utils 0.9.1

Build script helper for linking Qt libraries and using moc code generator. Intended to be used together with cc, cpp_build, or cxx_build
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
// SPDX-FileContributor: Be Wilson <be.wilson@kdab.com>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

#![deny(missing_docs)]

//! This crate provides information about the Qt installation and can invoke Qt's
//! [moc](https://doc.qt.io/qt-6/moc.html) code generator. This crate does not build
//! 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)
//! together with
//! [cc](https://docs.rs/cc/latest/cc/),
//! [cxx_build](https://docs.rs/cxx-build/latest/cxx_build/), or
//! [cpp_build](https://docs.rs/cpp_build/latest/cpp_build/).
//!
//! ⚠️ THIS CRATE IS UNSTABLE!
//! It is used internally by [cxx-qt-build](https://crates.io/crates/cxx-qt-build) and may be
//! stabilized in the future. For now, prefer use [cxx-qt-build] directly.

#![allow(clippy::too_many_arguments)]
mod cfg;
pub use cfg::CfgGenerator;

mod error;
pub use error::QtBuildError;

mod initializer;
pub use initializer::Initializer;

mod installation;
pub use installation::QtInstallation;

#[cfg(feature = "qmake")]
pub use installation::qmake::QtInstallationQMake;

#[cfg(feature = "qt_minimal")]
pub use installation::qt_minimal::QtInstallationQtMinimal;

#[cfg(feature = "qmake")]
mod parse_cflags;

mod platform;
pub use platform::QtPlatformLinker;

mod qml;
pub use qml::{PluginType, QmlDirBuilder, QmlFile, QmlLsIniBuilder, QmlPluginCppBuilder, QmlUri};

mod qrc;
pub use qrc::{QResource, QResourceFile, QResources};

mod tool;
pub use tool::{
    MocArguments, MocProducts, QmlCacheArguments, QmlCacheProducts, QtPathsQueryArguments, QtTool,
    QtToolMoc, QtToolQmlCacheGen, QtToolQmlTypeRegistrar, QtToolQtPaths, QtToolRcc,
};

mod utils;

use std::{
    env,
    ffi::{OsStr, OsString},
    fs::File,
    path::{Path, PathBuf},
};

use semver::Version;

/// Paths to C++ files generated by [QtBuild::register_qml_module]
pub struct QmlModuleRegistrationFiles {
    /// File generated by [rcc](https://doc.qt.io/qt-6/rcc.html) for the QML plugin. The compiled static library
    /// must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
    /// or the linker will discard the generated static variables because they are not referenced from `main`.
    pub rcc: PathBuf,
    /// Files generated by [qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html). Must be linked with `+whole-archive`.
    pub qmlcachegen: Vec<PathBuf>,
    /// File generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
    pub qmltyperegistrar: Option<PathBuf>,
    /// The .qmltypes file generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
    /// Mostly used for IDE support (e.g. qmllint/qmlls).
    pub qmltypes: PathBuf,
    /// qmldir file path.
    /// Mostly used for better qmllint/qmlls support.
    pub qmldir: PathBuf,
    /// File with generated [QQmlEngineExtensionPlugin](https://doc.qt.io/qt-6/qqmlengineextensionplugin.html) that calls the function generated by qmltyperegistrar.
    pub plugin: PathBuf,
    /// Initializer that automatically registers the QQmlExtensionPlugin at startup.
    pub plugin_init: Initializer,
    /// An optional include path that should be included
    pub include_path: Option<PathBuf>,
    /// The original QML files defined in the QML module
    pub qml_files: Vec<PathBuf>,
}

/// Helper for build.rs scripts using Qt
/// ```
/// let qt_modules = vec!["Core", "Gui"]
///     .iter()
///     .map(|m| String::from(*m))
///     .collect();
/// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
/// ```
pub struct QtBuild {
    qt_installation: Box<dyn QtInstallation>,
    qt_modules: Vec<String>,
    autorcc_options: Vec<OsString>,
}

impl QtBuild {
    /// Create a [QtBuild] automatically determining the [QtInstallation] depending on enable features
    /// and specify which Qt modules you are linking, ommitting the `Qt` prefix (`"Core"`
    /// rather than `"QtCore"`).
    pub fn new(qt_modules: Vec<String>) -> anyhow::Result<Self> {
        let find_qt_installation = || -> anyhow::Result<Box<dyn QtInstallation>> {
            // If QMAKE env var is set then try this first
            //
            // NOTE: if the env is set but qmake doesn't exist we fail
            #[cfg(feature = "qmake")]
            {
                if let Some(result) = QtInstallationQMake::try_from_qmake_env() {
                    return result
                        .map(|installation| -> Box<dyn QtInstallation> { Box::new(installation) });
                }
            }

            #[cfg(feature = "qt_version")]
            let mut local_versions = std::collections::BTreeSet::new();

            // Auto determining Qt version from crates is enabled
            #[cfg(feature = "qt_version")]
            {
                let versions = qt_version::qt_versions();

                // Check for a qmake install in PATH
                #[cfg(feature = "qmake")]
                {
                    // See if qmake matches the Qt version range
                    if let Ok(qt_installation) = QtInstallationQMake::try_from_path() {
                        if versions.contains(&qt_installation.version()) {
                            return Ok(Box::new(qt_installation));
                        }

                        local_versions.insert(qt_installation.version());
                    }
                }

                // Check for a qt_minimal install
                #[cfg(feature = "qt_minimal")]
                {
                    // Search existing installed qt_minimal versions
                    if let Ok(local_artifacts) = QtInstallationQtMinimal::local_artifacts() {
                        // Find artifacts with the version range for our OS and arch
                        let artifacts = QtInstallationQtMinimal::match_artifact_requirements(
                            local_artifacts.clone(),
                            &versions,
                        );

                        // Merge artifacts into combined bin/ and include/
                        let mut artifacts = QtInstallationQtMinimal::group_artifacts(artifacts);

                        // Sort the artifacts by version
                        artifacts.sort_by_key(|artifact| artifact.version.clone());

                        // Try all the local available Qt minimal installs
                        // starting with the largest Qt version
                        for artifact in artifacts.into_iter().rev() {
                            // Try building a Qt installation from the url
                            if let Ok(qt_installation) =
                                QtInstallationQtMinimal::try_from(PathBuf::from(artifact.url))
                            {
                                return Ok(Box::new(qt_installation));
                            }
                        }

                        local_versions
                            .extend(local_artifacts.into_iter().map(|artifact| artifact.version));
                    }

                    // Download from Qt artifacts
                    //
                    // NOTE: we assume the last version is the newest and
                    // try each version in case there is a mismatch between
                    // qt_artifacts and qt_versions
                    for version in versions.into_iter().rev() {
                        if let Ok(qt_installation) = QtInstallationQtMinimal::try_from(version) {
                            return Ok(Box::new(qt_installation));
                        }
                    }
                }
            }

            #[cfg(not(feature = "qt_version"))]
            {
                // Check for a qmake install
                #[cfg(feature = "qmake")]
                {
                    // See if qmake matches the Qt version range
                    if let Ok(qt_installation) = QtInstallationQMake::try_from_path() {
                        return Ok(Box::new(qt_installation));
                    }
                }

                // NOTE: qt_minimal feature implies qt_version feature
                // so we do not need to check for qt_minimal
            }

            #[cfg(feature = "qt_version")]
            {
                Err(QtBuildError::QtMissingVersion {
                    available_versions: local_versions.into_iter().collect(),
                    requested_versions: qt_version::qt_versions(),
                }
                .into())
            }

            #[cfg(not(feature = "qt_version"))]
            Err(QtBuildError::QtMissing.into())
        };

        Ok(Self::with_installation(find_qt_installation()?, qt_modules))
    }

    /// Create a [QtBuild] using the given [QtInstallation] and specify which
    /// Qt modules you are linking, ommitting the `Qt` prefix (`"Core"` rather than `"QtCore"`).
    pub fn with_installation(
        qt_installation: Box<dyn QtInstallation>,
        mut qt_modules: Vec<String>,
    ) -> Self {
        if qt_modules.is_empty() {
            qt_modules.push("Core".to_owned());
        }

        Self {
            qt_installation,
            qt_modules,
            autorcc_options: Vec::new(),
        }
    }

    /// Add custom arguments to be passed to the end of the rcc invocation when converting qrc files.
    pub fn autorcc_options(mut self, options: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Self {
        self.autorcc_options = options
            .into_iter()
            .map(|s| s.as_ref().to_os_string())
            .collect();
        self
    }

    /// Tell Cargo to link each Qt module.
    pub fn cargo_link_libraries(&self, builder: &mut cc::Build) {
        self.qt_installation.link_modules(builder, &self.qt_modules);
    }

    /// Get the frmaework paths for Qt. This is intended to be passed to whichever tool
    /// you are using to invoke the C++ compiler.
    pub fn framework_paths(&self) -> Vec<PathBuf> {
        self.qt_installation.framework_paths(&self.qt_modules)
    }

    /// Get the include paths for Qt, including Qt module subdirectories. This is intended
    /// to be passed to whichever tool you are using to invoke the C++ compiler.
    pub fn include_paths(&self) -> Vec<PathBuf> {
        self.qt_installation.include_paths(&self.qt_modules)
    }

    /// Get the inner [QtInstallation] implementation
    pub fn installation(&self) -> &dyn QtInstallation {
        self.qt_installation.as_ref()
    }

    /// Version of the detected Qt installation
    pub fn version(&self) -> Version {
        self.qt_installation.version()
    }

    /// Create a [QtToolMoc] for this [QtBuild]
    ///
    /// This allows for using [moc](https://doc.qt.io/qt-6/moc.html)
    pub fn moc(&mut self) -> QtToolMoc {
        QtToolMoc::new(self.qt_installation.as_ref(), &self.qt_modules)
    }

    /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
    ///
    /// This generates a [qmldir file](https://doc.qt.io/qt-6/qtqml-modules-qmldir.html) for the QML module.
    /// The `qml_files` and `qrc_files` are registered with the [Qt Resource System](https://doc.qt.io/qt-6/resources.html) in
    /// the [default QML import path](https://doc.qt.io/qt-6/qtqml-syntax-imports.html#qml-import-path) `qrc:/qt/qml/uri/of/module/`.
    ///
    /// 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.
    pub fn register_qml_module(
        &mut self,
        metatypes_json: &[impl AsRef<Path>],
        uri: &QmlUri,
        version_major: usize,
        version_minor: usize,
        plugin_name: &str,
        qml_files: &[QmlFile],
        depends: impl IntoIterator<Item = impl Into<QmlUri>>,
        plugin_type: PluginType,
    ) -> QmlModuleRegistrationFiles {
        let qml_uri_dirs = uri.as_dirs();
        let qml_uri_underscores = uri.as_underscores();
        let plugin_type_info = "plugin.qmltypes";
        let plugin_class_name = format!("{}_plugin", qml_uri_underscores);

        let out_dir = env::var("OUT_DIR").unwrap();
        let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils"));
        std::fs::create_dir_all(&qt_build_utils_dir).expect("Could not create qt_build_utils dir");

        let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs);
        std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");

        let qmltypes_path = qml_module_dir.join(plugin_type_info);

        // Generate qmldir file
        let qmldir_file_path = qml_module_dir.join("qmldir");
        {
            let qml_type_files = qml_files
                .iter()
                .filter(|file| {
                    // Qt by default only includes uppercase files in the qmldir file.
                    // Mirror this behavior.
                    file.get_path()
                        .file_name()
                        .and_then(OsStr::to_str)
                        .and_then(|file_name| file_name.chars().next())
                        .map(char::is_uppercase)
                        .unwrap_or_default()
                })
                .cloned();
            let mut file = File::create(&qmldir_file_path).expect("Could not create qmldir file");
            QmlDirBuilder::new(uri.clone())
                .depends(depends)
                .plugin(plugin_name, true)
                .class_name(&plugin_class_name)
                .type_info(plugin_type_info)
                .qml_files(qml_type_files)
                .write(&mut file)
                .expect("Could not write qmldir file");
        }

        // Generate .qrc file and run rcc on it
        // TODO: Replace with an equivalent of [qt_add_resources](https://doc.qt.io/qt-6/qt-add-resources.html)
        let qrc_path =
            qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc"));
        {
            let qml_module_dir_str = qml_module_dir.to_str().unwrap();
            let qml_uri_dirs_prefix = format!("/qt/qml/{qml_uri_dirs}");
            let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
            QResources::new()
                .resource({
                    let mut resource = QResource::new().prefix(qml_uri_dirs_prefix.clone()).file(
                        QResourceFile::new(format!("{qml_module_dir_str}/qmldir"))
                            .alias("qmldir".to_string()),
                    );

                    fn resource_add_path(resource: QResource, path: &Path) -> QResource {
                        let resolved = std::fs::canonicalize(path)
                            .unwrap_or_else(|_| {
                                panic!("Could not canonicalize path {}", path.display())
                            })
                            .display()
                            .to_string();
                        resource
                            .file(QResourceFile::new(resolved).alias(path.display().to_string()))
                    }

                    for file in qml_files {
                        resource = resource_add_path(resource, file.get_path());
                    }
                    resource
                })
                .write(&mut qrc)
                .expect("Could note write qrc file");
        }

        // Run qmlcachegen
        // qmlcachegen needs to be run once for each .qml file with --resource-path,
        // then once for the module with --resource-name.
        let mut qmlcachegen_file_paths = Vec::new();

        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
        if self.qt_installation.version().major >= 6 {
            let qml_cache_args = QmlCacheArguments {
                uri: uri.clone(),
                qmldir_path: qmldir_file_path.clone(),
                qmldir_qrc_path: qrc_path.clone(),
            };
            let mut qml_resource_paths = Vec::new();
            for file in qml_files {
                let result = QtToolQmlCacheGen::new(self.qt_installation.as_ref())
                    .compile(qml_cache_args.clone(), file.get_path());
                qmlcachegen_file_paths.push(result.qml_cache_path);
                qml_resource_paths.push(result.qml_resource_path);
            }

            // If there are no QML files there is nothing for qmlcachegen to run with
            if !qml_files.is_empty() {
                qmlcachegen_file_paths.push(
                    QtToolQmlCacheGen::new(self.qt_installation.as_ref())
                        .compile_loader(qml_cache_args.clone(), &qml_resource_paths),
                );
            }
        }

        let qml_plugin_dir = PathBuf::from(format!("{out_dir}/qt-build-utils/qml_plugin"));
        std::fs::create_dir_all(&qml_plugin_dir).expect("Could not create qml_plugin dir");

        // Run qmltyperegistrar over the meta types
        let qmltyperegistrar_path = self.qmltyperegistrar().compile(
            metatypes_json,
            &qmltypes_path,
            uri,
            Version::new(version_major as u64, version_minor as u64, 0),
        );

        // Generate QQmlEngineExtensionPlugin
        let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
        let include_path;
        {
            let mut file = File::create(&qml_plugin_cpp_path)
                .expect("Could not create plugin definition file");
            let plugin_init = QmlPluginCppBuilder::new(uri.clone(), plugin_class_name.clone())
                .qml_cache(!qml_files.is_empty() && !qmlcachegen_file_paths.is_empty())
                .plugin_type(plugin_type)
                .write(&mut file)
                .expect("Failed to write plugin definition");

            let moc_product = self.moc().compile(
                &qml_plugin_cpp_path,
                MocArguments::default().uri(uri.to_owned()),
            );
            // Pass the include directory of the moc file to the caller
            include_path = moc_product.cpp.parent().map(Path::to_path_buf);

            let qml_files = qml_files
                .iter()
                .map(|qml_file| qml_file.get_path().to_path_buf())
                .collect();

            let rcc = self.rcc().compile(&qrc_path);
            QmlModuleRegistrationFiles {
                // The rcc file is automatically initialized when importing the plugin.
                // so we don't need to treat it like an initializer here.
                rcc: rcc.file.unwrap(),
                qmlcachegen: qmlcachegen_file_paths,
                qmltyperegistrar: qmltyperegistrar_path,
                qmltypes: qmltypes_path,
                qmldir: qmldir_file_path,
                plugin: qml_plugin_cpp_path,
                plugin_init,
                include_path,
                qml_files,
            }
        }
    }

    /// Create a [QtToolRcc] for this [QtBuild]
    ///
    /// This allows for using [rcc](https://doc.qt.io/qt-6/resources.html)
    pub fn rcc(&self) -> QtToolRcc {
        QtToolRcc::new(self.qt_installation.as_ref()).custom_args(&self.autorcc_options)
    }

    /// Create a [QtToolQmlTypeRegistrar] for this [QtBuild]
    pub fn qmltyperegistrar(&self) -> QtToolQmlTypeRegistrar {
        QtToolQmlTypeRegistrar::new(self.qt_installation.as_ref())
    }

    /// Create a [QtToolQtPaths] for this [QtBuild]
    pub fn qtpaths(&self) -> QtToolQtPaths {
        QtToolQtPaths::new(self.qt_installation.as_ref())
    }
}