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#![allow(clippy::too_many_arguments)]
17
18mod parse_cflags;
19
20use std::{
21    env,
22    fs::File,
23    io::Write,
24    path::{Path, PathBuf},
25    process::Command,
26};
27
28pub use versions::SemVer;
29
30use thiserror::Error;
31
32#[derive(Error, Debug)]
33/// Errors that can occur while using [QtBuild]
34pub enum QtBuildError {
35    /// `QMAKE` environment variable was set but Qt was not detected
36    #[error("QMAKE environment variable specified as {qmake_env_var} but could not detect Qt: {error:?}")]
37    QMakeSetQtMissing {
38        /// The value of the qmake environment variable when the error occurred
39        qmake_env_var: String,
40        /// The inner [QtBuildError] that occurred
41        error: Box<QtBuildError>,
42    },
43    /// Qt was not found
44    #[error("Could not find Qt")]
45    QtMissing,
46    /// Executing `qmake -query` failed
47    #[error("Executing `qmake -query` failed: {0:?}")]
48    QmakeFailed(#[from] std::io::Error),
49    /// `QT_VERSION_MAJOR` environment variable was specified but could not be parsed as an integer
50    #[error("QT_VERSION_MAJOR environment variable specified as {qt_version_major_env_var} but could not parse as integer: {source:?}")]
51    QtVersionMajorInvalid {
52        /// The Qt major version from `QT_VERSION_MAJOR`
53        qt_version_major_env_var: String,
54        /// The [std::num::ParseIntError] when parsing the `QT_VERSION_MAJOR`
55        source: std::num::ParseIntError,
56    },
57    /// `QT_VERSION_MAJOR` environment variable was specified but the Qt version specified by `qmake -query QT_VERSION` did not match
58    #[error("qmake version ({qmake_version}) does not match version specified by QT_VERSION_MAJOR ({qt_version_major})")]
59    QtVersionMajorDoesNotMatch {
60        /// The qmake version
61        qmake_version: u32,
62        /// The Qt major version from `QT_VERSION_MAJOR`
63        qt_version_major: u32,
64    },
65}
66
67fn command_help_output(command: &str) -> std::io::Result<std::process::Output> {
68    Command::new(command).args(["--help"]).output()
69}
70
71/// Whether apple is the current target
72fn is_apple_target() -> bool {
73    env::var("TARGET")
74        .map(|target| target.contains("apple"))
75        .unwrap_or_else(|_| false)
76}
77
78/// Linking executables (including tests) with Cargo that link to Qt fails to link with GNU ld.bfd,
79/// which is the default on most Linux distributions, so use GNU ld.gold, lld, or mold instead.
80/// If you are using a C++ build system such as CMake to do the final link of the executable, you do
81/// not need to call this function.
82///
83/// With Apple devices we set -fapple-link-rtlib as we build with -nodefaultlibs
84/// otherwise we cannot user helpers from the compiler runtime in Qt
85///
86/// This does nothing on non-Unix platforms.
87pub fn setup_linker() {
88    if env::var("CARGO_CFG_UNIX").is_err() {
89        return;
90    }
91
92    if let Ok(vendor) = env::var("CARGO_CFG_TARGET_VENDOR") {
93        if vendor == "apple" {
94            // Tell clang link to clang_rt as we build with -nodefaultlibs
95            // otherwise we cannot use helpers from the compiler runtime in Qt
96            println!("cargo::rustc-link-arg=-fapple-link-rtlib");
97        }
98    }
99
100    let flags = env::var("CARGO_ENCODED_RUSTFLAGS").unwrap();
101    // Don't override custom flags
102    if !flags.contains("-fuse-ld") {
103        // ld is the system default linker. On Linux, this is usually GNU ld.bfd, but it may be symlinked to another
104        // linker. On macOS, Xcode ships lld with the executable named ld.
105        let ld_help = String::from_utf8(
106            command_help_output("ld")
107                .expect("Could not run ld command")
108                .stdout,
109        )
110        .unwrap();
111        // bfd supports some exotic targets that other linkers do not.
112        let ld_is_bfd = ld_help.contains("symbolsrec")
113            || ld_help.contains("verilog")
114            || ld_help.contains("tekhex");
115
116        // Whatever linker is being used that's not bfd will likely work.
117        if !ld_is_bfd {
118            return;
119        }
120
121        // mold is fastest, but specifing mold with -fuse-ld requires GCC >= 12 or Clang.
122        // Unfortunately cargo does not provide a means to set the linker driver via build scripts,
123        // so linking would fail trying to use -fuse-ld=mold with GCC < 12 even if clang is installed.
124        // So, prefer lld and gold to mold for robustness on the widest range of systems.
125        // mold can still be used by manually specifying it in ~/.cargo/config.toml or the RUSTFLAGS environment variable.
126        if command_help_output("lld").is_ok() {
127            println!("cargo::rustc-link-arg=-fuse-ld=lld");
128        } else if command_help_output("ld.gold").is_ok() {
129            println!("cargo::rustc-link-arg=-fuse-ld=gold");
130        } else if command_help_output("mold").is_ok() {
131            println!("cargo::rustc-link-arg=-fuse-ld=mold");
132        } else {
133            println!("cargo::warning=Neither mold, lld, nor gold linkers were found. Linking with GNU ld.bfd will likely fail.");
134        }
135    }
136}
137
138/// Paths to files generated by [QtBuild::moc]
139pub struct MocProducts {
140    /// Generated C++ file
141    pub cpp: PathBuf,
142    /// Generated JSON file
143    pub metatypes_json: PathBuf,
144}
145
146/// Arguments for a Qt moc invocation.
147/// See: [QtBuild::moc]
148#[derive(Default, Clone)]
149pub struct MocArguments {
150    uri: Option<String>,
151    include_paths: Vec<PathBuf>,
152}
153
154impl MocArguments {
155    /// Should be passed if the input_file is part of a QML module
156    pub fn uri(mut self, uri: String) -> Self {
157        self.uri = Some(uri);
158        self
159    }
160
161    /// Additional include path to pass to moc
162    pub fn include_path(mut self, include_path: PathBuf) -> Self {
163        self.include_paths.push(include_path);
164        self
165    }
166
167    /// Additional include paths to pass to moc.
168    pub fn include_paths(mut self, mut include_paths: Vec<PathBuf>) -> Self {
169        self.include_paths.append(&mut include_paths);
170        self
171    }
172}
173
174/// Paths to C++ files generated by [QtBuild::register_qml_module]
175pub struct QmlModuleRegistrationFiles {
176    /// File generated by [rcc](https://doc.qt.io/qt-6/rcc.html) for the QML plugin. The compiled static library
177    /// must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
178    /// or the linker will discard the generated static variables because they are not referenced from `main`.
179    pub rcc: PathBuf,
180    /// Files generated by [qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html). Must be linked with `+whole-archive`.
181    pub qmlcachegen: Vec<PathBuf>,
182    /// File generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
183    pub qmltyperegistrar: PathBuf,
184    /// File with generated [QQmlEngineExtensionPlugin](https://doc.qt.io/qt-6/qqmlengineextensionplugin.html) that calls the function generated by qmltyperegistrar.
185    pub plugin: PathBuf,
186    /// File that automatically registers the QQmlExtensionPlugin at startup. Must be linked with `+whole-archive`.
187    pub plugin_init: PathBuf,
188    /// An optional include path that should be included
189    pub include_path: Option<PathBuf>,
190}
191
192/// Helper for build.rs scripts using Qt
193/// ```
194/// let qt_modules = vec!["Core", "Gui"]
195///     .iter()
196///     .map(|m| String::from(*m))
197///     .collect();
198/// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
199/// ```
200pub struct QtBuild {
201    version: SemVer,
202    qmake_executable: String,
203    moc_executable: Option<String>,
204    qmltyperegistrar_executable: Option<String>,
205    qmlcachegen_executable: Option<String>,
206    rcc_executable: Option<String>,
207    qt_modules: Vec<String>,
208}
209
210impl QtBuild {
211    /// Search for where Qt is installed using qmake. Specify the Qt modules you are
212    /// linking with the `qt_modules` parameter, ommitting the `Qt` prefix (`"Core"`
213    /// rather than `"QtCore"`). After construction, use the [QtBuild::qmake_query]
214    /// method to get information about the Qt installation.
215    ///
216    /// The directories specified by the `PATH` environment variable are where qmake is
217    /// searched for. Alternatively, the `QMAKE` environment variable may be set to specify
218    /// an explicit path to qmake.
219    ///
220    /// If multiple major versions (for example, `5` and `6`) of Qt could be installed, set
221    /// the `QT_VERSION_MAJOR` environment variable to force which one to use. When using Cargo
222    /// as the build system for the whole build, prefer using `QT_VERSION_MAJOR` over the `QMAKE`
223    /// environment variable because it will account for different names for the qmake executable
224    /// that some Linux distributions use.
225    ///
226    /// However, when building a Rust staticlib that gets linked to C++ code by a C++ build
227    /// system, it is best to use the `QMAKE` environment variable to ensure that the Rust
228    /// staticlib is linked to the same installation of Qt that the C++ build system has
229    /// detected.
230    /// With CMake, this will automatically be set up for you when using cxxqt_import_crate.
231    ///
232    /// Alternatively, you can get this from the `Qt::qmake` target's `IMPORTED_LOCATION`
233    /// property, for example:
234    /// ```cmake
235    /// find_package(Qt6 COMPONENTS Core)
236    /// if(NOT Qt6_FOUND)
237    ///     find_package(Qt5 5.15 COMPONENTS Core REQUIRED)
238    /// endif()
239    /// get_target_property(QMAKE Qt::qmake IMPORTED_LOCATION)
240    ///
241    /// execute_process(
242    ///     COMMAND cmake -E env
243    ///         "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}/cargo"
244    ///         "QMAKE=${QMAKE}"
245    ///         cargo build
246    ///     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
247    /// )
248    /// ```
249    pub fn new(mut qt_modules: Vec<String>) -> Result<Self, QtBuildError> {
250        if qt_modules.is_empty() {
251            qt_modules.push("Core".to_string());
252        }
253        println!("cargo::rerun-if-env-changed=QMAKE");
254        println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
255        fn verify_candidate(candidate: &str) -> Result<(&str, versions::SemVer), QtBuildError> {
256            match Command::new(candidate)
257                .args(["-query", "QT_VERSION"])
258                .output()
259            {
260                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(QtBuildError::QtMissing),
261                Err(e) => Err(QtBuildError::QmakeFailed(e)),
262                Ok(output) => {
263                    if output.status.success() {
264                        let version_string = std::str::from_utf8(&output.stdout)
265                            .unwrap()
266                            .trim()
267                            .to_string();
268                        let qmake_version = versions::SemVer::new(version_string).unwrap();
269                        if let Ok(env_version) = env::var("QT_VERSION_MAJOR") {
270                            let env_version = match env_version.trim().parse::<u32>() {
271                                Err(e) if *e.kind() == std::num::IntErrorKind::Empty => {
272                                    println!(
273                                        "cargo::warning=QT_VERSION_MAJOR environment variable defined but empty"
274                                    );
275                                    return Ok((candidate, qmake_version));
276                                }
277                                Err(e) => {
278                                    return Err(QtBuildError::QtVersionMajorInvalid {
279                                        qt_version_major_env_var: env_version,
280                                        source: e,
281                                    })
282                                }
283                                Ok(int) => int,
284                            };
285                            if env_version == qmake_version.major {
286                                return Ok((candidate, qmake_version));
287                            } else {
288                                return Err(QtBuildError::QtVersionMajorDoesNotMatch {
289                                    qmake_version: qmake_version.major,
290                                    qt_version_major: env_version,
291                                });
292                            }
293                        }
294                        Ok((candidate, qmake_version))
295                    } else {
296                        Err(QtBuildError::QtMissing)
297                    }
298                }
299            }
300        }
301
302        if let Ok(qmake_env_var) = env::var("QMAKE") {
303            match verify_candidate(qmake_env_var.trim()) {
304                Ok((executable_name, version)) => {
305                    return Ok(Self {
306                        qmake_executable: executable_name.to_string(),
307                        moc_executable: None,
308                        qmltyperegistrar_executable: None,
309                        qmlcachegen_executable: None,
310                        rcc_executable: None,
311                        version,
312                        qt_modules,
313                    });
314                }
315                Err(e) => {
316                    return Err(QtBuildError::QMakeSetQtMissing {
317                        qmake_env_var,
318                        error: Box::new(e),
319                    })
320                }
321            }
322        }
323
324        // Fedora 36 renames Qt5's qmake to qmake-qt5
325        let candidate_executable_names = ["qmake6", "qmake-qt5", "qmake"];
326        for (index, executable_name) in candidate_executable_names.iter().enumerate() {
327            match verify_candidate(executable_name) {
328                Ok((executable_name, version)) => {
329                    return Ok(Self {
330                        qmake_executable: executable_name.to_string(),
331                        moc_executable: None,
332                        qmltyperegistrar_executable: None,
333                        qmlcachegen_executable: None,
334                        rcc_executable: None,
335                        version,
336                        qt_modules,
337                    });
338                }
339                // If QT_VERSION_MAJOR is specified, it is expected that one of the versioned
340                // executable names will not match, so the unversioned `qmake` needs to be
341                // attempted last and QtVersionMajorDoesNotMatch should only be returned if
342                // none of the candidate executable names match.
343                Err(QtBuildError::QtVersionMajorDoesNotMatch {
344                    qmake_version,
345                    qt_version_major,
346                }) => {
347                    if index == candidate_executable_names.len() - 1 {
348                        return Err(QtBuildError::QtVersionMajorDoesNotMatch {
349                            qmake_version,
350                            qt_version_major,
351                        });
352                    }
353                    eprintln!("Candidate qmake executable `{executable_name}` is for Qt{qmake_version} but QT_VERSION_MAJOR environment variable specified as {qt_version_major}. Trying next candidate executable name `{}`...", candidate_executable_names[index + 1]);
354                    continue;
355                }
356                Err(QtBuildError::QtMissing) => continue,
357                Err(e) => return Err(e),
358            }
359        }
360
361        Err(QtBuildError::QtMissing)
362    }
363
364    /// Get the output of running `qmake -query var_name`
365    pub fn qmake_query(&self, var_name: &str) -> String {
366        std::str::from_utf8(
367            &Command::new(&self.qmake_executable)
368                .args(["-query", var_name])
369                .output()
370                .unwrap()
371                .stdout,
372        )
373        .unwrap()
374        .trim()
375        .to_string()
376    }
377
378    fn cargo_link_qt_library(
379        &self,
380        name: &str,
381        prefix_path: &str,
382        lib_path: &str,
383        link_lib: &str,
384        prl_path: &str,
385        builder: &mut cc::Build,
386    ) {
387        println!("cargo::rustc-link-lib={link_lib}");
388
389        match std::fs::read_to_string(prl_path) {
390            Ok(prl) => {
391                for line in prl.lines() {
392                    if let Some(line) = line.strip_prefix("QMAKE_PRL_LIBS = ") {
393                        parse_cflags::parse_libs_cflags(
394                            name,
395                            line.replace(r"$$[QT_INSTALL_LIBS]", lib_path)
396                                .replace(r"$$[QT_INSTALL_PREFIX]", prefix_path)
397                                .as_bytes(),
398                            builder,
399                        );
400                    }
401                }
402            }
403            Err(e) => {
404                println!(
405                    "cargo::warning=Could not open {} file to read libraries to link: {}",
406                    &prl_path, e
407                );
408            }
409        }
410    }
411
412    /// Some prl files include their architecture in their naming scheme.
413    /// Just try all known architectures and fallback to non when they all failed.
414    fn find_qt_module_prl(
415        &self,
416        lib_path: &str,
417        prefix: &str,
418        version_major: u32,
419        qt_module: &str,
420    ) -> String {
421        for arch in ["", "_arm64-v8a", "_armeabi-v7a", "_x86", "_x86_64"] {
422            let prl_path = format!("{lib_path}/{prefix}Qt{version_major}{qt_module}{arch}.prl");
423            match Path::new(&prl_path).try_exists() {
424                Ok(exists) => {
425                    if exists {
426                        return prl_path;
427                    }
428                }
429                Err(e) => {
430                    println!("cargo::warning=failed checking for existence of {prl_path}: {e}");
431                }
432            }
433        }
434
435        format!("{lib_path}/{prefix}Qt{version_major}{qt_module}.prl")
436    }
437
438    /// Tell Cargo to link each Qt module.
439    pub fn cargo_link_libraries(&self, builder: &mut cc::Build) {
440        let prefix_path = self.qmake_query("QT_INSTALL_PREFIX");
441        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
442        println!("cargo::rustc-link-search={lib_path}");
443
444        let target = env::var("TARGET");
445
446        // Add the QT_INSTALL_LIBS as a framework link search path as well
447        //
448        // Note that leaving the kind empty should default to all,
449        // but this doesn't appear to find frameworks in all situations
450        // https://github.com/KDAB/cxx-qt/issues/885
451        //
452        // Note this doesn't have an adverse affect running all the time
453        // as it appears that all rustc-link-search are added
454        //
455        // Note that this adds the framework path which allows for
456        // includes such as <QtCore/QObject> to be resolved correctly
457        if is_apple_target() {
458            println!("cargo::rustc-link-search=framework={lib_path}");
459
460            // Ensure that any framework paths are set to -F
461            for framework_path in self.framework_paths() {
462                builder.flag_if_supported(format!("-F{}", framework_path.display()));
463                // Also set the -rpath otherwise frameworks can not be found at runtime
464                println!(
465                    "cargo::rustc-link-arg=-Wl,-rpath,{}",
466                    framework_path.display()
467                );
468            }
469        }
470
471        let prefix = match &target {
472            Ok(target) => {
473                if target.contains("windows") {
474                    ""
475                } else {
476                    "lib"
477                }
478            }
479            Err(_) => "lib",
480        };
481
482        for qt_module in &self.qt_modules {
483            let framework = if is_apple_target() {
484                Path::new(&format!("{lib_path}/Qt{qt_module}.framework")).exists()
485            } else {
486                false
487            };
488
489            let (link_lib, prl_path) = if framework {
490                (
491                    format!("framework=Qt{qt_module}"),
492                    format!("{lib_path}/Qt{qt_module}.framework/Resources/Qt{qt_module}.prl"),
493                )
494            } else {
495                (
496                    format!("Qt{}{qt_module}", self.version.major),
497                    self.find_qt_module_prl(&lib_path, prefix, self.version.major, qt_module),
498                )
499            };
500
501            self.cargo_link_qt_library(
502                &format!("Qt{}{qt_module}", self.version.major),
503                &prefix_path,
504                &lib_path,
505                &link_lib,
506                &prl_path,
507                builder,
508            );
509        }
510
511        let emscripten_targeted = match env::var("CARGO_CFG_TARGET_OS") {
512            Ok(val) => val == "emscripten",
513            Err(_) => false,
514        };
515        if emscripten_targeted {
516            let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
517            println!("cargo::rustc-link-search={platforms_path}");
518            self.cargo_link_qt_library(
519                "qwasm",
520                &prefix_path,
521                &lib_path,
522                "qwasm",
523                &format!("{platforms_path}/libqwasm.prl"),
524                builder,
525            );
526        }
527    }
528
529    /// Get the framework paths for Qt. This is intended
530    /// to be passed to whichever tool you are using to invoke the C++ compiler.
531    pub fn framework_paths(&self) -> Vec<PathBuf> {
532        let mut framework_paths = vec![];
533
534        if is_apple_target() {
535            // Note that this adds the framework path which allows for
536            // includes such as <QtCore/QObject> to be resolved correctly
537            let framework_path = self.qmake_query("QT_INSTALL_LIBS");
538            framework_paths.push(framework_path);
539        }
540
541        framework_paths
542            .iter()
543            .map(PathBuf::from)
544            // Only add paths if they exist
545            .filter(|path| path.exists())
546            .collect()
547    }
548
549    /// Get the include paths for Qt, including Qt module subdirectories. This is intended
550    /// to be passed to whichever tool you are using to invoke the C++ compiler.
551    pub fn include_paths(&self) -> Vec<PathBuf> {
552        let root_path = self.qmake_query("QT_INSTALL_HEADERS");
553        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
554        let mut paths = Vec::new();
555        for qt_module in &self.qt_modules {
556            // Add the usual location for the Qt module
557            paths.push(format!("{root_path}/Qt{qt_module}"));
558
559            // Ensure that we add any framework's headers path
560            let header_path = format!("{lib_path}/Qt{qt_module}.framework/Headers");
561            if is_apple_target() && Path::new(&header_path).exists() {
562                paths.push(header_path);
563            }
564        }
565
566        // Add the QT_INSTALL_HEADERS itself
567        paths.push(root_path);
568
569        paths
570            .iter()
571            .map(PathBuf::from)
572            // Only add paths if they exist
573            .filter(|path| path.exists())
574            .collect()
575    }
576
577    /// Version of the detected Qt installation
578    pub fn version(&self) -> &SemVer {
579        &self.version
580    }
581
582    /// Lazy load the path of a Qt executable tool
583    /// Skip doing this in the constructor because not every user of this crate will use each tool
584    fn get_qt_tool(&self, tool_name: &str) -> Result<String, ()> {
585        // "qmake -query" exposes a list of paths that describe where Qt executables and libraries
586        // are located, as well as where new executables & libraries should be installed to.
587        // We can use these variables to find any Qt tool.
588        //
589        // The order is important here.
590        // First, we check the _HOST_ variables.
591        // In cross-compilation contexts, these variables should point to the host toolchain used
592        // for building. The _INSTALL_ directories describe where to install new binaries to
593        // (i.e. the target directories).
594        // We still use the _INSTALL_ paths as fallback.
595        //
596        // The _LIBEXECS variables point to the executable Qt-internal tools (i.e. moc and
597        // friends), whilst _BINS point to the developer-facing executables (qdoc, qmake, etc.).
598        // As we mostly use the Qt-internal tools in this library, check _LIBEXECS first.
599        //
600        // Furthermore, in some contexts these variables include a `/get` variant.
601        // This is important for contexts where qmake and the Qt build tools do not have a static
602        // location, but are moved around during building.
603        // This notably happens with yocto builds.
604        // For each package, yocto builds a `sysroot` folder for both the host machine, as well
605        // as the target. This is done to keep package builds reproducable & separate.
606        // As a result the qmake executable is copied into each host sysroot for building.
607        //
608        // In this case the variables compiled into qmake still point to the paths relative
609        // from the host sysroot (e.g. /usr/bin).
610        // The /get variant in comparison will "get" the right full path from the current environment.
611        // Therefore prefer to use the `/get` variant when available.
612        // See: https://github.com/KDAB/cxx-qt/pull/430
613        //
614        // To check & debug all variables available on your system, simply run:
615        //
616        //              qmake -query
617        //
618        for qmake_query_var in [
619            "QT_HOST_LIBEXECS/get",
620            "QT_HOST_LIBEXECS",
621            "QT_HOST_BINS/get",
622            "QT_HOST_BINS",
623            "QT_INSTALL_LIBEXECS/get",
624            "QT_INSTALL_LIBEXECS",
625            "QT_INSTALL_BINS/get",
626            "QT_INSTALL_BINS",
627        ] {
628            let executable_path = format!("{}/{tool_name}", self.qmake_query(qmake_query_var));
629            match Command::new(&executable_path).args(["-help"]).output() {
630                Ok(_) => return Ok(executable_path),
631                Err(_) => continue,
632            }
633        }
634        Err(())
635    }
636
637    /// Run moc on a C++ header file and save the output into [cargo's OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html).
638    /// The return value contains the path to the generated C++ file, which can then be passed to [cc::Build::files](https://docs.rs/cc/latest/cc/struct.Build.html#method.file),
639    /// as well as the path to the generated metatypes.json file, which can be passed to [register_qml_module](Self::register_qml_module).
640    ///
641    pub fn moc(&mut self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
642        if self.moc_executable.is_none() {
643            self.moc_executable = Some(self.get_qt_tool("moc").expect("Could not find moc"));
644        }
645
646        let input_path = input_file.as_ref();
647
648        // Put all the moc files into one place, this can then be added to the include path
649        let moc_dir = PathBuf::from(format!(
650            "{}/qt-build-utils/moc",
651            env::var("OUT_DIR").unwrap()
652        ));
653        std::fs::create_dir_all(&moc_dir).expect("Could not create moc dir");
654        let output_path = moc_dir.join(format!(
655            "moc_{}.cpp",
656            input_path.file_name().unwrap().to_str().unwrap()
657        ));
658
659        let metatypes_json_path = PathBuf::from(&format!("{}.json", output_path.display()));
660
661        let mut include_args = vec![];
662        // Qt includes
663        for include_path in self
664            .include_paths()
665            .iter()
666            .chain(arguments.include_paths.iter())
667        {
668            include_args.push(format!("-I{}", include_path.display()));
669        }
670
671        let mut cmd = Command::new(self.moc_executable.as_ref().unwrap());
672
673        if let Some(uri) = arguments.uri {
674            cmd.arg(format!("-Muri={uri}"));
675        }
676
677        cmd.args(include_args);
678        cmd.arg(input_path.to_str().unwrap())
679            .arg("-o")
680            .arg(output_path.to_str().unwrap())
681            .arg("--output-json");
682        let cmd = cmd
683            .output()
684            .unwrap_or_else(|_| panic!("moc failed for {}", input_path.display()));
685
686        if !cmd.status.success() {
687            panic!(
688                "moc failed for {}:\n{}",
689                input_path.display(),
690                String::from_utf8_lossy(&cmd.stderr)
691            );
692        }
693
694        MocProducts {
695            cpp: output_path,
696            metatypes_json: metatypes_json_path,
697        }
698    }
699
700    /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
701    ///
702    /// This generates a [qmldir file](https://doc.qt.io/qt-6/qtqml-modules-qmldir.html) for the QML module.
703    /// The `qml_files` and `qrc_files` are registered with the [Qt Resource System](https://doc.qt.io/qt-6/resources.html) in
704    /// the [default QML import path](https://doc.qt.io/qt-6/qtqml-syntax-imports.html#qml-import-path) `qrc:/qt/qml/uri/of/module/`.
705    ///
706    /// 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.
707    pub fn register_qml_module(
708        &mut self,
709        metatypes_json: &[impl AsRef<Path>],
710        uri: &str,
711        version_major: usize,
712        version_minor: usize,
713        plugin_name: &str,
714        qml_files: &[impl AsRef<Path>],
715        qrc_files: &[impl AsRef<Path>],
716    ) -> QmlModuleRegistrationFiles {
717        if self.qmltyperegistrar_executable.is_none() {
718            self.qmltyperegistrar_executable = Some(
719                self.get_qt_tool("qmltyperegistrar")
720                    .expect("Could not find qmltyperegistrar"),
721            );
722        }
723        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
724        if self.qmlcachegen_executable.is_none() && self.version.major >= 6 {
725            if let Ok(qmlcachegen_executable) = self.get_qt_tool("qmlcachegen") {
726                self.qmlcachegen_executable = Some(qmlcachegen_executable);
727            }
728        }
729
730        let qml_uri_dirs = uri.replace('.', "/");
731
732        let out_dir = env::var("OUT_DIR").unwrap();
733        let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils"));
734        std::fs::create_dir_all(&qt_build_utils_dir).expect("Could not create qt_build_utils dir");
735
736        let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs);
737        std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");
738
739        let qml_uri_underscores = uri.replace('.', "_");
740        let qmltypes_path = qml_module_dir.join("plugin.qmltypes");
741        let plugin_class_name = format!("{qml_uri_underscores}_plugin");
742
743        // Generate qmldir file
744        let qmldir_file_path = qml_module_dir.join("qmldir");
745        {
746            let mut qmldir = File::create(&qmldir_file_path).expect("Could not create qmldir file");
747            write!(
748                qmldir,
749                "module {uri}
750optional plugin {plugin_name}
751classname {plugin_class_name}
752typeinfo plugin.qmltypes
753prefer :/qt/qml/{qml_uri_dirs}/
754"
755            )
756            .expect("Could not write qmldir file");
757        }
758
759        // Generate .qrc file and run rcc on it
760        let qrc_path =
761            qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc"));
762        {
763            fn qrc_file_line(file_path: &impl AsRef<Path>) -> String {
764                let path_display = file_path.as_ref().display();
765                format!(
766                    "    <file alias=\"{}\">{}</file>\n",
767                    path_display,
768                    std::fs::canonicalize(file_path)
769                        .unwrap_or_else(|_| panic!("Could not canonicalize path {path_display}"))
770                        .display()
771                )
772            }
773
774            let mut qml_files_qrc = String::new();
775            for file_path in qml_files {
776                qml_files_qrc.push_str(&qrc_file_line(file_path));
777            }
778            for file_path in qrc_files {
779                qml_files_qrc.push_str(&qrc_file_line(file_path));
780            }
781
782            let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
783            let qml_module_dir_str = qml_module_dir.to_str().unwrap();
784            write!(
785                qrc,
786                r#"<RCC>
787<qresource prefix="/">
788    <file alias="/qt/qml/{qml_uri_dirs}">{qml_module_dir_str}</file>
789</qresource>
790<qresource prefix="/qt/qml/{qml_uri_dirs}">
791{qml_files_qrc}
792    <file alias="qmldir">{qml_module_dir_str}/qmldir</file>
793</qresource>
794</RCC>
795"#
796            )
797            .expect("Could note write qrc file");
798        }
799
800        // Run qmlcachegen
801        // qmlcachegen needs to be run once for each .qml file with --resource-path,
802        // then once for the module with --resource-name.
803        let mut qmlcachegen_file_paths = Vec::new();
804        if let Some(qmlcachegen_executable) = &self.qmlcachegen_executable {
805            let qmlcachegen_dir = qt_build_utils_dir.join("qmlcachegen").join(&qml_uri_dirs);
806            std::fs::create_dir_all(&qmlcachegen_dir)
807                .expect("Could not create qmlcachegen directory for QML module");
808
809            let common_args = [
810                "-i".to_string(),
811                qmldir_file_path.to_string_lossy().to_string(),
812                "--resource".to_string(),
813                qrc_path.to_string_lossy().to_string(),
814            ];
815
816            let mut qml_file_qrc_paths = Vec::new();
817            for file in qml_files {
818                let qrc_resource_path =
819                    format!("/qt/qml/{qml_uri_dirs}/{}", file.as_ref().display());
820
821                let qml_compiled_file = qmlcachegen_dir.join(format!(
822                    "{}.cpp",
823                    file.as_ref().file_name().unwrap().to_string_lossy()
824                ));
825                qmlcachegen_file_paths.push(PathBuf::from(&qml_compiled_file));
826
827                let specific_args = vec![
828                    "--resource-path".to_string(),
829                    qrc_resource_path.clone(),
830                    "-o".to_string(),
831                    qml_compiled_file.to_string_lossy().to_string(),
832                    std::fs::canonicalize(file)
833                        .unwrap()
834                        .to_string_lossy()
835                        .to_string(),
836                ];
837
838                let cmd = Command::new(qmlcachegen_executable)
839                    .args(common_args.iter().chain(&specific_args))
840                    .output()
841                    .unwrap_or_else(|_| {
842                        panic!(
843                            "qmlcachegen failed for {} in QML module {uri}",
844                            file.as_ref().display()
845                        )
846                    });
847                if !cmd.status.success() {
848                    panic!(
849                        "qmlcachegen failed for {} in QML module {uri}:\n{}",
850                        file.as_ref().display(),
851                        String::from_utf8_lossy(&cmd.stderr)
852                    );
853                }
854                qml_file_qrc_paths.push(qrc_resource_path);
855            }
856
857            let qmlcachegen_loader = qmlcachegen_dir.join("qmlcache_loader.cpp");
858            let specific_args = vec![
859                "--resource-name".to_string(),
860                format!("qmlcache_{qml_uri_underscores}"),
861                "-o".to_string(),
862                qmlcachegen_loader.to_string_lossy().to_string(),
863            ];
864
865            // If there are no QML files there is nothing for qmlcachegen to run with
866            if !qml_files.is_empty() {
867                let cmd = Command::new(qmlcachegen_executable)
868                    .args(
869                        common_args
870                            .iter()
871                            .chain(&specific_args)
872                            .chain(&qml_file_qrc_paths),
873                    )
874                    .output()
875                    .unwrap_or_else(|_| panic!("qmlcachegen failed for QML module {uri}"));
876                if !cmd.status.success() {
877                    panic!(
878                        "qmlcachegen failed for QML module {uri}:\n{}",
879                        String::from_utf8_lossy(&cmd.stderr)
880                    );
881                }
882                qmlcachegen_file_paths.push(PathBuf::from(&qmlcachegen_loader));
883            }
884        }
885
886        let qml_plugin_dir = PathBuf::from(format!("{out_dir}/qt-build-utils/qml_plugin"));
887        std::fs::create_dir_all(&qml_plugin_dir).expect("Could not create qml_plugin dir");
888
889        // Run qmltyperegistrar
890        let qmltyperegistrar_output_path =
891            qml_plugin_dir.join(format!("{qml_uri_underscores}_qmltyperegistration.cpp"));
892
893        // Filter out empty jsons
894        let metatypes_json: Vec<_> = metatypes_json
895            .iter()
896            .filter(|f| {
897                std::fs::metadata(f)
898                    .unwrap_or_else(|_| {
899                        panic!("couldn't open json file {}", f.as_ref().to_string_lossy())
900                    })
901                    .len()
902                    > 0
903            })
904            .map(|f| f.as_ref().to_string_lossy().to_string())
905            .collect();
906
907        // Only run qmltyperegistrar if we have valid json files left out
908        if !metatypes_json.is_empty() {
909            let mut args = vec![
910                "--generate-qmltypes".to_string(),
911                qmltypes_path.to_string_lossy().to_string(),
912                "--major-version".to_string(),
913                version_major.to_string(),
914                "--minor-version".to_string(),
915                version_minor.to_string(),
916                "--import-name".to_string(),
917                uri.to_string(),
918                "-o".to_string(),
919                qmltyperegistrar_output_path.to_string_lossy().to_string(),
920            ];
921            args.extend(metatypes_json);
922            let cmd = Command::new(self.qmltyperegistrar_executable.as_ref().unwrap())
923                .args(args)
924                .output()
925                .unwrap_or_else(|_| panic!("qmltyperegistrar failed for {uri}"));
926            if !cmd.status.success() {
927                panic!(
928                    "qmltyperegistrar failed for {uri}:\n{}",
929                    String::from_utf8_lossy(&cmd.stderr)
930                );
931            }
932        }
933
934        // Generate QQmlEngineExtensionPlugin
935        let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
936        let qml_plugin_init_path = qml_plugin_dir.join(format!("{plugin_class_name}_init.cpp"));
937        let include_path;
938        {
939            let mut declarations = Vec::default();
940            let mut usages = Vec::default();
941
942            let mut generate_usage = |return_type: &str, function_name: &str| {
943                declarations.push(format!("extern {return_type} {function_name}();"));
944                usages.push(format!("volatile auto {function_name}_usage = &{function_name};\nQ_UNUSED({function_name}_usage);"));
945            };
946
947            // This function is generated by qmltyperegistrar
948            generate_usage("void", &format!("qml_register_types_{qml_uri_underscores}"));
949            generate_usage(
950                "int",
951                &format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"),
952            );
953
954            if !qml_files.is_empty() && self.qmlcachegen_executable.is_some() {
955                generate_usage(
956                    "int",
957                    &format!("qInitResources_qmlcache_{qml_uri_underscores}"),
958                );
959            }
960            let declarations = declarations.join("\n");
961            let usages = usages.join("\n");
962
963            std::fs::write(
964                &qml_plugin_cpp_path,
965                format!(
966                    r#"
967#include <QtQml/qqmlextensionplugin.h>
968
969// TODO: Add missing handling for GHS (Green Hills Software compiler) that is in
970// https://code.qt.io/cgit/qt/qtbase.git/plain/src/corelib/global/qtsymbolmacros.h
971{declarations}
972
973class {plugin_class_name} : public QQmlEngineExtensionPlugin
974{{
975    Q_OBJECT
976    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlEngineExtensionInterface")
977
978public:
979    {plugin_class_name}(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent)
980    {{
981        {usages}
982    }}
983}};
984
985// The moc-generated cpp file doesn't compile on its own; it needs to be #included here.
986#include "moc_{plugin_class_name}.cpp.cpp"
987"#,
988                ),
989            )
990            .expect("Failed to write plugin definition");
991
992            let moc_product = self.moc(
993                &qml_plugin_cpp_path,
994                MocArguments::default().uri(uri.to_owned()),
995            );
996            // Pass the include directory of the moc file to the caller
997            include_path = moc_product.cpp.parent().map(|path| path.to_path_buf());
998
999            // Generate file to load static QQmlExtensionPlugin
1000            std::fs::write(
1001                &qml_plugin_init_path,
1002                format!(
1003                    r#"
1004#include <QtPlugin>
1005Q_IMPORT_PLUGIN({plugin_class_name});
1006"#
1007                ),
1008            )
1009            .expect("Failed to write plugin initializer file");
1010        }
1011
1012        QmlModuleRegistrationFiles {
1013            rcc: self.qrc(&qrc_path),
1014            qmlcachegen: qmlcachegen_file_paths,
1015            qmltyperegistrar: qmltyperegistrar_output_path,
1016            plugin: qml_plugin_cpp_path,
1017            plugin_init: qml_plugin_init_path,
1018            include_path,
1019        }
1020    }
1021
1022    /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and save the output into [cargo's OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html).
1023    /// The path to the generated C++ file is returned, which can then be passed to [cc::Build::files](https://docs.rs/cc/latest/cc/struct.Build.html#method.file).
1024    /// The compiled static library must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
1025    /// or the linker will discard the generated static variables because they are not referenced from `main`.
1026    pub fn qrc(&mut self, input_file: &impl AsRef<Path>) -> PathBuf {
1027        if self.rcc_executable.is_none() {
1028            self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
1029        }
1030
1031        let input_path = input_file.as_ref();
1032        let output_folder = PathBuf::from(&format!(
1033            "{}/qt-build-utils/qrc",
1034            env::var("OUT_DIR").unwrap()
1035        ));
1036        std::fs::create_dir_all(&output_folder).expect("Could not create qrc dir");
1037        let output_path = output_folder.join(format!(
1038            "{}.cpp",
1039            input_path.file_name().unwrap().to_string_lossy(),
1040        ));
1041
1042        let cmd = Command::new(self.rcc_executable.as_ref().unwrap())
1043            .args([
1044                input_path.to_str().unwrap(),
1045                "-o",
1046                output_path.to_str().unwrap(),
1047                "--name",
1048                input_path.file_name().unwrap().to_str().unwrap(),
1049            ])
1050            .output()
1051            .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));
1052
1053        if !cmd.status.success() {
1054            panic!(
1055                "rcc failed for {}:\n{}",
1056                input_path.display(),
1057                String::from_utf8_lossy(&cmd.stderr)
1058            );
1059        }
1060
1061        output_path
1062    }
1063
1064    /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources
1065    pub fn qrc_list(&mut self, input_file: &impl AsRef<Path>) -> Vec<PathBuf> {
1066        if self.rcc_executable.is_none() {
1067            self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
1068        }
1069
1070        // Add the qrc file contents to the cargo rerun list
1071        let input_path = input_file.as_ref();
1072        let cmd_list = Command::new(self.rcc_executable.as_ref().unwrap())
1073            .args(["--list", input_path.to_str().unwrap()])
1074            .output()
1075            .unwrap_or_else(|_| panic!("rcc --list failed for {}", input_path.display()));
1076
1077        if !cmd_list.status.success() {
1078            panic!(
1079                "rcc --list failed for {}:\n{}",
1080                input_path.display(),
1081                String::from_utf8_lossy(&cmd_list.stderr)
1082            );
1083        }
1084
1085        String::from_utf8_lossy(&cmd_list.stdout)
1086            .split('\n')
1087            .map(PathBuf::from)
1088            .collect()
1089    }
1090}