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!(
423                "{}/{}Qt{}{}{}.prl",
424                lib_path, prefix, version_major, qt_module, arch
425            );
426            match Path::new(&prl_path).try_exists() {
427                Ok(exists) => {
428                    if exists {
429                        return prl_path;
430                    }
431                }
432                Err(e) => {
433                    println!(
434                        "cargo::warning=failed checking for existence of {}: {}",
435                        prl_path, e
436                    );
437                }
438            }
439        }
440
441        format!(
442            "{}/{}Qt{}{}.prl",
443            lib_path, prefix, version_major, qt_module
444        )
445    }
446
447    /// Tell Cargo to link each Qt module.
448    pub fn cargo_link_libraries(&self, builder: &mut cc::Build) {
449        let prefix_path = self.qmake_query("QT_INSTALL_PREFIX");
450        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
451        println!("cargo::rustc-link-search={lib_path}");
452
453        let target = env::var("TARGET");
454
455        // Add the QT_INSTALL_LIBS as a framework link search path as well
456        //
457        // Note that leaving the kind empty should default to all,
458        // but this doesn't appear to find frameworks in all situations
459        // https://github.com/KDAB/cxx-qt/issues/885
460        //
461        // Note this doesn't have an adverse affect running all the time
462        // as it appears that all rustc-link-search are added
463        //
464        // Note that this adds the framework path which allows for
465        // includes such as <QtCore/QObject> to be resolved correctly
466        if is_apple_target() {
467            println!("cargo::rustc-link-search=framework={lib_path}");
468
469            // Ensure that any framework paths are set to -F
470            for framework_path in self.framework_paths() {
471                builder.flag_if_supported(format!("-F{}", framework_path.display()));
472                // Also set the -rpath otherwise frameworks can not be found at runtime
473                println!(
474                    "cargo::rustc-link-arg=-Wl,-rpath,{}",
475                    framework_path.display()
476                );
477            }
478        }
479
480        let prefix = match &target {
481            Ok(target) => {
482                if target.contains("windows") {
483                    ""
484                } else {
485                    "lib"
486                }
487            }
488            Err(_) => "lib",
489        };
490
491        for qt_module in &self.qt_modules {
492            let framework = if is_apple_target() {
493                Path::new(&format!("{lib_path}/Qt{qt_module}.framework")).exists()
494            } else {
495                false
496            };
497
498            let (link_lib, prl_path) = if framework {
499                (
500                    format!("framework=Qt{qt_module}"),
501                    format!("{lib_path}/Qt{qt_module}.framework/Resources/Qt{qt_module}.prl"),
502                )
503            } else {
504                (
505                    format!("Qt{}{qt_module}", self.version.major),
506                    self.find_qt_module_prl(&lib_path, prefix, self.version.major, qt_module),
507                )
508            };
509
510            self.cargo_link_qt_library(
511                &format!("Qt{}{qt_module}", self.version.major),
512                &prefix_path,
513                &lib_path,
514                &link_lib,
515                &prl_path,
516                builder,
517            );
518        }
519
520        let emscripten_targeted = match env::var("CARGO_CFG_TARGET_OS") {
521            Ok(val) => val == "emscripten",
522            Err(_) => false,
523        };
524        if emscripten_targeted {
525            let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
526            println!("cargo::rustc-link-search={platforms_path}");
527            self.cargo_link_qt_library(
528                "qwasm",
529                &prefix_path,
530                &lib_path,
531                "qwasm",
532                &format!("{platforms_path}/libqwasm.prl"),
533                builder,
534            );
535        }
536    }
537
538    /// Get the framework paths for Qt. This is intended
539    /// to be passed to whichever tool you are using to invoke the C++ compiler.
540    pub fn framework_paths(&self) -> Vec<PathBuf> {
541        let mut framework_paths = vec![];
542
543        if is_apple_target() {
544            // Note that this adds the framework path which allows for
545            // includes such as <QtCore/QObject> to be resolved correctly
546            let framework_path = self.qmake_query("QT_INSTALL_LIBS");
547            framework_paths.push(framework_path);
548        }
549
550        framework_paths
551            .iter()
552            .map(PathBuf::from)
553            // Only add paths if they exist
554            .filter(|path| path.exists())
555            .collect()
556    }
557
558    /// Get the include paths for Qt, including Qt module subdirectories. This is intended
559    /// to be passed to whichever tool you are using to invoke the C++ compiler.
560    pub fn include_paths(&self) -> Vec<PathBuf> {
561        let root_path = self.qmake_query("QT_INSTALL_HEADERS");
562        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
563        let mut paths = Vec::new();
564        for qt_module in &self.qt_modules {
565            // Add the usual location for the Qt module
566            paths.push(format!("{root_path}/Qt{qt_module}"));
567
568            // Ensure that we add any framework's headers path
569            let header_path = format!("{lib_path}/Qt{qt_module}.framework/Headers");
570            if is_apple_target() && Path::new(&header_path).exists() {
571                paths.push(header_path);
572            }
573        }
574
575        // Add the QT_INSTALL_HEADERS itself
576        paths.push(root_path);
577
578        paths
579            .iter()
580            .map(PathBuf::from)
581            // Only add paths if they exist
582            .filter(|path| path.exists())
583            .collect()
584    }
585
586    /// Version of the detected Qt installation
587    pub fn version(&self) -> &SemVer {
588        &self.version
589    }
590
591    /// Lazy load the path of a Qt executable tool
592    /// Skip doing this in the constructor because not every user of this crate will use each tool
593    fn get_qt_tool(&self, tool_name: &str) -> Result<String, ()> {
594        // "qmake -query" exposes a list of paths that describe where Qt executables and libraries
595        // are located, as well as where new executables & libraries should be installed to.
596        // We can use these variables to find any Qt tool.
597        //
598        // The order is important here.
599        // First, we check the _HOST_ variables.
600        // In cross-compilation contexts, these variables should point to the host toolchain used
601        // for building. The _INSTALL_ directories describe where to install new binaries to
602        // (i.e. the target directories).
603        // We still use the _INSTALL_ paths as fallback.
604        //
605        // The _LIBEXECS variables point to the executable Qt-internal tools (i.e. moc and
606        // friends), whilst _BINS point to the developer-facing executables (qdoc, qmake, etc.).
607        // As we mostly use the Qt-internal tools in this library, check _LIBEXECS first.
608        //
609        // Furthermore, in some contexts these variables include a `/get` variant.
610        // This is important for contexts where qmake and the Qt build tools do not have a static
611        // location, but are moved around during building.
612        // This notably happens with yocto builds.
613        // For each package, yocto builds a `sysroot` folder for both the host machine, as well
614        // as the target. This is done to keep package builds reproducable & separate.
615        // As a result the qmake executable is copied into each host sysroot for building.
616        //
617        // In this case the variables compiled into qmake still point to the paths relative
618        // from the host sysroot (e.g. /usr/bin).
619        // The /get variant in comparison will "get" the right full path from the current environment.
620        // Therefore prefer to use the `/get` variant when available.
621        // See: https://github.com/KDAB/cxx-qt/pull/430
622        //
623        // To check & debug all variables available on your system, simply run:
624        //
625        //              qmake -query
626        //
627        for qmake_query_var in [
628            "QT_HOST_LIBEXECS/get",
629            "QT_HOST_LIBEXECS",
630            "QT_HOST_BINS/get",
631            "QT_HOST_BINS",
632            "QT_INSTALL_LIBEXECS/get",
633            "QT_INSTALL_LIBEXECS",
634            "QT_INSTALL_BINS/get",
635            "QT_INSTALL_BINS",
636        ] {
637            let executable_path = format!("{}/{tool_name}", self.qmake_query(qmake_query_var));
638            match Command::new(&executable_path).args(["-help"]).output() {
639                Ok(_) => return Ok(executable_path),
640                Err(_) => continue,
641            }
642        }
643        Err(())
644    }
645
646    /// 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).
647    /// 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),
648    /// as well as the path to the generated metatypes.json file, which can be passed to [register_qml_module](Self::register_qml_module).
649    ///
650    pub fn moc(&mut self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
651        if self.moc_executable.is_none() {
652            self.moc_executable = Some(self.get_qt_tool("moc").expect("Could not find moc"));
653        }
654
655        let input_path = input_file.as_ref();
656
657        // Put all the moc files into one place, this can then be added to the include path
658        let moc_dir = PathBuf::from(format!(
659            "{}/qt-build-utils/moc",
660            env::var("OUT_DIR").unwrap()
661        ));
662        std::fs::create_dir_all(&moc_dir).expect("Could not create moc dir");
663        let output_path = moc_dir.join(format!(
664            "moc_{}.cpp",
665            input_path.file_name().unwrap().to_str().unwrap()
666        ));
667
668        let metatypes_json_path = PathBuf::from(&format!("{}.json", output_path.display()));
669
670        let mut include_args = vec![];
671        // Qt includes
672        for include_path in self
673            .include_paths()
674            .iter()
675            .chain(arguments.include_paths.iter())
676        {
677            include_args.push(format!("-I{}", include_path.display()));
678        }
679
680        let mut cmd = Command::new(self.moc_executable.as_ref().unwrap());
681
682        if let Some(uri) = arguments.uri {
683            cmd.arg(format!("-Muri={uri}"));
684        }
685
686        cmd.args(include_args);
687        cmd.arg(input_path.to_str().unwrap())
688            .arg("-o")
689            .arg(output_path.to_str().unwrap())
690            .arg("--output-json");
691        let cmd = cmd
692            .output()
693            .unwrap_or_else(|_| panic!("moc failed for {}", input_path.display()));
694
695        if !cmd.status.success() {
696            panic!(
697                "moc failed for {}:\n{}",
698                input_path.display(),
699                String::from_utf8_lossy(&cmd.stderr)
700            );
701        }
702
703        MocProducts {
704            cpp: output_path,
705            metatypes_json: metatypes_json_path,
706        }
707    }
708
709    /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
710    ///
711    /// This generates a [qmldir file](https://doc.qt.io/qt-6/qtqml-modules-qmldir.html) for the QML module.
712    /// The `qml_files` and `qrc_files` are registered with the [Qt Resource System](https://doc.qt.io/qt-6/resources.html) in
713    /// the [default QML import path](https://doc.qt.io/qt-6/qtqml-syntax-imports.html#qml-import-path) `qrc:/qt/qml/uri/of/module/`.
714    ///
715    /// 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.
716    pub fn register_qml_module(
717        &mut self,
718        metatypes_json: &[impl AsRef<Path>],
719        uri: &str,
720        version_major: usize,
721        version_minor: usize,
722        plugin_name: &str,
723        qml_files: &[impl AsRef<Path>],
724        qrc_files: &[impl AsRef<Path>],
725    ) -> QmlModuleRegistrationFiles {
726        if self.qmltyperegistrar_executable.is_none() {
727            self.qmltyperegistrar_executable = Some(
728                self.get_qt_tool("qmltyperegistrar")
729                    .expect("Could not find qmltyperegistrar"),
730            );
731        }
732        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
733        if self.qmlcachegen_executable.is_none() && self.version.major >= 6 {
734            if let Ok(qmlcachegen_executable) = self.get_qt_tool("qmlcachegen") {
735                self.qmlcachegen_executable = Some(qmlcachegen_executable);
736            }
737        }
738
739        let qml_uri_dirs = uri.replace('.', "/");
740
741        let out_dir = env::var("OUT_DIR").unwrap();
742        let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils"));
743        std::fs::create_dir_all(&qt_build_utils_dir).expect("Could not create qt_build_utils dir");
744
745        let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs);
746        std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");
747
748        let qml_uri_underscores = uri.replace('.', "_");
749        let qmltypes_path = qml_module_dir.join("plugin.qmltypes");
750        let plugin_class_name = format!("{qml_uri_underscores}_plugin");
751
752        // Generate qmldir file
753        let qmldir_file_path = qml_module_dir.join("qmldir");
754        {
755            let mut qmldir = File::create(&qmldir_file_path).expect("Could not create qmldir file");
756            write!(
757                qmldir,
758                "module {uri}
759optional plugin {plugin_name}
760classname {plugin_class_name}
761typeinfo plugin.qmltypes
762prefer :/qt/qml/{qml_uri_dirs}/
763"
764            )
765            .expect("Could not write qmldir file");
766        }
767
768        // Generate .qrc file and run rcc on it
769        let qrc_path =
770            qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc"));
771        {
772            fn qrc_file_line(file_path: &impl AsRef<Path>) -> String {
773                let path_display = file_path.as_ref().display();
774                format!(
775                    "    <file alias=\"{}\">{}</file>\n",
776                    path_display,
777                    std::fs::canonicalize(file_path)
778                        .unwrap_or_else(|_| panic!("Could not canonicalize path {}", path_display))
779                        .display()
780                )
781            }
782
783            let mut qml_files_qrc = String::new();
784            for file_path in qml_files {
785                qml_files_qrc.push_str(&qrc_file_line(file_path));
786            }
787            for file_path in qrc_files {
788                qml_files_qrc.push_str(&qrc_file_line(file_path));
789            }
790
791            let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
792            let qml_module_dir_str = qml_module_dir.to_str().unwrap();
793            write!(
794                qrc,
795                r#"<RCC>
796<qresource prefix="/">
797    <file alias="/qt/qml/{qml_uri_dirs}">{qml_module_dir_str}</file>
798</qresource>
799<qresource prefix="/qt/qml/{qml_uri_dirs}">
800{qml_files_qrc}
801    <file alias="qmldir">{qml_module_dir_str}/qmldir</file>
802</qresource>
803</RCC>
804"#
805            )
806            .expect("Could note write qrc file");
807        }
808
809        // Run qmlcachegen
810        // qmlcachegen needs to be run once for each .qml file with --resource-path,
811        // then once for the module with --resource-name.
812        let mut qmlcachegen_file_paths = Vec::new();
813        if let Some(qmlcachegen_executable) = &self.qmlcachegen_executable {
814            let qmlcachegen_dir = qt_build_utils_dir.join("qmlcachegen").join(&qml_uri_dirs);
815            std::fs::create_dir_all(&qmlcachegen_dir)
816                .expect("Could not create qmlcachegen directory for QML module");
817
818            let common_args = [
819                "-i".to_string(),
820                qmldir_file_path.to_string_lossy().to_string(),
821                "--resource".to_string(),
822                qrc_path.to_string_lossy().to_string(),
823            ];
824
825            let mut qml_file_qrc_paths = Vec::new();
826            for file in qml_files {
827                let qrc_resource_path =
828                    format!("/qt/qml/{qml_uri_dirs}/{}", file.as_ref().display());
829
830                let qml_compiled_file = qmlcachegen_dir.join(format!(
831                    "{}.cpp",
832                    file.as_ref().file_name().unwrap().to_string_lossy()
833                ));
834                qmlcachegen_file_paths.push(PathBuf::from(&qml_compiled_file));
835
836                let specific_args = vec![
837                    "--resource-path".to_string(),
838                    qrc_resource_path.clone(),
839                    "-o".to_string(),
840                    qml_compiled_file.to_string_lossy().to_string(),
841                    std::fs::canonicalize(file)
842                        .unwrap()
843                        .to_string_lossy()
844                        .to_string(),
845                ];
846
847                let cmd = Command::new(qmlcachegen_executable)
848                    .args(common_args.iter().chain(&specific_args))
849                    .output()
850                    .unwrap_or_else(|_| {
851                        panic!(
852                            "qmlcachegen failed for {} in QML module {uri}",
853                            file.as_ref().display()
854                        )
855                    });
856                if !cmd.status.success() {
857                    panic!(
858                        "qmlcachegen failed for {} in QML module {uri}:\n{}",
859                        file.as_ref().display(),
860                        String::from_utf8_lossy(&cmd.stderr)
861                    );
862                }
863                qml_file_qrc_paths.push(qrc_resource_path);
864            }
865
866            let qmlcachegen_loader = qmlcachegen_dir.join("qmlcache_loader.cpp");
867            let specific_args = vec![
868                "--resource-name".to_string(),
869                format!("qmlcache_{qml_uri_underscores}"),
870                "-o".to_string(),
871                qmlcachegen_loader.to_string_lossy().to_string(),
872            ];
873
874            // If there are no QML files there is nothing for qmlcachegen to run with
875            if !qml_files.is_empty() {
876                let cmd = Command::new(qmlcachegen_executable)
877                    .args(
878                        common_args
879                            .iter()
880                            .chain(&specific_args)
881                            .chain(&qml_file_qrc_paths),
882                    )
883                    .output()
884                    .unwrap_or_else(|_| panic!("qmlcachegen failed for QML module {uri}"));
885                if !cmd.status.success() {
886                    panic!(
887                        "qmlcachegen failed for QML module {uri}:\n{}",
888                        String::from_utf8_lossy(&cmd.stderr)
889                    );
890                }
891                qmlcachegen_file_paths.push(PathBuf::from(&qmlcachegen_loader));
892            }
893        }
894
895        let qml_plugin_dir = PathBuf::from(format!("{out_dir}/qt-build-utils/qml_plugin"));
896        std::fs::create_dir_all(&qml_plugin_dir).expect("Could not create qml_plugin dir");
897
898        // Run qmltyperegistrar
899        let qmltyperegistrar_output_path =
900            qml_plugin_dir.join(format!("{qml_uri_underscores}_qmltyperegistration.cpp"));
901
902        // Filter out empty jsons
903        let metatypes_json: Vec<_> = metatypes_json
904            .iter()
905            .filter(|f| {
906                std::fs::metadata(f)
907                    .unwrap_or_else(|_| {
908                        panic!("couldn't open json file {}", f.as_ref().to_string_lossy())
909                    })
910                    .len()
911                    > 0
912            })
913            .map(|f| f.as_ref().to_string_lossy().to_string())
914            .collect();
915
916        // Only run qmltyperegistrar if we have valid json files left out
917        if !metatypes_json.is_empty() {
918            let mut args = vec![
919                "--generate-qmltypes".to_string(),
920                qmltypes_path.to_string_lossy().to_string(),
921                "--major-version".to_string(),
922                version_major.to_string(),
923                "--minor-version".to_string(),
924                version_minor.to_string(),
925                "--import-name".to_string(),
926                uri.to_string(),
927                "-o".to_string(),
928                qmltyperegistrar_output_path.to_string_lossy().to_string(),
929            ];
930            args.extend(metatypes_json);
931            let cmd = Command::new(self.qmltyperegistrar_executable.as_ref().unwrap())
932                .args(args)
933                .output()
934                .unwrap_or_else(|_| panic!("qmltyperegistrar failed for {uri}"));
935            if !cmd.status.success() {
936                panic!(
937                    "qmltyperegistrar failed for {uri}:\n{}",
938                    String::from_utf8_lossy(&cmd.stderr)
939                );
940            }
941        }
942
943        // Generate QQmlEngineExtensionPlugin
944        let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
945        let qml_plugin_init_path = qml_plugin_dir.join(format!("{plugin_class_name}_init.cpp"));
946        let include_path;
947        {
948            let mut declarations = Vec::default();
949            let mut usages = Vec::default();
950
951            let mut generate_usage = |return_type: &str, function_name: &str| {
952                declarations.push(format!("extern {return_type} {function_name}();"));
953                usages.push(format!("volatile auto {function_name}_usage = &{function_name};\nQ_UNUSED({function_name}_usage);"));
954            };
955
956            // This function is generated by qmltyperegistrar
957            generate_usage("void", &format!("qml_register_types_{qml_uri_underscores}"));
958            generate_usage(
959                "int",
960                &format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"),
961            );
962
963            if !qml_files.is_empty() && self.qmlcachegen_executable.is_some() {
964                generate_usage(
965                    "int",
966                    &format!("qInitResources_qmlcache_{qml_uri_underscores}"),
967                );
968            }
969            let declarations = declarations.join("\n");
970            let usages = usages.join("\n");
971
972            std::fs::write(
973                &qml_plugin_cpp_path,
974                format!(
975                    r#"
976#include <QtQml/qqmlextensionplugin.h>
977
978// TODO: Add missing handling for GHS (Green Hills Software compiler) that is in
979// https://code.qt.io/cgit/qt/qtbase.git/plain/src/corelib/global/qtsymbolmacros.h
980{declarations}
981
982class {plugin_class_name} : public QQmlEngineExtensionPlugin
983{{
984    Q_OBJECT
985    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlEngineExtensionInterface")
986
987public:
988    {plugin_class_name}(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent)
989    {{
990        {usages}
991    }}
992}};
993
994// The moc-generated cpp file doesn't compile on its own; it needs to be #included here.
995#include "moc_{plugin_class_name}.cpp.cpp"
996"#,
997                ),
998            )
999            .expect("Failed to write plugin definition");
1000
1001            let moc_product = self.moc(
1002                &qml_plugin_cpp_path,
1003                MocArguments::default().uri(uri.to_owned()),
1004            );
1005            // Pass the include directory of the moc file to the caller
1006            include_path = moc_product.cpp.parent().map(|path| path.to_path_buf());
1007
1008            // Generate file to load static QQmlExtensionPlugin
1009            std::fs::write(
1010                &qml_plugin_init_path,
1011                format!(
1012                    r#"
1013#include <QtPlugin>
1014Q_IMPORT_PLUGIN({plugin_class_name});
1015"#
1016                ),
1017            )
1018            .expect("Failed to write plugin initializer file");
1019        }
1020
1021        QmlModuleRegistrationFiles {
1022            rcc: self.qrc(&qrc_path),
1023            qmlcachegen: qmlcachegen_file_paths,
1024            qmltyperegistrar: qmltyperegistrar_output_path,
1025            plugin: qml_plugin_cpp_path,
1026            plugin_init: qml_plugin_init_path,
1027            include_path,
1028        }
1029    }
1030
1031    /// 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).
1032    /// 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).
1033    /// The compiled static library must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
1034    /// or the linker will discard the generated static variables because they are not referenced from `main`.
1035    pub fn qrc(&mut self, input_file: &impl AsRef<Path>) -> PathBuf {
1036        if self.rcc_executable.is_none() {
1037            self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
1038        }
1039
1040        let input_path = input_file.as_ref();
1041        let output_folder = PathBuf::from(&format!(
1042            "{}/qt-build-utils/qrc",
1043            env::var("OUT_DIR").unwrap()
1044        ));
1045        std::fs::create_dir_all(&output_folder).expect("Could not create qrc dir");
1046        let output_path = output_folder.join(format!(
1047            "{}.cpp",
1048            input_path.file_name().unwrap().to_string_lossy(),
1049        ));
1050
1051        let cmd = Command::new(self.rcc_executable.as_ref().unwrap())
1052            .args([
1053                input_path.to_str().unwrap(),
1054                "-o",
1055                output_path.to_str().unwrap(),
1056                "--name",
1057                input_path.file_name().unwrap().to_str().unwrap(),
1058            ])
1059            .output()
1060            .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));
1061
1062        if !cmd.status.success() {
1063            panic!(
1064                "rcc failed for {}:\n{}",
1065                input_path.display(),
1066                String::from_utf8_lossy(&cmd.stderr)
1067            );
1068        }
1069
1070        output_path
1071    }
1072
1073    /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources
1074    pub fn qrc_list(&mut self, input_file: &impl AsRef<Path>) -> Vec<PathBuf> {
1075        if self.rcc_executable.is_none() {
1076            self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
1077        }
1078
1079        // Add the qrc file contents to the cargo rerun list
1080        let input_path = input_file.as_ref();
1081        let cmd_list = Command::new(self.rcc_executable.as_ref().unwrap())
1082            .args(["--list", input_path.to_str().unwrap()])
1083            .output()
1084            .unwrap_or_else(|_| panic!("rcc --list failed for {}", input_path.display()));
1085
1086        if !cmd_list.status.success() {
1087            panic!(
1088                "rcc --list failed for {}:\n{}",
1089                input_path.display(),
1090                String::from_utf8_lossy(&cmd_list.stderr)
1091            );
1092        }
1093
1094        String::from_utf8_lossy(&cmd_list.stdout)
1095            .split('\n')
1096            .map(PathBuf::from)
1097            .collect()
1098    }
1099}