From 57e0b7898b6fa0beb9ac8e928e717ee70db957d7 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Fri, 14 Mar 2025 15:06:51 +0000
Subject: [PATCH 01/27] qt-build-utils: use u64 as major type this allows us to
 move to SemVer

---
 crates/qt-build-utils/src/lib.rs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index bca04b71f..66324c690 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -58,9 +58,9 @@ pub enum QtBuildError {
     #[error("qmake version ({qmake_version}) does not match version specified by QT_VERSION_MAJOR ({qt_version_major})")]
     QtVersionMajorDoesNotMatch {
         /// The qmake version
-        qmake_version: u32,
+        qmake_version: u64,
         /// The Qt major version from `QT_VERSION_MAJOR`
-        qt_version_major: u32,
+        qt_version_major: u64,
     },
 }
 
@@ -296,7 +296,7 @@ impl QtBuild {
                             .to_string();
                         let qmake_version = versions::SemVer::new(version_string).unwrap();
                         if let Ok(env_version) = env::var("QT_VERSION_MAJOR") {
-                            let env_version = match env_version.trim().parse::<u32>() {
+                            let env_version = match env_version.trim().parse::<u64>() {
                                 Err(e) if *e.kind() == std::num::IntErrorKind::Empty => {
                                     println!(
                                         "cargo::warning=QT_VERSION_MAJOR environment variable defined but empty"
@@ -311,11 +311,11 @@ impl QtBuild {
                                 }
                                 Ok(int) => int,
                             };
-                            if env_version == qmake_version.major {
+                            if env_version == qmake_version.major as u64 {
                                 return Ok((candidate, qmake_version));
                             } else {
                                 return Err(QtBuildError::QtVersionMajorDoesNotMatch {
-                                    qmake_version: qmake_version.major,
+                                    qmake_version: qmake_version.major as u64,
                                     qt_version_major: env_version,
                                 });
                             }

From 72bba130c10b50aa6162ecaef22995b95cf0d643 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Fri, 14 Mar 2025 15:07:43 +0000
Subject: [PATCH 02/27] qt-build-utils: add anyhow and semver crates for later

---
 Cargo.lock                       | 14 ++++++++++++++
 crates/qt-build-utils/Cargo.toml |  2 ++
 crates/qt-build-utils/src/lib.rs |  6 +++---
 3 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 44e39dc81..b17c246df 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,6 +23,12 @@ version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
 
+[[package]]
+name = "anyhow"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+
 [[package]]
 name = "async-channel"
 version = "1.9.0"
@@ -1174,7 +1180,9 @@ dependencies = [
 name = "qt-build-utils"
 version = "0.7.2"
 dependencies = [
+ "anyhow",
  "cc",
+ "semver",
  "serde",
  "thiserror",
  "versions",
@@ -1234,6 +1242,12 @@ version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
 
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
 [[package]]
 name = "serde"
 version = "1.0.217"
diff --git a/crates/qt-build-utils/Cargo.toml b/crates/qt-build-utils/Cargo.toml
index acfc569e2..96e6521b2 100644
--- a/crates/qt-build-utils/Cargo.toml
+++ b/crates/qt-build-utils/Cargo.toml
@@ -14,7 +14,9 @@ repository.workspace = true
 rust-version.workspace = true
 
 [dependencies]
+anyhow = "1.0"
 cc.workspace = true
+semver = "1.0"
 serde = { workspace = true, optional = true }
 versions = "6.3"
 thiserror.workspace = true
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 66324c690..16e25e5e0 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -37,8 +37,8 @@ pub enum QtBuildError {
     QMakeSetQtMissing {
         /// The value of the qmake environment variable when the error occurred
         qmake_env_var: String,
-        /// The inner [QtBuildError] that occurred
-        error: Box<QtBuildError>,
+        /// The inner error that occurred
+        error: Box<anyhow::Error>,
     },
     /// Qt was not found
     #[error("Could not find Qt")]
@@ -344,7 +344,7 @@ impl QtBuild {
                 Err(e) => {
                     return Err(QtBuildError::QMakeSetQtMissing {
                         qmake_env_var,
-                        error: Box::new(e),
+                        error: Box::new(e.into()),
                     })
                 }
             }

From 095156eb00bbae3140478335716b256d1a3a2f8c Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 14:55:23 +0100
Subject: [PATCH 03/27] qt-build-utils: add a QtTool enum for later

---
 crates/qt-build-utils/src/lib.rs  |  3 +++
 crates/qt-build-utils/src/tool.rs | 29 +++++++++++++++++++++++++++++
 2 files changed, 32 insertions(+)
 create mode 100644 crates/qt-build-utils/src/tool.rs

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 16e25e5e0..7a0545d40 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -17,6 +17,9 @@
 
 mod parse_cflags;
 
+mod tool;
+pub use tool::QtTool;
+
 use std::{
     env,
     fs::File,
diff --git a/crates/qt-build-utils/src/tool.rs b/crates/qt-build-utils/src/tool.rs
new file mode 100644
index 000000000..ce6adf0ac
--- /dev/null
+++ b/crates/qt-build-utils/src/tool.rs
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+/// An enum representing known Qt tools
+#[non_exhaustive]
+pub enum QtTool {
+    /// Moc
+    Moc,
+    /// Rcc (Qt resources)
+    Rcc,
+    /// Qml cachegen
+    QmlCacheGen,
+    /// Qml Type Registrar
+    QmlTypeRegistrar,
+    // TODO: could add a Custom(&str) thing here
+}
+
+impl QtTool {
+    pub(crate) fn binary_name(&self) -> &str {
+        match self {
+            Self::Moc => "moc",
+            Self::Rcc => "rcc",
+            Self::QmlCacheGen => "qmlcachegen",
+            Self::QmlTypeRegistrar => "qmltyperegistrar",
+        }
+    }
+}

From dffb2420ce2b8f986b3e3f8ebd24017758f929a6 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Fri, 14 Mar 2025 15:08:46 +0000
Subject: [PATCH 04/27] qt-build-utils: add QtInstallation trait

---
 crates/qt-build-utils/src/installation/mod.rs | 39 +++++++++++++++++++
 crates/qt-build-utils/src/lib.rs              |  3 ++
 2 files changed, 42 insertions(+)
 create mode 100644 crates/qt-build-utils/src/installation/mod.rs

diff --git a/crates/qt-build-utils/src/installation/mod.rs b/crates/qt-build-utils/src/installation/mod.rs
new file mode 100644
index 000000000..b786c8ade
--- /dev/null
+++ b/crates/qt-build-utils/src/installation/mod.rs
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use semver::Version;
+use std::path::PathBuf;
+
+use crate::QtTool;
+
+/// A Qt Installation that can be used by cxx-qt-build to run Qt related tasks
+///
+/// Note that it is the responsbility of the QtInstallation implementation
+/// to print any cargo::rerun-if-changed lines
+pub trait QtInstallation {
+    /// Return the include paths for Qt, including Qt module subdirectories.
+    ///
+    /// This is intended to be passed to whichever tool you are using to invoke the C++ compiler.
+    fn include_paths(&self, qt_modules: &[String]) -> Vec<PathBuf>;
+    /// Configure the given cc::Build and cargo to link to the given Qt modules
+    ///
+    // TODO: should we hand in a cc::Build or should we instead return a struct
+    // with details of the rustc-link-lib / search paths ? and then have the
+    // calling function apply those and any flags to the cc::Build?
+    // eg return the following?
+    //
+    // pub struct LinkArgs {
+    //     builder_flag_if_supported: Vec<String>,
+    //     builder_object: Vec<String>,
+    //     rustc_link_arg: Vec<String>,
+    //     rustc_link_lib: Vec<String>,
+    //     rustc_link_search: Vec<String>,
+    // }
+    fn link_modules(&self, builder: &mut cc::Build, qt_modules: &[String]);
+    /// Find the path to a given Qt tool for the Qt installation
+    fn try_find_tool(&self, tool: QtTool) -> Option<PathBuf>;
+    /// Version of the detected Qt installation
+    fn version(&self) -> Version;
+}
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 7a0545d40..27b6ed0b8 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -15,6 +15,9 @@
 
 #![allow(clippy::too_many_arguments)]
 
+mod installation;
+pub use installation::QtInstallation;
+
 mod parse_cflags;
 
 mod tool;

From 9cc3405e87340d8e3f860e106868c0db86096cef Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Fri, 14 Mar 2025 15:09:05 +0000
Subject: [PATCH 05/27] qt-build-utils: add qmake implementation of
 QtInstallation

---
 crates/qt-build-utils/Cargo.toml              |   3 +
 crates/qt-build-utils/src/installation/mod.rs |   3 +
 .../qt-build-utils/src/installation/qmake.rs  | 183 ++++++++++++++++++
 crates/qt-build-utils/src/lib.rs              |   3 +
 4 files changed, 192 insertions(+)
 create mode 100644 crates/qt-build-utils/src/installation/qmake.rs

diff --git a/crates/qt-build-utils/Cargo.toml b/crates/qt-build-utils/Cargo.toml
index 96e6521b2..70c6ddd2d 100644
--- a/crates/qt-build-utils/Cargo.toml
+++ b/crates/qt-build-utils/Cargo.toml
@@ -22,6 +22,8 @@ versions = "6.3"
 thiserror.workspace = true
 
 [features]
+# TODO: should we default to qmake or let downstream crates specify, such as cxx-qt-build
+default = ["qmake"]
 # When Cargo links an executable, whether a bin crate or test executable,
 # and Qt 6 is linked statically, this feature must be enabled to link
 # unarchived .o files with static symbols that Qt ships (for example
@@ -33,6 +35,7 @@ thiserror.workspace = true
 #
 # When linking Qt dynamically, this makes no difference.
 link_qt_object_files = []
+qmake = []
 serde = ["dep:serde"]
 
 [lints]
diff --git a/crates/qt-build-utils/src/installation/mod.rs b/crates/qt-build-utils/src/installation/mod.rs
index b786c8ade..3847024f8 100644
--- a/crates/qt-build-utils/src/installation/mod.rs
+++ b/crates/qt-build-utils/src/installation/mod.rs
@@ -3,6 +3,9 @@
 //
 // SPDX-License-Identifier: MIT OR Apache-2.0
 
+#[cfg(feature = "qmake")]
+pub(crate) mod qmake;
+
 use semver::Version;
 use std::path::PathBuf;
 
diff --git a/crates/qt-build-utils/src/installation/qmake.rs b/crates/qt-build-utils/src/installation/qmake.rs
new file mode 100644
index 000000000..f39770f6c
--- /dev/null
+++ b/crates/qt-build-utils/src/installation/qmake.rs
@@ -0,0 +1,183 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use semver::Version;
+use std::{env, io::ErrorKind, path::PathBuf, process::Command};
+
+use crate::{QtBuildError, QtInstallation, QtTool};
+
+/// TODO
+pub struct QtInstallationQMake {
+    qmake_path: PathBuf,
+    qmake_version: Version,
+}
+
+impl QtInstallationQMake {
+    /// TODO
+    pub fn new() -> anyhow::Result<Self> {
+        // Try the QMAKE variable first
+        println!("cargo::rerun-if-env-changed=QMAKE");
+        if let Ok(qmake_env_var) = env::var("QMAKE") {
+            return QtInstallationQMake::try_from(PathBuf::from(&qmake_env_var)).map_err(|err| {
+                QtBuildError::QMakeSetQtMissing {
+                    qmake_env_var,
+                    error: err.into(),
+                }
+                .into()
+            });
+        }
+
+        // Try variable candidates within the patch
+        ["qmake6", "qmake-qt5", "qmake"]
+            .iter()
+            // Use the first non-errored installation
+            // If there are no valid installations we display the last error
+            .fold(None, |acc, qmake_path| {
+                Some(acc.map_or_else(
+                    // Value is None so try to create installation
+                    || QtInstallationQMake::try_from(PathBuf::from(qmake_path)),
+                    // Value is Some so pass through or create if Err
+                    |prev: anyhow::Result<Self>| {
+                        prev.or_else(|_|
+                            // Value is Err so try to create installation
+                            QtInstallationQMake::try_from(PathBuf::from(qmake_path)))
+                    },
+                ))
+            })
+            .unwrap_or_else(|| Err(QtBuildError::QtMissing.into()))
+    }
+}
+
+impl TryFrom<PathBuf> for QtInstallationQMake {
+    type Error = anyhow::Error;
+
+    fn try_from(qmake_path: PathBuf) -> anyhow::Result<Self> {
+        // Attempt to read the QT_VERSION from qmake
+        let qmake_version = match Command::new(&qmake_path)
+            .args(["-query", "QT_VERSION"])
+            .output()
+        {
+            Err(e) if e.kind() == ErrorKind::NotFound => Err(QtBuildError::QtMissing),
+            Err(e) => Err(QtBuildError::QmakeFailed(e)),
+            Ok(output) if !output.status.success() => Err(QtBuildError::QtMissing),
+            Ok(output) => Ok(Version::parse(
+                String::from_utf8_lossy(&output.stdout).trim(),
+            )?),
+        }?;
+
+        // Check QT_VERSION_MAJOR is the same as the qmake version
+        println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
+        if let Ok(env_qt_version_major) = env::var("QT_VERSION_MAJOR") {
+            // Parse to an integer
+            let env_qt_version_major = env_qt_version_major.trim().parse::<u64>().map_err(|e| {
+                QtBuildError::QtVersionMajorInvalid {
+                    qt_version_major_env_var: env_qt_version_major,
+                    source: e,
+                }
+            })?;
+
+            // Ensure the version major is the same
+            if qmake_version.major != env_qt_version_major {
+                return Err(QtBuildError::QtVersionMajorDoesNotMatch {
+                    qmake_version: qmake_version.major,
+                    qt_version_major: env_qt_version_major,
+                }
+                .into());
+            }
+        }
+
+        Ok(Self {
+            qmake_path,
+            qmake_version,
+        })
+    }
+}
+
+impl QtInstallation for QtInstallationQMake {
+    fn include_paths(&self, _qt_modules: &[String]) -> Vec<PathBuf> {
+        todo!()
+    }
+
+    fn link_modules(&self, _builder: &mut cc::Build, _qt_modules: &[String]) {
+        todo!()
+    }
+
+    fn try_find_tool(&self, tool: QtTool) -> Option<PathBuf> {
+        self.try_qmake_find_tool(tool.binary_name())
+    }
+
+    fn version(&self) -> semver::Version {
+        self.qmake_version.clone()
+    }
+}
+
+impl QtInstallationQMake {
+    fn qmake_query(&self, var_name: &str) -> String {
+        String::from_utf8_lossy(
+            &Command::new(&self.qmake_path)
+                .args(["-query", var_name])
+                .output()
+                .unwrap()
+                .stdout,
+        )
+        .trim()
+        .to_string()
+    }
+
+    fn try_qmake_find_tool(&self, tool_name: &str) -> Option<PathBuf> {
+        // "qmake -query" exposes a list of paths that describe where Qt executables and libraries
+        // are located, as well as where new executables & libraries should be installed to.
+        // We can use these variables to find any Qt tool.
+        //
+        // The order is important here.
+        // First, we check the _HOST_ variables.
+        // In cross-compilation contexts, these variables should point to the host toolchain used
+        // for building. The _INSTALL_ directories describe where to install new binaries to
+        // (i.e. the target directories).
+        // We still use the _INSTALL_ paths as fallback.
+        //
+        // The _LIBEXECS variables point to the executable Qt-internal tools (i.e. moc and
+        // friends), whilst _BINS point to the developer-facing executables (qdoc, qmake, etc.).
+        // As we mostly use the Qt-internal tools in this library, check _LIBEXECS first.
+        //
+        // Furthermore, in some contexts these variables include a `/get` variant.
+        // This is important for contexts where qmake and the Qt build tools do not have a static
+        // location, but are moved around during building.
+        // This notably happens with yocto builds.
+        // For each package, yocto builds a `sysroot` folder for both the host machine, as well
+        // as the target. This is done to keep package builds reproducable & separate.
+        // As a result the qmake executable is copied into each host sysroot for building.
+        //
+        // In this case the variables compiled into qmake still point to the paths relative
+        // from the host sysroot (e.g. /usr/bin).
+        // The /get variant in comparison will "get" the right full path from the current environment.
+        // Therefore prefer to use the `/get` variant when available.
+        // See: https://github.com/KDAB/cxx-qt/pull/430
+        //
+        // To check & debug all variables available on your system, simply run:
+        //
+        //              qmake -query
+        [
+            "QT_HOST_LIBEXECS/get",
+            "QT_HOST_LIBEXECS",
+            "QT_HOST_BINS/get",
+            "QT_HOST_BINS",
+            "QT_INSTALL_LIBEXECS/get",
+            "QT_INSTALL_LIBEXECS",
+            "QT_INSTALL_BINS/get",
+            "QT_INSTALL_BINS",
+        ]
+        .iter()
+        // Find the first valid executable path
+        .find_map(|qmake_query_var| {
+            let executable_path = PathBuf::from(self.qmake_query(qmake_query_var)).join(tool_name);
+            Command::new(&executable_path)
+                .args(["-help"])
+                .output()
+                .map(|_| executable_path)
+                .ok()
+        })
+    }
+}
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 27b6ed0b8..3f17f3f43 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -18,6 +18,9 @@
 mod installation;
 pub use installation::QtInstallation;
 
+#[cfg(feature = "qmake")]
+pub use installation::qmake::QtInstallationQMake;
+
 mod parse_cflags;
 
 mod tool;

From dd7f0d1e9baec3fcc417c8ad979262ea3f3b7ec3 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Fri, 14 Mar 2025 15:59:05 +0000
Subject: [PATCH 06/27] qt-build-utils: create a QtInstallation but don't use
 it

This is just to test what we can do still with the old API
---
 crates/qt-build-utils/src/lib.rs | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 3f17f3f43..d8cfaaee3 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -236,6 +236,7 @@ pub struct QmlModuleRegistrationFiles {
 /// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
 /// ```
 pub struct QtBuild {
+    qt_installation: Box<dyn QtInstallation>,
     version: SemVer,
     qmake_executable: String,
     moc_executable: Option<String>,
@@ -284,10 +285,16 @@ impl QtBuild {
     ///     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
     /// )
     /// ```
-    pub fn new(mut qt_modules: Vec<String>) -> Result<Self, QtBuildError> {
+    pub fn new(mut qt_modules: Vec<String>) -> anyhow::Result<Self> {
         if qt_modules.is_empty() {
             qt_modules.push("Core".to_string());
         }
+
+        #[cfg(feature = "qmake")]
+        let qt_installation = Box::new(QtInstallationQMake::new()?);
+        #[cfg(not(feature = "qmake"))]
+        unsupported!("Only qmake feature is supported");
+
         println!("cargo::rerun-if-env-changed=QMAKE");
         println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
         fn verify_candidate(candidate: &str) -> Result<(&str, versions::SemVer), QtBuildError> {
@@ -341,6 +348,7 @@ impl QtBuild {
             match verify_candidate(qmake_env_var.trim()) {
                 Ok((executable_name, version)) => {
                     return Ok(Self {
+                        qt_installation,
                         qmake_executable: executable_name.to_string(),
                         moc_executable: None,
                         qmltyperegistrar_executable: None,
@@ -354,7 +362,8 @@ impl QtBuild {
                     return Err(QtBuildError::QMakeSetQtMissing {
                         qmake_env_var,
                         error: Box::new(e.into()),
-                    })
+                    }
+                    .into())
                 }
             }
         }
@@ -365,6 +374,7 @@ impl QtBuild {
             match verify_candidate(executable_name) {
                 Ok((executable_name, version)) => {
                     return Ok(Self {
+                        qt_installation,
                         qmake_executable: executable_name.to_string(),
                         moc_executable: None,
                         qmltyperegistrar_executable: None,
@@ -386,17 +396,18 @@ impl QtBuild {
                         return Err(QtBuildError::QtVersionMajorDoesNotMatch {
                             qmake_version,
                             qt_version_major,
-                        });
+                        }
+                        .into());
                     }
                     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]);
                     continue;
                 }
                 Err(QtBuildError::QtMissing) => continue,
-                Err(e) => return Err(e),
+                Err(e) => return Err(e.into()),
             }
         }
 
-        Err(QtBuildError::QtMissing)
+        Err(QtBuildError::QtMissing.into())
     }
 
     /// Get the output of running `qmake -query var_name`
@@ -623,6 +634,7 @@ impl QtBuild {
 
     /// Version of the detected Qt installation
     pub fn version(&self) -> &SemVer {
+        let _ = self.qt_installation.version();
         &self.version
     }
 

From 53a39b6d3f2f2cdfcd40821c66629e1c8c86b617 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 14:27:30 +0100
Subject: [PATCH 07/27] cxx-qt-build: use version from QtInstallation

---
 Cargo.lock                       | 50 +-------------------------------
 Cargo.toml                       |  1 +
 crates/cxx-qt-build/Cargo.toml   |  2 +-
 crates/cxx-qt-build/src/lib.rs   |  4 +--
 crates/qt-build-utils/Cargo.toml |  3 +-
 crates/qt-build-utils/src/lib.rs | 48 ++++++++++++++----------------
 6 files changed, 28 insertions(+), 80 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index b17c246df..8b013fc8a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -394,9 +394,9 @@ dependencies = [
  "proc-macro2",
  "qt-build-utils",
  "quote",
+ "semver",
  "serde",
  "serde_json",
- "version_check",
 ]
 
 [[package]]
@@ -537,12 +537,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "either"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
-
 [[package]]
 name = "errno"
 version = "0.3.10"
@@ -935,15 +929,6 @@ version = "2.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
 
-[[package]]
-name = "itertools"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itoa"
 version = "1.0.14"
@@ -1020,22 +1005,6 @@ version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
 [[package]]
 name = "num-conv"
 version = "0.1.0"
@@ -1185,7 +1154,6 @@ dependencies = [
  "semver",
  "serde",
  "thiserror",
- "versions",
 ]
 
 [[package]]
@@ -1491,22 +1459,6 @@ version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
 
-[[package]]
-name = "version_check"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
-
-[[package]]
-name = "versions"
-version = "6.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f25d498b63d1fdb376b4250f39ab3a5ee8d103957346abacd911e2d8b612c139"
-dependencies = [
- "itertools",
- "nom",
-]
-
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.100"
diff --git a/Cargo.toml b/Cargo.toml
index 14d391692..90150e22f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,6 +59,7 @@ cxx-gen = "0.7.121"
 proc-macro2 = "1.0"
 syn = { version = "2.0", features = ["extra-traits", "full"] }
 quote = "1.0"
+semver = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 thiserror = "1.0"
diff --git a/crates/cxx-qt-build/Cargo.toml b/crates/cxx-qt-build/Cargo.toml
index d58970138..5f4afbbb2 100644
--- a/crates/cxx-qt-build/Cargo.toml
+++ b/crates/cxx-qt-build/Cargo.toml
@@ -21,9 +21,9 @@ proc-macro2.workspace = true
 quote.workspace = true
 qt-build-utils = { workspace = true, features = ["serde"] }
 codespan-reporting = "0.11"
-version_check = "0.9"
 serde.workspace = true
 serde_json = "1.0"
+semver.workspace = true
 
 [features]
 link_qt_object_files = ["qt-build-utils/link_qt_object_files"]
diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index 6522a27c7..19220880e 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -32,8 +32,8 @@ use qml_modules::OwningQmlModule;
 pub use qml_modules::QmlModule;
 
 pub use qt_build_utils::MocArguments;
-use qt_build_utils::SemVer;
 use quote::ToTokens;
+use semver::Version;
 use std::{
     collections::HashSet,
     env,
@@ -600,7 +600,7 @@ impl CxxQtBuilder {
         }
     }
 
-    fn define_qt_version_cfg_variables(version: &SemVer) {
+    fn define_qt_version_cfg_variables(version: Version) {
         // Allow for Qt 5 or Qt 6 as valid values
         CxxQtBuilder::define_cfg_check_variable(
             "cxxqt_qt_version_major".to_owned(),
diff --git a/crates/qt-build-utils/Cargo.toml b/crates/qt-build-utils/Cargo.toml
index 70c6ddd2d..b5fb673ae 100644
--- a/crates/qt-build-utils/Cargo.toml
+++ b/crates/qt-build-utils/Cargo.toml
@@ -16,9 +16,8 @@ rust-version.workspace = true
 [dependencies]
 anyhow = "1.0"
 cc.workspace = true
-semver = "1.0"
+semver.workspace = true
 serde = { workspace = true, optional = true }
-versions = "6.3"
 thiserror.workspace = true
 
 [features]
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index d8cfaaee3..3b753395b 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -34,8 +34,7 @@ use std::{
     process::Command,
 };
 
-pub use versions::SemVer;
-
+use semver::Version;
 use thiserror::Error;
 
 #[derive(Error, Debug)]
@@ -237,7 +236,6 @@ pub struct QmlModuleRegistrationFiles {
 /// ```
 pub struct QtBuild {
     qt_installation: Box<dyn QtInstallation>,
-    version: SemVer,
     qmake_executable: String,
     moc_executable: Option<String>,
     qmltyperegistrar_executable: Option<String>,
@@ -297,7 +295,7 @@ impl QtBuild {
 
         println!("cargo::rerun-if-env-changed=QMAKE");
         println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
-        fn verify_candidate(candidate: &str) -> Result<(&str, versions::SemVer), QtBuildError> {
+        fn verify_candidate(candidate: &str) -> Result<&str, QtBuildError> {
             match Command::new(candidate)
                 .args(["-query", "QT_VERSION"])
                 .output()
@@ -310,14 +308,14 @@ impl QtBuild {
                             .unwrap()
                             .trim()
                             .to_string();
-                        let qmake_version = versions::SemVer::new(version_string).unwrap();
+                        let qmake_version = Version::parse(&version_string).unwrap();
                         if let Ok(env_version) = env::var("QT_VERSION_MAJOR") {
                             let env_version = match env_version.trim().parse::<u64>() {
                                 Err(e) if *e.kind() == std::num::IntErrorKind::Empty => {
                                     println!(
                                         "cargo::warning=QT_VERSION_MAJOR environment variable defined but empty"
                                     );
-                                    return Ok((candidate, qmake_version));
+                                    return Ok(candidate);
                                 }
                                 Err(e) => {
                                     return Err(QtBuildError::QtVersionMajorInvalid {
@@ -328,7 +326,7 @@ impl QtBuild {
                                 Ok(int) => int,
                             };
                             if env_version == qmake_version.major as u64 {
-                                return Ok((candidate, qmake_version));
+                                return Ok(candidate);
                             } else {
                                 return Err(QtBuildError::QtVersionMajorDoesNotMatch {
                                     qmake_version: qmake_version.major as u64,
@@ -336,7 +334,7 @@ impl QtBuild {
                                 });
                             }
                         }
-                        Ok((candidate, qmake_version))
+                        Ok(candidate)
                     } else {
                         Err(QtBuildError::QtMissing)
                     }
@@ -346,7 +344,7 @@ impl QtBuild {
 
         if let Ok(qmake_env_var) = env::var("QMAKE") {
             match verify_candidate(qmake_env_var.trim()) {
-                Ok((executable_name, version)) => {
+                Ok(executable_name) => {
                     return Ok(Self {
                         qt_installation,
                         qmake_executable: executable_name.to_string(),
@@ -354,7 +352,6 @@ impl QtBuild {
                         qmltyperegistrar_executable: None,
                         qmlcachegen_executable: None,
                         rcc_executable: None,
-                        version,
                         qt_modules,
                     });
                 }
@@ -372,7 +369,7 @@ impl QtBuild {
         let candidate_executable_names = ["qmake6", "qmake-qt5", "qmake"];
         for (index, executable_name) in candidate_executable_names.iter().enumerate() {
             match verify_candidate(executable_name) {
-                Ok((executable_name, version)) => {
+                Ok(executable_name) => {
                     return Ok(Self {
                         qt_installation,
                         qmake_executable: executable_name.to_string(),
@@ -380,7 +377,6 @@ impl QtBuild {
                         qmltyperegistrar_executable: None,
                         qmlcachegen_executable: None,
                         rcc_executable: None,
-                        version,
                         qt_modules,
                     });
                 }
@@ -464,7 +460,7 @@ impl QtBuild {
         &self,
         lib_path: &str,
         prefix: &str,
-        version_major: u32,
+        version_major: u64,
         qt_module: &str,
     ) -> String {
         for arch in ["", "_arm64-v8a", "_armeabi-v7a", "_x86", "_x86_64"] {
@@ -551,13 +547,18 @@ impl QtBuild {
                 )
             } else {
                 (
-                    format!("Qt{}{qt_module}", self.version.major),
-                    self.find_qt_module_prl(&lib_path, prefix, self.version.major, qt_module),
+                    format!("Qt{}{qt_module}", self.qt_installation.version().major),
+                    self.find_qt_module_prl(
+                        &lib_path,
+                        prefix,
+                        self.qt_installation.version().major,
+                        qt_module,
+                    ),
                 )
             };
 
             self.cargo_link_qt_library(
-                &format!("Qt{}{qt_module}", self.version.major),
+                &format!("Qt{}{qt_module}", self.qt_installation.version().major),
                 &prefix_path,
                 &lib_path,
                 &link_lib,
@@ -633,9 +634,8 @@ impl QtBuild {
     }
 
     /// Version of the detected Qt installation
-    pub fn version(&self) -> &SemVer {
-        let _ = self.qt_installation.version();
-        &self.version
+    pub fn version(&self) -> Version {
+        self.qt_installation.version()
     }
 
     /// Lazy load the path of a Qt executable tool
@@ -780,7 +780,7 @@ impl QtBuild {
             );
         }
         // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
-        if self.qmlcachegen_executable.is_none() && self.version.major >= 6 {
+        if self.qmlcachegen_executable.is_none() && self.qt_installation.version().major >= 6 {
             if let Ok(qmlcachegen_executable) = self.get_qt_tool("qmlcachegen") {
                 self.qmlcachegen_executable = Some(qmlcachegen_executable);
             }
@@ -1127,12 +1127,8 @@ Q_IMPORT_PLUGIN({plugin_class_name});
             );
         }
 
-        let qt_6_5 = SemVer {
-            major: 6,
-            minor: 5,
-            ..SemVer::default()
-        };
-        let init_header = if self.version >= qt_6_5 {
+        let qt_6_5 = Version::new(6, 5, 0);
+        let init_header = if self.qt_installation.version() >= qt_6_5 {
             // With Qt6.5 the Q_INIT_RESOURCE macro is in the QtResource header
             "QtCore/QtResource"
         } else {

From e188bd51247205b97eeaba0c348e7e4b5e22fc64 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 15:12:02 +0100
Subject: [PATCH 08/27] qt-build-utils: ask QtInstallation for location of Qt
 tools

---
 crates/qt-build-utils/src/lib.rs | 126 ++++++++-----------------------
 1 file changed, 31 insertions(+), 95 deletions(-)

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 3b753395b..d9b5217a7 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -237,10 +237,6 @@ pub struct QmlModuleRegistrationFiles {
 pub struct QtBuild {
     qt_installation: Box<dyn QtInstallation>,
     qmake_executable: String,
-    moc_executable: Option<String>,
-    qmltyperegistrar_executable: Option<String>,
-    qmlcachegen_executable: Option<String>,
-    rcc_executable: Option<String>,
     qt_modules: Vec<String>,
 }
 
@@ -348,10 +344,6 @@ impl QtBuild {
                     return Ok(Self {
                         qt_installation,
                         qmake_executable: executable_name.to_string(),
-                        moc_executable: None,
-                        qmltyperegistrar_executable: None,
-                        qmlcachegen_executable: None,
-                        rcc_executable: None,
                         qt_modules,
                     });
                 }
@@ -373,10 +365,6 @@ impl QtBuild {
                     return Ok(Self {
                         qt_installation,
                         qmake_executable: executable_name.to_string(),
-                        moc_executable: None,
-                        qmltyperegistrar_executable: None,
-                        qmlcachegen_executable: None,
-                        rcc_executable: None,
                         qt_modules,
                     });
                 }
@@ -638,70 +626,15 @@ impl QtBuild {
         self.qt_installation.version()
     }
 
-    /// Lazy load the path of a Qt executable tool
-    /// Skip doing this in the constructor because not every user of this crate will use each tool
-    fn get_qt_tool(&self, tool_name: &str) -> Result<String, ()> {
-        // "qmake -query" exposes a list of paths that describe where Qt executables and libraries
-        // are located, as well as where new executables & libraries should be installed to.
-        // We can use these variables to find any Qt tool.
-        //
-        // The order is important here.
-        // First, we check the _HOST_ variables.
-        // In cross-compilation contexts, these variables should point to the host toolchain used
-        // for building. The _INSTALL_ directories describe where to install new binaries to
-        // (i.e. the target directories).
-        // We still use the _INSTALL_ paths as fallback.
-        //
-        // The _LIBEXECS variables point to the executable Qt-internal tools (i.e. moc and
-        // friends), whilst _BINS point to the developer-facing executables (qdoc, qmake, etc.).
-        // As we mostly use the Qt-internal tools in this library, check _LIBEXECS first.
-        //
-        // Furthermore, in some contexts these variables include a `/get` variant.
-        // This is important for contexts where qmake and the Qt build tools do not have a static
-        // location, but are moved around during building.
-        // This notably happens with yocto builds.
-        // For each package, yocto builds a `sysroot` folder for both the host machine, as well
-        // as the target. This is done to keep package builds reproducable & separate.
-        // As a result the qmake executable is copied into each host sysroot for building.
-        //
-        // In this case the variables compiled into qmake still point to the paths relative
-        // from the host sysroot (e.g. /usr/bin).
-        // The /get variant in comparison will "get" the right full path from the current environment.
-        // Therefore prefer to use the `/get` variant when available.
-        // See: https://github.com/KDAB/cxx-qt/pull/430
-        //
-        // To check & debug all variables available on your system, simply run:
-        //
-        //              qmake -query
-        //
-        for qmake_query_var in [
-            "QT_HOST_LIBEXECS/get",
-            "QT_HOST_LIBEXECS",
-            "QT_HOST_BINS/get",
-            "QT_HOST_BINS",
-            "QT_INSTALL_LIBEXECS/get",
-            "QT_INSTALL_LIBEXECS",
-            "QT_INSTALL_BINS/get",
-            "QT_INSTALL_BINS",
-        ] {
-            let executable_path = format!("{}/{tool_name}", self.qmake_query(qmake_query_var));
-            match Command::new(&executable_path).args(["-help"]).output() {
-                Ok(_) => return Ok(executable_path),
-                Err(_) => continue,
-            }
-        }
-        Err(())
-    }
-
     /// 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).
     /// 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),
     /// as well as the path to the generated metatypes.json file, which can be passed to [register_qml_module](Self::register_qml_module).
     ///
     pub fn moc(&mut self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
-        if self.moc_executable.is_none() {
-            self.moc_executable = Some(self.get_qt_tool("moc").expect("Could not find moc"));
-        }
-
+        let moc_executable = self
+            .qt_installation
+            .try_find_tool(QtTool::Moc)
+            .expect("Could not find moc");
         let input_path = input_file.as_ref();
 
         // Put all the moc files into one place, this can then be added to the include path
@@ -727,7 +660,7 @@ impl QtBuild {
             include_args.push(format!("-I{}", include_path.display()));
         }
 
-        let mut cmd = Command::new(self.moc_executable.as_ref().unwrap());
+        let mut cmd = Command::new(moc_executable);
 
         if let Some(uri) = arguments.uri {
             cmd.arg(format!("-Muri={uri}"));
@@ -773,18 +706,20 @@ impl QtBuild {
         qml_files: &[impl AsRef<Path>],
         qrc_files: &[impl AsRef<Path>],
     ) -> QmlModuleRegistrationFiles {
-        if self.qmltyperegistrar_executable.is_none() {
-            self.qmltyperegistrar_executable = Some(
-                self.get_qt_tool("qmltyperegistrar")
-                    .expect("Could not find qmltyperegistrar"),
-            );
-        }
+        let qmltyperegistrar_executable = self
+            .qt_installation
+            .try_find_tool(QtTool::QmlTypeRegistrar)
+            .expect("Could not find qmltyperegistrar");
         // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
-        if self.qmlcachegen_executable.is_none() && self.qt_installation.version().major >= 6 {
-            if let Ok(qmlcachegen_executable) = self.get_qt_tool("qmlcachegen") {
-                self.qmlcachegen_executable = Some(qmlcachegen_executable);
-            }
-        }
+        let qmlcachegen_executable = if self.qt_installation.version().major >= 6 {
+            Some(
+                self.qt_installation
+                    .try_find_tool(QtTool::QmlCacheGen)
+                    .expect("Could not find qmlcachegen"),
+            )
+        } else {
+            None
+        };
 
         let qml_uri_dirs = uri.replace('.', "/");
 
@@ -860,7 +795,7 @@ prefer :/qt/qml/{qml_uri_dirs}/
         // qmlcachegen needs to be run once for each .qml file with --resource-path,
         // then once for the module with --resource-name.
         let mut qmlcachegen_file_paths = Vec::new();
-        if let Some(qmlcachegen_executable) = &self.qmlcachegen_executable {
+        if let Some(qmlcachegen_executable) = &qmlcachegen_executable {
             let qmlcachegen_dir = qt_build_utils_dir.join("qmlcachegen").join(&qml_uri_dirs);
             std::fs::create_dir_all(&qmlcachegen_dir)
                 .expect("Could not create qmlcachegen directory for QML module");
@@ -978,7 +913,7 @@ prefer :/qt/qml/{qml_uri_dirs}/
                 qmltyperegistrar_output_path.to_string_lossy().to_string(),
             ];
             args.extend(metatypes_json);
-            let cmd = Command::new(self.qmltyperegistrar_executable.as_ref().unwrap())
+            let cmd = Command::new(qmltyperegistrar_executable)
                 .args(args)
                 .output()
                 .unwrap_or_else(|_| panic!("qmltyperegistrar failed for {uri}"));
@@ -1009,7 +944,7 @@ prefer :/qt/qml/{qml_uri_dirs}/
                 &format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"),
             );
 
-            if !qml_files.is_empty() && self.qmlcachegen_executable.is_some() {
+            if !qml_files.is_empty() && qmlcachegen_executable.is_some() {
                 generate_usage(
                     "int",
                     &format!("qInitResources_qmlcache_{qml_uri_underscores}"),
@@ -1088,10 +1023,10 @@ Q_IMPORT_PLUGIN({plugin_class_name});
     /// the `+whole-archive` flag is used, or the initializer function is called by the
     /// application.
     pub fn qrc(&mut self, input_file: &impl AsRef<Path>) -> Initializer {
-        if self.rcc_executable.is_none() {
-            self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
-        }
-
+        let rcc_executable = self
+            .qt_installation
+            .try_find_tool(QtTool::Rcc)
+            .expect("Could not find rcc");
         let input_path = input_file.as_ref();
         let output_folder = PathBuf::from(&format!(
             "{}/qt-build-utils/qrc",
@@ -1108,7 +1043,7 @@ Q_IMPORT_PLUGIN({plugin_class_name});
             .to_string_lossy()
             .replace('.', "_");
 
-        let cmd = Command::new(self.rcc_executable.as_ref().unwrap())
+        let cmd = Command::new(rcc_executable)
             .args([
                 input_path.to_str().unwrap(),
                 "-o",
@@ -1143,13 +1078,14 @@ Q_IMPORT_PLUGIN({plugin_class_name});
 
     /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources
     pub fn qrc_list(&mut self, input_file: &impl AsRef<Path>) -> Vec<PathBuf> {
-        if self.rcc_executable.is_none() {
-            self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
-        }
+        let rcc_executable = self
+            .qt_installation
+            .try_find_tool(QtTool::Rcc)
+            .expect("Could not find rcc");
 
         // Add the qrc file contents to the cargo rerun list
         let input_path = input_file.as_ref();
-        let cmd_list = Command::new(self.rcc_executable.as_ref().unwrap())
+        let cmd_list = Command::new(rcc_executable)
             .args(["--list", input_path.to_str().unwrap()])
             .output()
             .unwrap_or_else(|_| panic!("rcc --list failed for {}", input_path.display()));

From a4effb304c4bca6aafca98353e2bd5d1c56bed9a Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 15:25:38 +0100
Subject: [PATCH 09/27] qt-build-utils: split out common is_*_target

---
 crates/qt-build-utils/src/lib.rs   | 21 ++++++---------------
 crates/qt-build-utils/src/utils.rs | 16 ++++++++++++++++
 2 files changed, 22 insertions(+), 15 deletions(-)
 create mode 100644 crates/qt-build-utils/src/utils.rs

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index d9b5217a7..4329f8d12 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -26,6 +26,8 @@ mod parse_cflags;
 mod tool;
 pub use tool::QtTool;
 
+mod utils;
+
 use std::{
     env,
     fs::File,
@@ -76,13 +78,6 @@ fn command_help_output(command: &str) -> std::io::Result<std::process::Output> {
     Command::new(command).args(["--help"]).output()
 }
 
-/// Whether apple is the current target
-fn is_apple_target() -> bool {
-    env::var("TARGET")
-        .map(|target| target.contains("apple"))
-        .unwrap_or_else(|_| false)
-}
-
 /// Linking executables (including tests) with Cargo that link to Qt fails to link with GNU ld.bfd,
 /// which is the default on most Linux distributions, so use GNU ld.gold, lld, or mold instead.
 /// If you are using a C++ build system such as CMake to do the final link of the executable, you do
@@ -496,7 +491,7 @@ impl QtBuild {
         //
         // Note that this adds the framework path which allows for
         // includes such as <QtCore/QObject> to be resolved correctly
-        if is_apple_target() {
+        if utils::is_apple_target() {
             println!("cargo::rustc-link-search=framework={lib_path}");
 
             // Ensure that any framework paths are set to -F
@@ -522,7 +517,7 @@ impl QtBuild {
         };
 
         for qt_module in &self.qt_modules {
-            let framework = if is_apple_target() {
+            let framework = if utils::is_apple_target() {
                 Path::new(&format!("{lib_path}/Qt{qt_module}.framework")).exists()
             } else {
                 false
@@ -555,11 +550,7 @@ impl QtBuild {
             );
         }
 
-        let emscripten_targeted = match env::var("CARGO_CFG_TARGET_OS") {
-            Ok(val) => val == "emscripten",
-            Err(_) => false,
-        };
-        if emscripten_targeted {
+        if utils::is_emscripten_target() {
             let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
             println!("cargo::rustc-link-search={platforms_path}");
             self.cargo_link_qt_library(
@@ -605,7 +596,7 @@ impl QtBuild {
 
             // Ensure that we add any framework's headers path
             let header_path = format!("{lib_path}/Qt{qt_module}.framework/Headers");
-            if is_apple_target() && Path::new(&header_path).exists() {
+            if utils::is_apple_target() && Path::new(&header_path).exists() {
                 paths.push(header_path);
             }
         }
diff --git a/crates/qt-build-utils/src/utils.rs b/crates/qt-build-utils/src/utils.rs
new file mode 100644
index 000000000..347846276
--- /dev/null
+++ b/crates/qt-build-utils/src/utils.rs
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+/// Whether apple is the current target
+pub(crate) fn is_apple_target() -> bool {
+    std::env::var("TARGET")
+        .map(|target| target.contains("apple"))
+        .unwrap_or_else(|_| false)
+}
+
+/// Whether emscripten is the current target
+pub(crate) fn is_emscripten_target() -> bool {
+    std::env::var("CARGO_CFG_TARGET_OS") == Ok("emscripten".to_owned())
+}

From 6f3770c9376e9d8ec84ce62128c4b176c2927e90 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 15:38:34 +0100
Subject: [PATCH 10/27] qt-build-utils: move linking of Qt modules to Qt
 Installation

---
 .../qt-build-utils/src/installation/qmake.rs  | 184 +++++++++++++++++-
 crates/qt-build-utils/src/lib.rs              | 178 +----------------
 2 files changed, 181 insertions(+), 181 deletions(-)

diff --git a/crates/qt-build-utils/src/installation/qmake.rs b/crates/qt-build-utils/src/installation/qmake.rs
index f39770f6c..6e8cbc2da 100644
--- a/crates/qt-build-utils/src/installation/qmake.rs
+++ b/crates/qt-build-utils/src/installation/qmake.rs
@@ -4,9 +4,14 @@
 // SPDX-License-Identifier: MIT OR Apache-2.0
 
 use semver::Version;
-use std::{env, io::ErrorKind, path::PathBuf, process::Command};
+use std::{
+    env,
+    io::ErrorKind,
+    path::{Path, PathBuf},
+    process::Command,
+};
 
-use crate::{QtBuildError, QtInstallation, QtTool};
+use crate::{parse_cflags, utils, QtBuildError, QtInstallation, QtTool};
 
 /// TODO
 pub struct QtInstallationQMake {
@@ -100,8 +105,90 @@ impl QtInstallation for QtInstallationQMake {
         todo!()
     }
 
-    fn link_modules(&self, _builder: &mut cc::Build, _qt_modules: &[String]) {
-        todo!()
+    fn link_modules(&self, builder: &mut cc::Build, qt_modules: &[String]) {
+        let prefix_path = self.qmake_query("QT_INSTALL_PREFIX");
+        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
+        println!("cargo::rustc-link-search={lib_path}");
+
+        let target = env::var("TARGET");
+
+        // Add the QT_INSTALL_LIBS as a framework link search path as well
+        //
+        // Note that leaving the kind empty should default to all,
+        // but this doesn't appear to find frameworks in all situations
+        // https://github.com/KDAB/cxx-qt/issues/885
+        //
+        // Note this doesn't have an adverse affect running all the time
+        // as it appears that all rustc-link-search are added
+        //
+        // Note that this adds the framework path which allows for
+        // includes such as <QtCore/QObject> to be resolved correctly
+        if utils::is_apple_target() {
+            println!("cargo::rustc-link-search=framework={lib_path}");
+
+            // Ensure that any framework paths are set to -F
+            for framework_path in self.qmake_framework_paths() {
+                builder.flag_if_supported(format!("-F{}", framework_path.display()));
+                // Also set the -rpath otherwise frameworks can not be found at runtime
+                println!(
+                    "cargo::rustc-link-arg=-Wl,-rpath,{}",
+                    framework_path.display()
+                );
+            }
+        }
+
+        let prefix = match &target {
+            Ok(target) => {
+                if target.contains("windows") {
+                    ""
+                } else {
+                    "lib"
+                }
+            }
+            Err(_) => "lib",
+        };
+
+        for qt_module in qt_modules {
+            let framework = if utils::is_apple_target() {
+                Path::new(&format!("{lib_path}/Qt{qt_module}.framework")).exists()
+            } else {
+                false
+            };
+
+            let (link_lib, prl_path) = if framework {
+                (
+                    format!("framework=Qt{qt_module}"),
+                    format!("{lib_path}/Qt{qt_module}.framework/Resources/Qt{qt_module}.prl"),
+                )
+            } else {
+                (
+                    format!("Qt{}{qt_module}", self.qmake_version.major),
+                    self.find_qt_module_prl(&lib_path, prefix, self.qmake_version.major, qt_module),
+                )
+            };
+
+            self.link_qt_library(
+                &format!("Qt{}{qt_module}", self.qmake_version.major),
+                &prefix_path,
+                &lib_path,
+                &link_lib,
+                &prl_path,
+                builder,
+            );
+        }
+
+        if utils::is_emscripten_target() {
+            let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
+            println!("cargo::rustc-link-search={platforms_path}");
+            self.link_qt_library(
+                "qwasm",
+                &prefix_path,
+                &lib_path,
+                "qwasm",
+                &format!("{platforms_path}/libqwasm.prl"),
+                builder,
+            );
+        }
     }
 
     fn try_find_tool(&self, tool: QtTool) -> Option<PathBuf> {
@@ -114,6 +201,95 @@ impl QtInstallation for QtInstallationQMake {
 }
 
 impl QtInstallationQMake {
+    /// Some prl files include their architecture in their naming scheme.
+    /// Just try all known architectures and fallback to non when they all failed.
+    fn find_qt_module_prl(
+        &self,
+        lib_path: &str,
+        prefix: &str,
+        version_major: u64,
+        qt_module: &str,
+    ) -> String {
+        for arch in ["", "_arm64-v8a", "_armeabi-v7a", "_x86", "_x86_64"] {
+            let prl_path = format!(
+                "{}/{}Qt{}{}{}.prl",
+                lib_path, prefix, version_major, qt_module, arch
+            );
+            match Path::new(&prl_path).try_exists() {
+                Ok(exists) => {
+                    if exists {
+                        return prl_path;
+                    }
+                }
+                Err(e) => {
+                    println!(
+                        "cargo::warning=failed checking for existence of {}: {}",
+                        prl_path, e
+                    );
+                }
+            }
+        }
+
+        format!(
+            "{}/{}Qt{}{}.prl",
+            lib_path, prefix, version_major, qt_module
+        )
+    }
+
+    fn link_qt_library(
+        &self,
+        name: &str,
+        prefix_path: &str,
+        lib_path: &str,
+        link_lib: &str,
+        prl_path: &str,
+        builder: &mut cc::Build,
+    ) {
+        println!("cargo::rustc-link-lib={link_lib}");
+
+        match std::fs::read_to_string(prl_path) {
+            Ok(prl) => {
+                for line in prl.lines() {
+                    if let Some(line) = line.strip_prefix("QMAKE_PRL_LIBS = ") {
+                        parse_cflags::parse_libs_cflags(
+                            name,
+                            line.replace(r"$$[QT_INSTALL_LIBS]", lib_path)
+                                .replace(r"$$[QT_INSTALL_PREFIX]", prefix_path)
+                                .as_bytes(),
+                            builder,
+                        );
+                    }
+                }
+            }
+            Err(e) => {
+                println!(
+                    "cargo::warning=Could not open {} file to read libraries to link: {}",
+                    &prl_path, e
+                );
+            }
+        }
+    }
+
+    /// Get the framework paths for Qt. This is intended
+    /// to be passed to whichever tool you are using to invoke the C++ compiler.
+    fn qmake_framework_paths(&self) -> Vec<PathBuf> {
+        let mut framework_paths = vec![];
+
+        if utils::is_apple_target() {
+            // Note that this adds the framework path which allows for
+            // includes such as <QtCore/QObject> to be resolved correctly
+            let framework_path = self.qmake_query("QT_INSTALL_LIBS");
+            framework_paths.push(framework_path);
+        }
+
+        framework_paths
+            .iter()
+            .map(PathBuf::from)
+            // Only add paths if they exist
+            .filter(|path| path.exists())
+            .collect()
+    }
+
     fn qmake_query(&self, var_name: &str) -> String {
         String::from_utf8_lossy(
             &Command::new(&self.qmake_path)
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 4329f8d12..47c406f55 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -403,185 +403,9 @@ impl QtBuild {
         .to_string()
     }
 
-    fn cargo_link_qt_library(
-        &self,
-        name: &str,
-        prefix_path: &str,
-        lib_path: &str,
-        link_lib: &str,
-        prl_path: &str,
-        builder: &mut cc::Build,
-    ) {
-        println!("cargo::rustc-link-lib={link_lib}");
-
-        match std::fs::read_to_string(prl_path) {
-            Ok(prl) => {
-                for line in prl.lines() {
-                    if let Some(line) = line.strip_prefix("QMAKE_PRL_LIBS = ") {
-                        parse_cflags::parse_libs_cflags(
-                            name,
-                            line.replace(r"$$[QT_INSTALL_LIBS]", lib_path)
-                                .replace(r"$$[QT_INSTALL_PREFIX]", prefix_path)
-                                .as_bytes(),
-                            builder,
-                        );
-                    }
-                }
-            }
-            Err(e) => {
-                println!(
-                    "cargo::warning=Could not open {} file to read libraries to link: {}",
-                    &prl_path, e
-                );
-            }
-        }
-    }
-
-    /// Some prl files include their architecture in their naming scheme.
-    /// Just try all known architectures and fallback to non when they all failed.
-    fn find_qt_module_prl(
-        &self,
-        lib_path: &str,
-        prefix: &str,
-        version_major: u64,
-        qt_module: &str,
-    ) -> String {
-        for arch in ["", "_arm64-v8a", "_armeabi-v7a", "_x86", "_x86_64"] {
-            let prl_path = format!(
-                "{}/{}Qt{}{}{}.prl",
-                lib_path, prefix, version_major, qt_module, arch
-            );
-            match Path::new(&prl_path).try_exists() {
-                Ok(exists) => {
-                    if exists {
-                        return prl_path;
-                    }
-                }
-                Err(e) => {
-                    println!(
-                        "cargo::warning=failed checking for existence of {}: {}",
-                        prl_path, e
-                    );
-                }
-            }
-        }
-
-        format!(
-            "{}/{}Qt{}{}.prl",
-            lib_path, prefix, version_major, qt_module
-        )
-    }
-
     /// Tell Cargo to link each Qt module.
     pub fn cargo_link_libraries(&self, builder: &mut cc::Build) {
-        let prefix_path = self.qmake_query("QT_INSTALL_PREFIX");
-        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
-        println!("cargo::rustc-link-search={lib_path}");
-
-        let target = env::var("TARGET");
-
-        // Add the QT_INSTALL_LIBS as a framework link search path as well
-        //
-        // Note that leaving the kind empty should default to all,
-        // but this doesn't appear to find frameworks in all situations
-        // https://github.com/KDAB/cxx-qt/issues/885
-        //
-        // Note this doesn't have an adverse affect running all the time
-        // as it appears that all rustc-link-search are added
-        //
-        // Note that this adds the framework path which allows for
-        // includes such as <QtCore/QObject> to be resolved correctly
-        if utils::is_apple_target() {
-            println!("cargo::rustc-link-search=framework={lib_path}");
-
-            // Ensure that any framework paths are set to -F
-            for framework_path in self.framework_paths() {
-                builder.flag_if_supported(format!("-F{}", framework_path.display()));
-                // Also set the -rpath otherwise frameworks can not be found at runtime
-                println!(
-                    "cargo::rustc-link-arg=-Wl,-rpath,{}",
-                    framework_path.display()
-                );
-            }
-        }
-
-        let prefix = match &target {
-            Ok(target) => {
-                if target.contains("windows") {
-                    ""
-                } else {
-                    "lib"
-                }
-            }
-            Err(_) => "lib",
-        };
-
-        for qt_module in &self.qt_modules {
-            let framework = if utils::is_apple_target() {
-                Path::new(&format!("{lib_path}/Qt{qt_module}.framework")).exists()
-            } else {
-                false
-            };
-
-            let (link_lib, prl_path) = if framework {
-                (
-                    format!("framework=Qt{qt_module}"),
-                    format!("{lib_path}/Qt{qt_module}.framework/Resources/Qt{qt_module}.prl"),
-                )
-            } else {
-                (
-                    format!("Qt{}{qt_module}", self.qt_installation.version().major),
-                    self.find_qt_module_prl(
-                        &lib_path,
-                        prefix,
-                        self.qt_installation.version().major,
-                        qt_module,
-                    ),
-                )
-            };
-
-            self.cargo_link_qt_library(
-                &format!("Qt{}{qt_module}", self.qt_installation.version().major),
-                &prefix_path,
-                &lib_path,
-                &link_lib,
-                &prl_path,
-                builder,
-            );
-        }
-
-        if utils::is_emscripten_target() {
-            let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
-            println!("cargo::rustc-link-search={platforms_path}");
-            self.cargo_link_qt_library(
-                "qwasm",
-                &prefix_path,
-                &lib_path,
-                "qwasm",
-                &format!("{platforms_path}/libqwasm.prl"),
-                builder,
-            );
-        }
-    }
-
-    /// Get the framework paths for Qt. This is intended
-    /// to be passed to whichever tool you are using to invoke the C++ compiler.
-    pub fn framework_paths(&self) -> Vec<PathBuf> {
-        let mut framework_paths = vec![];
-
-        if is_apple_target() {
-            // Note that this adds the framework path which allows for
-            // includes such as <QtCore/QObject> to be resolved correctly
-            let framework_path = self.qmake_query("QT_INSTALL_LIBS");
-            framework_paths.push(framework_path);
-        }
-
-        framework_paths
-            .iter()
-            .map(PathBuf::from)
-            // Only add paths if they exist
-            .filter(|path| path.exists())
-            .collect()
+        self.qt_installation.link_modules(builder, &self.qt_modules);
     }
 
     /// Get the include paths for Qt, including Qt module subdirectories. This is intended

From 02c434171017a85f2b5b1fe9345ac373ff6cfd49 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 15:46:41 +0100
Subject: [PATCH 11/27] qt-build-utils: move include paths to qmake
 installation

---
 .../qt-build-utils/src/installation/qmake.rs  | 26 +++++++++++++++++--
 crates/qt-build-utils/src/lib.rs              | 24 +----------------
 2 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/crates/qt-build-utils/src/installation/qmake.rs b/crates/qt-build-utils/src/installation/qmake.rs
index 6e8cbc2da..bedc8248f 100644
--- a/crates/qt-build-utils/src/installation/qmake.rs
+++ b/crates/qt-build-utils/src/installation/qmake.rs
@@ -101,8 +101,30 @@ impl TryFrom<PathBuf> for QtInstallationQMake {
 }
 
 impl QtInstallation for QtInstallationQMake {
-    fn include_paths(&self, _qt_modules: &[String]) -> Vec<PathBuf> {
-        todo!()
+    fn include_paths(&self, qt_modules: &[String]) -> Vec<PathBuf> {
+        let root_path = self.qmake_query("QT_INSTALL_HEADERS");
+        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
+        let mut paths = Vec::new();
+        for qt_module in qt_modules {
+            // Add the usual location for the Qt module
+            paths.push(format!("{root_path}/Qt{qt_module}"));
+
+            // Ensure that we add any framework's headers path
+            let header_path = format!("{lib_path}/Qt{qt_module}.framework/Headers");
+            if utils::is_apple_target() && Path::new(&header_path).exists() {
+                paths.push(header_path);
+            }
+        }
+
+        // Add the QT_INSTALL_HEADERS itself
+        paths.push(root_path);
+
+        paths
+            .iter()
+            .map(PathBuf::from)
+            // Only add paths if they exist
+            .filter(|path| path.exists())
+            .collect()
     }
 
     fn link_modules(&self, builder: &mut cc::Build, qt_modules: &[String]) {
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 47c406f55..3a61f0f32 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -411,29 +411,7 @@ impl QtBuild {
     /// Get the include paths for Qt, including Qt module subdirectories. This is intended
     /// to be passed to whichever tool you are using to invoke the C++ compiler.
     pub fn include_paths(&self) -> Vec<PathBuf> {
-        let root_path = self.qmake_query("QT_INSTALL_HEADERS");
-        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
-        let mut paths = Vec::new();
-        for qt_module in &self.qt_modules {
-            // Add the usual location for the Qt module
-            paths.push(format!("{root_path}/Qt{qt_module}"));
-
-            // Ensure that we add any framework's headers path
-            let header_path = format!("{lib_path}/Qt{qt_module}.framework/Headers");
-            if utils::is_apple_target() && Path::new(&header_path).exists() {
-                paths.push(header_path);
-            }
-        }
-
-        // Add the QT_INSTALL_HEADERS itself
-        paths.push(root_path);
-
-        paths
-            .iter()
-            .map(PathBuf::from)
-            // Only add paths if they exist
-            .filter(|path| path.exists())
-            .collect()
+        self.qt_installation.include_paths(&self.qt_modules)
     }
 
     /// Version of the detected Qt installation

From 112871aec773320b5ca33b060c1bd422edc9af75 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 15:48:50 +0100
Subject: [PATCH 12/27] qt-build-utils: remove qmake executable path as it is
 in installation

---
 crates/qt-build-utils/src/lib.rs | 122 +------------------------------
 1 file changed, 4 insertions(+), 118 deletions(-)

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 3a61f0f32..e3013852b 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -231,7 +231,6 @@ pub struct QmlModuleRegistrationFiles {
 /// ```
 pub struct QtBuild {
     qt_installation: Box<dyn QtInstallation>,
-    qmake_executable: String,
     qt_modules: Vec<String>,
 }
 
@@ -284,123 +283,10 @@ impl QtBuild {
         #[cfg(not(feature = "qmake"))]
         unsupported!("Only qmake feature is supported");
 
-        println!("cargo::rerun-if-env-changed=QMAKE");
-        println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
-        fn verify_candidate(candidate: &str) -> Result<&str, QtBuildError> {
-            match Command::new(candidate)
-                .args(["-query", "QT_VERSION"])
-                .output()
-            {
-                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(QtBuildError::QtMissing),
-                Err(e) => Err(QtBuildError::QmakeFailed(e)),
-                Ok(output) => {
-                    if output.status.success() {
-                        let version_string = std::str::from_utf8(&output.stdout)
-                            .unwrap()
-                            .trim()
-                            .to_string();
-                        let qmake_version = Version::parse(&version_string).unwrap();
-                        if let Ok(env_version) = env::var("QT_VERSION_MAJOR") {
-                            let env_version = match env_version.trim().parse::<u64>() {
-                                Err(e) if *e.kind() == std::num::IntErrorKind::Empty => {
-                                    println!(
-                                        "cargo::warning=QT_VERSION_MAJOR environment variable defined but empty"
-                                    );
-                                    return Ok(candidate);
-                                }
-                                Err(e) => {
-                                    return Err(QtBuildError::QtVersionMajorInvalid {
-                                        qt_version_major_env_var: env_version,
-                                        source: e,
-                                    })
-                                }
-                                Ok(int) => int,
-                            };
-                            if env_version == qmake_version.major as u64 {
-                                return Ok(candidate);
-                            } else {
-                                return Err(QtBuildError::QtVersionMajorDoesNotMatch {
-                                    qmake_version: qmake_version.major as u64,
-                                    qt_version_major: env_version,
-                                });
-                            }
-                        }
-                        Ok(candidate)
-                    } else {
-                        Err(QtBuildError::QtMissing)
-                    }
-                }
-            }
-        }
-
-        if let Ok(qmake_env_var) = env::var("QMAKE") {
-            match verify_candidate(qmake_env_var.trim()) {
-                Ok(executable_name) => {
-                    return Ok(Self {
-                        qt_installation,
-                        qmake_executable: executable_name.to_string(),
-                        qt_modules,
-                    });
-                }
-                Err(e) => {
-                    return Err(QtBuildError::QMakeSetQtMissing {
-                        qmake_env_var,
-                        error: Box::new(e.into()),
-                    }
-                    .into())
-                }
-            }
-        }
-
-        // Fedora 36 renames Qt5's qmake to qmake-qt5
-        let candidate_executable_names = ["qmake6", "qmake-qt5", "qmake"];
-        for (index, executable_name) in candidate_executable_names.iter().enumerate() {
-            match verify_candidate(executable_name) {
-                Ok(executable_name) => {
-                    return Ok(Self {
-                        qt_installation,
-                        qmake_executable: executable_name.to_string(),
-                        qt_modules,
-                    });
-                }
-                // If QT_VERSION_MAJOR is specified, it is expected that one of the versioned
-                // executable names will not match, so the unversioned `qmake` needs to be
-                // attempted last and QtVersionMajorDoesNotMatch should only be returned if
-                // none of the candidate executable names match.
-                Err(QtBuildError::QtVersionMajorDoesNotMatch {
-                    qmake_version,
-                    qt_version_major,
-                }) => {
-                    if index == candidate_executable_names.len() - 1 {
-                        return Err(QtBuildError::QtVersionMajorDoesNotMatch {
-                            qmake_version,
-                            qt_version_major,
-                        }
-                        .into());
-                    }
-                    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]);
-                    continue;
-                }
-                Err(QtBuildError::QtMissing) => continue,
-                Err(e) => return Err(e.into()),
-            }
-        }
-
-        Err(QtBuildError::QtMissing.into())
-    }
-
-    /// Get the output of running `qmake -query var_name`
-    pub fn qmake_query(&self, var_name: &str) -> String {
-        std::str::from_utf8(
-            &Command::new(&self.qmake_executable)
-                .args(["-query", var_name])
-                .output()
-                .unwrap()
-                .stdout,
-        )
-        .unwrap()
-        .trim()
-        .to_string()
+        Ok(Self {
+            qt_installation,
+            qt_modules,
+        })
     }
 
     /// Tell Cargo to link each Qt module.

From 84e18cdc2a355b04b10fed3b38d7c6f0ddf765f0 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 16:09:19 +0100
Subject: [PATCH 13/27] qt-build-utils: parse_cflags is now only needed for
 qmake builds

---
 crates/qt-build-utils/src/lib.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index e3013852b..5c174f315 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -21,6 +21,7 @@ pub use installation::QtInstallation;
 #[cfg(feature = "qmake")]
 pub use installation::qmake::QtInstallationQMake;
 
+#[cfg(feature = "qmake")]
 mod parse_cflags;
 
 mod tool;

From 8f48c03fdee5d11427fbad1df3ae4413ed382853 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 22 Apr 2025 17:49:01 +0100
Subject: [PATCH 14/27] qt-build-utils: fix docs and add new method with
 QtInstallation arg

---
 crates/cxx-qt-build/src/lib.rs                |  6 +-
 crates/cxx-qt-lib/build.rs                    |  2 +-
 .../qt-build-utils/src/installation/qmake.rs  | 36 +++++++++-
 crates/qt-build-utils/src/lib.rs              | 65 ++++++-------------
 tests/qt_types_standalone/rust/build.rs       |  2 +-
 5 files changed, 59 insertions(+), 52 deletions(-)

diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index 19220880e..75b65baeb 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -1122,8 +1122,10 @@ extern "C" bool {init_fun}() {{
 
         let header_root = dir::header_root();
 
-        let mut qtbuild = qt_build_utils::QtBuild::new(qt_modules.iter().cloned().collect())
-            .expect("Could not find Qt installation");
+        let mut qtbuild = qt_build_utils::QtBuild::new_with_default_installation(
+            qt_modules.iter().cloned().collect(),
+        )
+        .expect("Could not find Qt installation");
         qtbuild.cargo_link_libraries(&mut self.cc_builder);
         Self::define_qt_version_cfg_variables(qtbuild.version());
 
diff --git a/crates/cxx-qt-lib/build.rs b/crates/cxx-qt-lib/build.rs
index e132c51fc..5a06120ed 100644
--- a/crates/cxx-qt-lib/build.rs
+++ b/crates/cxx-qt-lib/build.rs
@@ -99,7 +99,7 @@ fn write_headers() {
 }
 
 fn main() {
-    let qtbuild = qt_build_utils::QtBuild::new(vec!["Core".to_owned()])
+    let qtbuild = qt_build_utils::QtBuild::new_with_default_installation(vec!["Core".to_owned()])
         .expect("Could not find Qt installation");
 
     write_headers();
diff --git a/crates/qt-build-utils/src/installation/qmake.rs b/crates/qt-build-utils/src/installation/qmake.rs
index bedc8248f..825b7aa61 100644
--- a/crates/qt-build-utils/src/installation/qmake.rs
+++ b/crates/qt-build-utils/src/installation/qmake.rs
@@ -13,14 +13,46 @@ use std::{
 
 use crate::{parse_cflags, utils, QtBuildError, QtInstallation, QtTool};
 
-/// TODO
+/// A implementation of [QtInstallation] using qmake
 pub struct QtInstallationQMake {
     qmake_path: PathBuf,
     qmake_version: Version,
 }
 
 impl QtInstallationQMake {
-    /// TODO
+    /// The directories specified by the `PATH` environment variable are where qmake is
+    /// searched for. Alternatively, the `QMAKE` environment variable may be set to specify
+    /// an explicit path to qmake.
+    ///
+    /// If multiple major versions (for example, `5` and `6`) of Qt could be installed, set
+    /// the `QT_VERSION_MAJOR` environment variable to force which one to use. When using Cargo
+    /// as the build system for the whole build, prefer using `QT_VERSION_MAJOR` over the `QMAKE`
+    /// environment variable because it will account for different names for the qmake executable
+    /// that some Linux distributions use.
+    ///
+    /// However, when building a Rust staticlib that gets linked to C++ code by a C++ build
+    /// system, it is best to use the `QMAKE` environment variable to ensure that the Rust
+    /// staticlib is linked to the same installation of Qt that the C++ build system has
+    /// detected.
+    /// With CMake, this will automatically be set up for you when using cxxqt_import_crate.
+    ///
+    /// Alternatively, you can get this from the `Qt::qmake` target's `IMPORTED_LOCATION`
+    /// property, for example:
+    /// ```cmake
+    /// find_package(Qt6 COMPONENTS Core)
+    /// if(NOT Qt6_FOUND)
+    ///     find_package(Qt5 5.15 COMPONENTS Core REQUIRED)
+    /// endif()
+    /// get_target_property(QMAKE Qt::qmake IMPORTED_LOCATION)
+    ///
+    /// execute_process(
+    ///     COMMAND cmake -E env
+    ///         "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}/cargo"
+    ///         "QMAKE=${QMAKE}"
+    ///         cargo build
+    ///     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+    /// )
+    /// ```
     pub fn new() -> anyhow::Result<Self> {
         // Try the QMAKE variable first
         println!("cargo::rerun-if-env-changed=QMAKE");
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 5c174f315..bc0fa54b0 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -228,7 +228,7 @@ pub struct QmlModuleRegistrationFiles {
 ///     .iter()
 ///     .map(|m| String::from(*m))
 ///     .collect();
-/// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
+/// let qtbuild = qt_build_utils::QtBuild::new_with_default_installation(qt_modules).expect("Could not find Qt installation");
 /// ```
 pub struct QtBuild {
     qt_installation: Box<dyn QtInstallation>,
@@ -236,58 +236,31 @@ pub struct QtBuild {
 }
 
 impl QtBuild {
-    /// Search for where Qt is installed using qmake. Specify the Qt modules you are
-    /// linking with the `qt_modules` parameter, ommitting the `Qt` prefix (`"Core"`
-    /// rather than `"QtCore"`). After construction, use the [QtBuild::qmake_query]
-    /// method to get information about the Qt installation.
-    ///
-    /// The directories specified by the `PATH` environment variable are where qmake is
-    /// searched for. Alternatively, the `QMAKE` environment variable may be set to specify
-    /// an explicit path to qmake.
-    ///
-    /// If multiple major versions (for example, `5` and `6`) of Qt could be installed, set
-    /// the `QT_VERSION_MAJOR` environment variable to force which one to use. When using Cargo
-    /// as the build system for the whole build, prefer using `QT_VERSION_MAJOR` over the `QMAKE`
-    /// environment variable because it will account for different names for the qmake executable
-    /// that some Linux distributions use.
-    ///
-    /// However, when building a Rust staticlib that gets linked to C++ code by a C++ build
-    /// system, it is best to use the `QMAKE` environment variable to ensure that the Rust
-    /// staticlib is linked to the same installation of Qt that the C++ build system has
-    /// detected.
-    /// With CMake, this will automatically be set up for you when using cxxqt_import_crate.
-    ///
-    /// Alternatively, you can get this from the `Qt::qmake` target's `IMPORTED_LOCATION`
-    /// property, for example:
-    /// ```cmake
-    /// find_package(Qt6 COMPONENTS Core)
-    /// if(NOT Qt6_FOUND)
-    ///     find_package(Qt5 5.15 COMPONENTS Core REQUIRED)
-    /// endif()
-    /// get_target_property(QMAKE Qt::qmake IMPORTED_LOCATION)
-    ///
-    /// execute_process(
-    ///     COMMAND cmake -E env
-    ///         "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}/cargo"
-    ///         "QMAKE=${QMAKE}"
-    ///         cargo build
-    ///     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-    /// )
-    /// ```
-    pub fn new(mut qt_modules: Vec<String>) -> anyhow::Result<Self> {
-        if qt_modules.is_empty() {
-            qt_modules.push("Core".to_string());
-        }
-
+    /// Create a [QtBuild] using the default [QtInstallation] (currently uses [QtInstallationQMake])
+    /// and specify which Qt modules you are linking, ommitting the `Qt` prefix (`"Core"`
+    /// rather than `"QtCore"`).
+    //
+    // TODO: is there a better name for this method or a sane way to create a default QtInstallation?
+    pub fn new_with_default_installation(qt_modules: Vec<String>) -> anyhow::Result<Self> {
         #[cfg(feature = "qmake")]
         let qt_installation = Box::new(QtInstallationQMake::new()?);
         #[cfg(not(feature = "qmake"))]
         unsupported!("Only qmake feature is supported");
 
-        Ok(Self {
+        Ok(Self::new(qt_installation, qt_modules))
+    }
+
+    /// Create a [QtBuild] using the given [QtInstallation] and specify which
+    /// Qt modules you are linking, ommitting the `Qt` prefix (`"Core"` rather than `"QtCore"`).
+    pub fn new(qt_installation: Box<dyn QtInstallation>, mut qt_modules: Vec<String>) -> Self {
+        if qt_modules.is_empty() {
+            qt_modules.push("Core".to_string());
+        }
+
+        Self {
             qt_installation,
             qt_modules,
-        })
+        }
     }
 
     /// Tell Cargo to link each Qt module.
diff --git a/tests/qt_types_standalone/rust/build.rs b/tests/qt_types_standalone/rust/build.rs
index 51f6df496..72993b6ed 100644
--- a/tests/qt_types_standalone/rust/build.rs
+++ b/tests/qt_types_standalone/rust/build.rs
@@ -6,7 +6,7 @@
 use cxx_qt_build::CxxQtBuilder;
 
 fn main() {
-    let qtbuild = qt_build_utils::QtBuild::new(vec!["Core".to_owned()])
+    let qtbuild = qt_build_utils::QtBuild::new_with_default_installation(vec!["Core".to_owned()])
         .expect("Could not find Qt installation");
 
     let mut builder = CxxQtBuilder::new()

From 3b1b3f3668449b83d6bcc89d74317c9a78f4c3b9 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 24 Apr 2025 10:22:59 +0100
Subject: [PATCH 15/27] qt-build-utils: add back a cache for finding the Qt
 tool

---
 .../qt-build-utils/src/installation/qmake.rs   | 18 +++++++++++++++++-
 crates/qt-build-utils/src/tool.rs              |  1 +
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/crates/qt-build-utils/src/installation/qmake.rs b/crates/qt-build-utils/src/installation/qmake.rs
index 825b7aa61..5b3820792 100644
--- a/crates/qt-build-utils/src/installation/qmake.rs
+++ b/crates/qt-build-utils/src/installation/qmake.rs
@@ -5,6 +5,8 @@
 
 use semver::Version;
 use std::{
+    cell::RefCell,
+    collections::HashMap,
     env,
     io::ErrorKind,
     path::{Path, PathBuf},
@@ -17,6 +19,8 @@ use crate::{parse_cflags, utils, QtBuildError, QtInstallation, QtTool};
 pub struct QtInstallationQMake {
     qmake_path: PathBuf,
     qmake_version: Version,
+    // Internal cache of paths for tools
+    tool_cache: RefCell<HashMap<QtTool, Option<PathBuf>>>,
 }
 
 impl QtInstallationQMake {
@@ -128,6 +132,7 @@ impl TryFrom<PathBuf> for QtInstallationQMake {
         Ok(Self {
             qmake_path,
             qmake_version,
+            tool_cache: HashMap::default().into(),
         })
     }
 }
@@ -246,7 +251,18 @@ impl QtInstallation for QtInstallationQMake {
     }
 
     fn try_find_tool(&self, tool: QtTool) -> Option<PathBuf> {
-        self.try_qmake_find_tool(tool.binary_name())
+        let find_tool_closure = |tool: &QtTool| self.try_qmake_find_tool(tool.binary_name());
+
+        // Attempt to use the cache
+        if let Ok(mut tool_cache) = self.tool_cache.try_borrow_mut() {
+            // Read the tool from the cache or insert
+            tool_cache
+                .entry(tool)
+                .or_insert_with_key(find_tool_closure)
+                .to_owned()
+        } else {
+            find_tool_closure(&tool)
+        }
     }
 
     fn version(&self) -> semver::Version {
diff --git a/crates/qt-build-utils/src/tool.rs b/crates/qt-build-utils/src/tool.rs
index ce6adf0ac..94e7116e7 100644
--- a/crates/qt-build-utils/src/tool.rs
+++ b/crates/qt-build-utils/src/tool.rs
@@ -5,6 +5,7 @@
 
 /// An enum representing known Qt tools
 #[non_exhaustive]
+#[derive(Eq, Hash, PartialEq)]
 pub enum QtTool {
     /// Moc
     Moc,

From 025ae244caf0ba7989309bd99c3f452103040c4f Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 1 May 2025 13:52:16 +0100
Subject: [PATCH 16/27] qt-build-utils: split error into separate module

---
 crates/qt-build-utils/src/error.rs | 41 ++++++++++++++++++++++++++++++
 crates/qt-build-utils/src/lib.rs   | 39 +++-------------------------
 2 files changed, 44 insertions(+), 36 deletions(-)
 create mode 100644 crates/qt-build-utils/src/error.rs

diff --git a/crates/qt-build-utils/src/error.rs b/crates/qt-build-utils/src/error.rs
new file mode 100644
index 000000000..fe48813d1
--- /dev/null
+++ b/crates/qt-build-utils/src/error.rs
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+/// Errors that can occur while using [crate::QtBuild]
+pub enum QtBuildError {
+    /// `QMAKE` environment variable was set but Qt was not detected
+    #[error("QMAKE environment variable specified as {qmake_env_var} but could not detect Qt: {error:?}")]
+    QMakeSetQtMissing {
+        /// The value of the qmake environment variable when the error occurred
+        qmake_env_var: String,
+        /// The inner error that occurred
+        error: Box<anyhow::Error>,
+    },
+    /// Qt was not found
+    #[error("Could not find Qt")]
+    QtMissing,
+    /// Executing `qmake -query` failed
+    #[error("Executing `qmake -query` failed: {0:?}")]
+    QmakeFailed(#[from] std::io::Error),
+    /// `QT_VERSION_MAJOR` environment variable was specified but could not be parsed as an integer
+    #[error("QT_VERSION_MAJOR environment variable specified as {qt_version_major_env_var} but could not parse as integer: {source:?}")]
+    QtVersionMajorInvalid {
+        /// The Qt major version from `QT_VERSION_MAJOR`
+        qt_version_major_env_var: String,
+        /// The [std::num::ParseIntError] when parsing the `QT_VERSION_MAJOR`
+        source: std::num::ParseIntError,
+    },
+    /// `QT_VERSION_MAJOR` environment variable was specified but the Qt version specified by `qmake -query QT_VERSION` did not match
+    #[error("qmake version ({qmake_version}) does not match version specified by QT_VERSION_MAJOR ({qt_version_major})")]
+    QtVersionMajorDoesNotMatch {
+        /// The qmake version
+        qmake_version: u64,
+        /// The Qt major version from `QT_VERSION_MAJOR`
+        qt_version_major: u64,
+    },
+}
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index bc0fa54b0..1b7b0760f 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -15,6 +15,9 @@
 
 #![allow(clippy::too_many_arguments)]
 
+mod error;
+pub use error::QtBuildError;
+
 mod installation;
 pub use installation::QtInstallation;
 
@@ -38,42 +41,6 @@ use std::{
 };
 
 use semver::Version;
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-/// Errors that can occur while using [QtBuild]
-pub enum QtBuildError {
-    /// `QMAKE` environment variable was set but Qt was not detected
-    #[error("QMAKE environment variable specified as {qmake_env_var} but could not detect Qt: {error:?}")]
-    QMakeSetQtMissing {
-        /// The value of the qmake environment variable when the error occurred
-        qmake_env_var: String,
-        /// The inner error that occurred
-        error: Box<anyhow::Error>,
-    },
-    /// Qt was not found
-    #[error("Could not find Qt")]
-    QtMissing,
-    /// Executing `qmake -query` failed
-    #[error("Executing `qmake -query` failed: {0:?}")]
-    QmakeFailed(#[from] std::io::Error),
-    /// `QT_VERSION_MAJOR` environment variable was specified but could not be parsed as an integer
-    #[error("QT_VERSION_MAJOR environment variable specified as {qt_version_major_env_var} but could not parse as integer: {source:?}")]
-    QtVersionMajorInvalid {
-        /// The Qt major version from `QT_VERSION_MAJOR`
-        qt_version_major_env_var: String,
-        /// The [std::num::ParseIntError] when parsing the `QT_VERSION_MAJOR`
-        source: std::num::ParseIntError,
-    },
-    /// `QT_VERSION_MAJOR` environment variable was specified but the Qt version specified by `qmake -query QT_VERSION` did not match
-    #[error("qmake version ({qmake_version}) does not match version specified by QT_VERSION_MAJOR ({qt_version_major})")]
-    QtVersionMajorDoesNotMatch {
-        /// The qmake version
-        qmake_version: u64,
-        /// The Qt major version from `QT_VERSION_MAJOR`
-        qt_version_major: u64,
-    },
-}
 
 fn command_help_output(command: &str) -> std::io::Result<std::process::Output> {
     Command::new(command).args(["--help"]).output()

From 42599e24f7ba20fa8cbdeb666d64277301f812c1 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 1 May 2025 14:14:54 +0100
Subject: [PATCH 17/27] qt-build-utils: split initialiser into module

---
 crates/qt-build-utils/src/initializer.rs | 35 ++++++++++++++++++++++++
 crates/qt-build-utils/src/lib.rs         | 32 ++--------------------
 2 files changed, 38 insertions(+), 29 deletions(-)
 create mode 100644 crates/qt-build-utils/src/initializer.rs

diff --git a/crates/qt-build-utils/src/initializer.rs b/crates/qt-build-utils/src/initializer.rs
new file mode 100644
index 000000000..b336eb772
--- /dev/null
+++ b/crates/qt-build-utils/src/initializer.rs
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use std::path::PathBuf;
+
+#[doc(hidden)]
+#[derive(Clone)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct Initializer {
+    pub file: Option<PathBuf>,
+    pub init_call: Option<String>,
+    pub init_declaration: Option<String>,
+}
+
+impl Initializer {
+    #[doc(hidden)]
+    pub fn default_signature(name: &str) -> Self {
+        Self {
+            file: None,
+            init_call: Some(format!("{name}();")),
+            init_declaration: Some(format!("extern \"C\" bool {name}();")),
+        }
+    }
+
+    #[doc(hidden)]
+    // Strip the init files from the public initializers
+    // For downstream dependencies, it's often enough to just declare the init function and
+    // call it.
+    pub fn strip_file(mut self) -> Self {
+        self.file = None;
+        self
+    }
+}
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 1b7b0760f..e69cf890f 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -18,6 +18,9 @@
 mod error;
 pub use error::QtBuildError;
 
+mod initializer;
+pub use initializer::Initializer;
+
 mod installation;
 pub use installation::QtInstallation;
 
@@ -106,35 +109,6 @@ pub fn setup_linker() {
     }
 }
 
-#[doc(hidden)]
-#[derive(Clone)]
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-pub struct Initializer {
-    pub file: Option<PathBuf>,
-    pub init_call: Option<String>,
-    pub init_declaration: Option<String>,
-}
-
-impl Initializer {
-    #[doc(hidden)]
-    pub fn default_signature(name: &str) -> Self {
-        Self {
-            file: None,
-            init_call: Some(format!("{name}();")),
-            init_declaration: Some(format!("extern \"C\" bool {name}();")),
-        }
-    }
-
-    #[doc(hidden)]
-    // Strip the init files from the public initializers
-    // For downstream dependencies, it's often enough to just declare the init function and
-    // call it.
-    pub fn strip_file(mut self) -> Self {
-        self.file = None;
-        self
-    }
-}
-
 /// Paths to files generated by [QtBuild::moc]
 pub struct MocProducts {
     /// Generated C++ file

From a60136719d1ae9284ce327fbded9483384570b6a Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 1 May 2025 14:16:05 +0100
Subject: [PATCH 18/27] qt-build-utils: move tool to a folder

---
 crates/qt-build-utils/src/{tool.rs => tool/mod.rs} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename crates/qt-build-utils/src/{tool.rs => tool/mod.rs} (100%)

diff --git a/crates/qt-build-utils/src/tool.rs b/crates/qt-build-utils/src/tool/mod.rs
similarity index 100%
rename from crates/qt-build-utils/src/tool.rs
rename to crates/qt-build-utils/src/tool/mod.rs

From 481c7f4926e922dea8ccebc24b6fb3406315381c Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 1 May 2025 14:40:27 +0100
Subject: [PATCH 19/27] qt-build-utils: split out rcc into a tool

---
 crates/cxx-qt-build/src/lib.rs        |   4 +-
 crates/qt-build-utils/src/lib.rs      |  94 ++++-----------------
 crates/qt-build-utils/src/tool/mod.rs |   3 +
 crates/qt-build-utils/src/tool/rcc.rs | 114 ++++++++++++++++++++++++++
 4 files changed, 135 insertions(+), 80 deletions(-)
 create mode 100644 crates/qt-build-utils/src/tool/rcc.rs

diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index 75b65baeb..e39feaced 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -1030,12 +1030,12 @@ extern "C" bool {init_fun}() {{
             .iter()
             .map(|qrc_file| {
                 // Also ensure that each of the files in the qrc can cause a change
-                for qrc_inner_file in qtbuild.qrc_list(&qrc_file) {
+                for qrc_inner_file in qtbuild.qrc_list(qrc_file) {
                     println!("cargo::rerun-if-changed={}", qrc_inner_file.display());
                 }
                 // We need to link this using an object file or +whole-achive, the static initializer of
                 // the qrc file isn't lost.
-                qtbuild.qrc(&qrc_file)
+                qtbuild.qrc(qrc_file)
             })
             .collect()
     }
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index e69cf890f..00bff2a56 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -31,7 +31,7 @@ pub use installation::qmake::QtInstallationQMake;
 mod parse_cflags;
 
 mod tool;
-pub use tool::QtTool;
+pub use tool::{QtTool, QtToolRcc};
 
 mod utils;
 
@@ -609,6 +609,13 @@ Q_IMPORT_PLUGIN({plugin_class_name});
         }
     }
 
+    /// Create a [QtToolRcc] for this [QtBuild]
+    ///
+    /// This allows for using [rcc](https://doc.qt.io/qt-6/resources.html)
+    pub fn rcc(&self) -> QtToolRcc {
+        QtToolRcc::new(self.qt_installation.as_ref())
+    }
+
     /// 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).
     /// 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).
     /// This function also returns a String that contains the name of the resource initializer
@@ -616,85 +623,16 @@ Q_IMPORT_PLUGIN({plugin_class_name});
     /// The build system must ensure that if the .cpp file is built into a static library, either
     /// the `+whole-archive` flag is used, or the initializer function is called by the
     /// application.
-    pub fn qrc(&mut self, input_file: &impl AsRef<Path>) -> Initializer {
-        let rcc_executable = self
-            .qt_installation
-            .try_find_tool(QtTool::Rcc)
-            .expect("Could not find rcc");
-        let input_path = input_file.as_ref();
-        let output_folder = PathBuf::from(&format!(
-            "{}/qt-build-utils/qrc",
-            env::var("OUT_DIR").unwrap()
-        ));
-        std::fs::create_dir_all(&output_folder).expect("Could not create qrc dir");
-        let output_path = output_folder.join(format!(
-            "{}.cpp",
-            input_path.file_name().unwrap().to_string_lossy(),
-        ));
-        let name = input_path
-            .file_name()
-            .unwrap()
-            .to_string_lossy()
-            .replace('.', "_");
-
-        let cmd = Command::new(rcc_executable)
-            .args([
-                input_path.to_str().unwrap(),
-                "-o",
-                output_path.to_str().unwrap(),
-                "--name",
-                &name,
-            ])
-            .output()
-            .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));
-
-        if !cmd.status.success() {
-            panic!(
-                "rcc failed for {}:\n{}",
-                input_path.display(),
-                String::from_utf8_lossy(&cmd.stderr)
-            );
-        }
-
-        let qt_6_5 = Version::new(6, 5, 0);
-        let init_header = if self.qt_installation.version() >= qt_6_5 {
-            // With Qt6.5 the Q_INIT_RESOURCE macro is in the QtResource header
-            "QtCore/QtResource"
-        } else {
-            "QtCore/QDir"
-        };
-        Initializer {
-            file: Some(output_path),
-            init_call: Some(format!("Q_INIT_RESOURCE({name});")),
-            init_declaration: Some(format!("#include <{init_header}>")),
-        }
+    pub fn qrc(&mut self, input_file: impl AsRef<Path>) -> Initializer {
+        // TODO: later change to just have a rcc() -> QtToolRcc
+        // but keep this for compat for now
+        QtToolRcc::new(self.qt_installation.as_ref()).compile(input_file)
     }
 
     /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources
-    pub fn qrc_list(&mut self, input_file: &impl AsRef<Path>) -> Vec<PathBuf> {
-        let rcc_executable = self
-            .qt_installation
-            .try_find_tool(QtTool::Rcc)
-            .expect("Could not find rcc");
-
-        // Add the qrc file contents to the cargo rerun list
-        let input_path = input_file.as_ref();
-        let cmd_list = Command::new(rcc_executable)
-            .args(["--list", input_path.to_str().unwrap()])
-            .output()
-            .unwrap_or_else(|_| panic!("rcc --list failed for {}", input_path.display()));
-
-        if !cmd_list.status.success() {
-            panic!(
-                "rcc --list failed for {}:\n{}",
-                input_path.display(),
-                String::from_utf8_lossy(&cmd_list.stderr)
-            );
-        }
-
-        String::from_utf8_lossy(&cmd_list.stdout)
-            .split('\n')
-            .map(PathBuf::from)
-            .collect()
+    pub fn qrc_list(&mut self, input_file: impl AsRef<Path>) -> Vec<PathBuf> {
+        // TODO: later change to just have a rcc() -> QtToolRcc
+        // but keep this for compat for now
+        QtToolRcc::new(self.qt_installation.as_ref()).list(input_file)
     }
 }
diff --git a/crates/qt-build-utils/src/tool/mod.rs b/crates/qt-build-utils/src/tool/mod.rs
index 94e7116e7..7726a485c 100644
--- a/crates/qt-build-utils/src/tool/mod.rs
+++ b/crates/qt-build-utils/src/tool/mod.rs
@@ -3,6 +3,9 @@
 //
 // SPDX-License-Identifier: MIT OR Apache-2.0
 
+mod rcc;
+pub use rcc::QtToolRcc;
+
 /// An enum representing known Qt tools
 #[non_exhaustive]
 #[derive(Eq, Hash, PartialEq)]
diff --git a/crates/qt-build-utils/src/tool/rcc.rs b/crates/qt-build-utils/src/tool/rcc.rs
new file mode 100644
index 000000000..ae45c49bd
--- /dev/null
+++ b/crates/qt-build-utils/src/tool/rcc.rs
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{Initializer, QtInstallation, QtTool};
+
+use semver::Version;
+use std::{
+    env,
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+/// A wrapper around the [rcc](https://doc.qt.io/qt-6/resources.html) tool
+pub struct QtToolRcc {
+    executable: PathBuf,
+    qt_version: Version,
+}
+
+impl QtToolRcc {
+    /// Construct a [QtToolRcc] from a given [QtInstallation]
+    pub fn new(qt_installation: &dyn QtInstallation) -> Self {
+        let executable = qt_installation
+            .try_find_tool(QtTool::Rcc)
+            .expect("Could not find rcc");
+        let qt_version = qt_installation.version();
+
+        Self {
+            executable,
+            qt_version,
+        }
+    }
+
+    /// 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).
+    /// 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).
+    /// This function also returns a String that contains the name of the resource initializer
+    /// function.
+    /// The build system must ensure that if the .cpp file is built into a static library, either
+    /// the `+whole-archive` flag is used, or the initializer function is called by the
+    /// application.
+    pub fn compile(&self, input_file: impl AsRef<Path>) -> Initializer {
+        let input_path = input_file.as_ref();
+        let output_folder = PathBuf::from(&format!(
+            "{}/qt-build-utils/qrc",
+            env::var("OUT_DIR").unwrap()
+        ));
+        std::fs::create_dir_all(&output_folder).expect("Could not create qrc dir");
+        let output_path = output_folder.join(format!(
+            "{}.cpp",
+            input_path.file_name().unwrap().to_string_lossy(),
+        ));
+        let name = input_path
+            .file_name()
+            .unwrap()
+            .to_string_lossy()
+            .replace('.', "_");
+
+        let cmd = Command::new(&self.executable)
+            .args([
+                input_path.to_str().unwrap(),
+                "-o",
+                output_path.to_str().unwrap(),
+                "--name",
+                &name,
+            ])
+            .output()
+            .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));
+
+        if !cmd.status.success() {
+            panic!(
+                "rcc failed for {}:\n{}",
+                input_path.display(),
+                String::from_utf8_lossy(&cmd.stderr)
+            );
+        }
+
+        let qt_6_5 = Version::new(6, 5, 0);
+        let init_header = if self.qt_version >= qt_6_5 {
+            // With Qt6.5 the Q_INIT_RESOURCE macro is in the QtResource header
+            "QtCore/QtResource"
+        } else {
+            "QtCore/QDir"
+        };
+        Initializer {
+            file: Some(output_path),
+            init_call: Some(format!("Q_INIT_RESOURCE({name});")),
+            init_declaration: Some(format!("#include <{init_header}>")),
+        }
+    }
+
+    /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources
+    pub fn list(&self, input_file: impl AsRef<Path>) -> Vec<PathBuf> {
+        // Add the qrc file contents to the cargo rerun list
+        let input_path = input_file.as_ref();
+        let cmd_list = Command::new(&self.executable)
+            .args(["--list", input_path.to_str().unwrap()])
+            .output()
+            .unwrap_or_else(|_| panic!("rcc --list failed for {}", input_path.display()));
+
+        if !cmd_list.status.success() {
+            panic!(
+                "rcc --list failed for {}:\n{}",
+                input_path.display(),
+                String::from_utf8_lossy(&cmd_list.stderr)
+            );
+        }
+
+        String::from_utf8_lossy(&cmd_list.stdout)
+            .split('\n')
+            .map(PathBuf::from)
+            .collect()
+    }
+}

From bdb9e4c479b82b161d07df514543cd462e5a6300 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 1 May 2025 15:01:50 +0100
Subject: [PATCH 20/27] qt-build-utils: split Moc into a separate tool

---
 crates/qt-build-utils/src/lib.rs      |  98 +-------------------
 crates/qt-build-utils/src/tool/moc.rs | 126 ++++++++++++++++++++++++++
 crates/qt-build-utils/src/tool/mod.rs |   3 +
 3 files changed, 133 insertions(+), 94 deletions(-)
 create mode 100644 crates/qt-build-utils/src/tool/moc.rs

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 00bff2a56..7ce231040 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -31,7 +31,7 @@ pub use installation::qmake::QtInstallationQMake;
 mod parse_cflags;
 
 mod tool;
-pub use tool::{QtTool, QtToolRcc};
+pub use tool::{MocArguments, MocProducts, QtTool, QtToolMoc, QtToolRcc};
 
 mod utils;
 
@@ -109,42 +109,6 @@ pub fn setup_linker() {
     }
 }
 
-/// Paths to files generated by [QtBuild::moc]
-pub struct MocProducts {
-    /// Generated C++ file
-    pub cpp: PathBuf,
-    /// Generated JSON file
-    pub metatypes_json: PathBuf,
-}
-
-/// Arguments for a Qt moc invocation.
-/// See: [QtBuild::moc]
-#[derive(Default, Clone)]
-pub struct MocArguments {
-    uri: Option<String>,
-    include_paths: Vec<PathBuf>,
-}
-
-impl MocArguments {
-    /// Should be passed if the input_file is part of a QML module
-    pub fn uri(mut self, uri: String) -> Self {
-        self.uri = Some(uri);
-        self
-    }
-
-    /// Additional include path to pass to moc
-    pub fn include_path(mut self, include_path: PathBuf) -> Self {
-        self.include_paths.push(include_path);
-        self
-    }
-
-    /// Additional include paths to pass to moc.
-    pub fn include_paths(mut self, mut include_paths: Vec<PathBuf>) -> Self {
-        self.include_paths.append(&mut include_paths);
-        self
-    }
-}
-
 /// Paths to C++ files generated by [QtBuild::register_qml_module]
 pub struct QmlModuleRegistrationFiles {
     /// File generated by [rcc](https://doc.qt.io/qt-6/rcc.html) for the QML plugin. The compiled static library
@@ -223,64 +187,10 @@ impl QtBuild {
     /// 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).
     /// 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),
     /// as well as the path to the generated metatypes.json file, which can be passed to [register_qml_module](Self::register_qml_module).
-    ///
     pub fn moc(&mut self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
-        let moc_executable = self
-            .qt_installation
-            .try_find_tool(QtTool::Moc)
-            .expect("Could not find moc");
-        let input_path = input_file.as_ref();
-
-        // Put all the moc files into one place, this can then be added to the include path
-        let moc_dir = PathBuf::from(format!(
-            "{}/qt-build-utils/moc",
-            env::var("OUT_DIR").unwrap()
-        ));
-        std::fs::create_dir_all(&moc_dir).expect("Could not create moc dir");
-        let output_path = moc_dir.join(format!(
-            "moc_{}.cpp",
-            input_path.file_name().unwrap().to_str().unwrap()
-        ));
-
-        let metatypes_json_path = PathBuf::from(&format!("{}.json", output_path.display()));
-
-        let mut include_args = vec![];
-        // Qt includes
-        for include_path in self
-            .include_paths()
-            .iter()
-            .chain(arguments.include_paths.iter())
-        {
-            include_args.push(format!("-I{}", include_path.display()));
-        }
-
-        let mut cmd = Command::new(moc_executable);
-
-        if let Some(uri) = arguments.uri {
-            cmd.arg(format!("-Muri={uri}"));
-        }
-
-        cmd.args(include_args);
-        cmd.arg(input_path.to_str().unwrap())
-            .arg("-o")
-            .arg(output_path.to_str().unwrap())
-            .arg("--output-json");
-        let cmd = cmd
-            .output()
-            .unwrap_or_else(|_| panic!("moc failed for {}", input_path.display()));
-
-        if !cmd.status.success() {
-            panic!(
-                "moc failed for {}:\n{}",
-                input_path.display(),
-                String::from_utf8_lossy(&cmd.stderr)
-            );
-        }
-
-        MocProducts {
-            cpp: output_path,
-            metatypes_json: metatypes_json_path,
-        }
+        // TODO: do we change this API to just be moc() -> QtToolMoc ?
+        QtToolMoc::new(self.qt_installation.as_ref(), &self.qt_modules)
+            .compile(input_file, arguments)
     }
 
     /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
diff --git a/crates/qt-build-utils/src/tool/moc.rs b/crates/qt-build-utils/src/tool/moc.rs
new file mode 100644
index 000000000..632580ad7
--- /dev/null
+++ b/crates/qt-build-utils/src/tool/moc.rs
@@ -0,0 +1,126 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{QtInstallation, QtTool};
+
+use std::{
+    env,
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+/// Paths to files generated by [QtToolMoc::compile]
+pub struct MocProducts {
+    /// Generated C++ file
+    pub cpp: PathBuf,
+    /// Generated JSON file
+    pub metatypes_json: PathBuf,
+}
+
+/// Arguments for a Qt moc invocation.
+/// See: [QtToolMoc::compile]
+#[derive(Default, Clone)]
+pub struct MocArguments {
+    uri: Option<String>,
+    include_paths: Vec<PathBuf>,
+}
+
+impl MocArguments {
+    /// Should be passed if the input_file is part of a QML module
+    pub fn uri(mut self, uri: String) -> Self {
+        self.uri = Some(uri);
+        self
+    }
+
+    /// Additional include path to pass to moc
+    pub fn include_path(mut self, include_path: PathBuf) -> Self {
+        self.include_paths.push(include_path);
+        self
+    }
+
+    /// Additional include paths to pass to moc.
+    pub fn include_paths(mut self, mut include_paths: Vec<PathBuf>) -> Self {
+        self.include_paths.append(&mut include_paths);
+        self
+    }
+}
+
+/// A wrapper around the [moc](https://doc.qt.io/qt-6/moc.html) tool
+pub struct QtToolMoc {
+    executable: PathBuf,
+    qt_include_paths: Vec<PathBuf>,
+}
+
+impl QtToolMoc {
+    /// Construct a [QtToolMoc] from a given [QtInstallation]
+    pub fn new(qt_installation: &dyn QtInstallation, qt_modules: &[String]) -> Self {
+        let executable = qt_installation
+            .try_find_tool(QtTool::Moc)
+            .expect("Could not find moc");
+        let qt_include_paths = qt_installation.include_paths(qt_modules);
+
+        Self {
+            executable,
+            qt_include_paths,
+        }
+    }
+
+    /// 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).
+    /// 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),
+    /// as well as the path to the generated metatypes.json file, which can be used for QML modules.
+    pub fn compile(&self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
+        let input_path = input_file.as_ref();
+        // Put all the moc files into one place, this can then be added to the include path
+        let moc_dir = PathBuf::from(format!(
+            "{}/qt-build-utils/moc",
+            env::var("OUT_DIR").unwrap()
+        ));
+        std::fs::create_dir_all(&moc_dir).expect("Could not create moc dir");
+        let output_path = moc_dir.join(format!(
+            "moc_{}.cpp",
+            input_path.file_name().unwrap().to_str().unwrap()
+        ));
+
+        let metatypes_json_path = PathBuf::from(&format!("{}.json", output_path.display()));
+
+        let mut include_args = vec![];
+        // Qt includes
+        for include_path in self
+            .qt_include_paths
+            .iter()
+            .chain(arguments.include_paths.iter())
+        {
+            include_args.push(format!("-I{}", include_path.display()));
+        }
+
+        let mut cmd = Command::new(&self.executable);
+
+        if let Some(uri) = arguments.uri {
+            cmd.arg(format!("-Muri={uri}"));
+        }
+
+        cmd.args(include_args);
+        cmd.arg(input_path.to_str().unwrap())
+            .arg("-o")
+            .arg(output_path.to_str().unwrap())
+            .arg("--output-json");
+        let cmd = cmd
+            .output()
+            .unwrap_or_else(|_| panic!("moc failed for {}", input_path.display()));
+
+        if !cmd.status.success() {
+            panic!(
+                "moc failed for {}:\n{}",
+                input_path.display(),
+                String::from_utf8_lossy(&cmd.stderr)
+            );
+        }
+
+        MocProducts {
+            cpp: output_path,
+            metatypes_json: metatypes_json_path,
+        }
+    }
+}
diff --git a/crates/qt-build-utils/src/tool/mod.rs b/crates/qt-build-utils/src/tool/mod.rs
index 7726a485c..965b927f9 100644
--- a/crates/qt-build-utils/src/tool/mod.rs
+++ b/crates/qt-build-utils/src/tool/mod.rs
@@ -3,6 +3,9 @@
 //
 // SPDX-License-Identifier: MIT OR Apache-2.0
 
+mod moc;
+pub use moc::{MocArguments, MocProducts, QtToolMoc};
+
 mod rcc;
 pub use rcc::QtToolRcc;
 

From 4b1873fa157e58b2d7c7162c751f2ac25596d20b Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Thu, 1 May 2025 15:08:21 +0100
Subject: [PATCH 21/27] qt-build-utils: have a command writable path method for
 tools

---
 crates/qt-build-utils/src/tool/moc.rs | 6 +-----
 crates/qt-build-utils/src/tool/mod.rs | 9 +++++++++
 crates/qt-build-utils/src/tool/rcc.rs | 6 +-----
 3 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/crates/qt-build-utils/src/tool/moc.rs b/crates/qt-build-utils/src/tool/moc.rs
index 632580ad7..fadf6152b 100644
--- a/crates/qt-build-utils/src/tool/moc.rs
+++ b/crates/qt-build-utils/src/tool/moc.rs
@@ -6,7 +6,6 @@
 use crate::{QtInstallation, QtTool};
 
 use std::{
-    env,
     path::{Path, PathBuf},
     process::Command,
 };
@@ -73,10 +72,7 @@ impl QtToolMoc {
     pub fn compile(&self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
         let input_path = input_file.as_ref();
         // Put all the moc files into one place, this can then be added to the include path
-        let moc_dir = PathBuf::from(format!(
-            "{}/qt-build-utils/moc",
-            env::var("OUT_DIR").unwrap()
-        ));
+        let moc_dir = QtTool::Moc.writable_path();
         std::fs::create_dir_all(&moc_dir).expect("Could not create moc dir");
         let output_path = moc_dir.join(format!(
             "moc_{}.cpp",
diff --git a/crates/qt-build-utils/src/tool/mod.rs b/crates/qt-build-utils/src/tool/mod.rs
index 965b927f9..1495ce17d 100644
--- a/crates/qt-build-utils/src/tool/mod.rs
+++ b/crates/qt-build-utils/src/tool/mod.rs
@@ -3,6 +3,8 @@
 //
 // SPDX-License-Identifier: MIT OR Apache-2.0
 
+use std::{env, path::PathBuf};
+
 mod moc;
 pub use moc::{MocArguments, MocProducts, QtToolMoc};
 
@@ -33,4 +35,11 @@ impl QtTool {
             Self::QmlTypeRegistrar => "qmltyperegistrar",
         }
     }
+
+    /// Return a directory where files can be written by this tool
+    ///
+    /// Note the location might not exist yet
+    pub(crate) fn writable_path(&self) -> PathBuf {
+        PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR was not set")).join(self.binary_name())
+    }
 }
diff --git a/crates/qt-build-utils/src/tool/rcc.rs b/crates/qt-build-utils/src/tool/rcc.rs
index ae45c49bd..223e15ea1 100644
--- a/crates/qt-build-utils/src/tool/rcc.rs
+++ b/crates/qt-build-utils/src/tool/rcc.rs
@@ -7,7 +7,6 @@ use crate::{Initializer, QtInstallation, QtTool};
 
 use semver::Version;
 use std::{
-    env,
     path::{Path, PathBuf},
     process::Command,
 };
@@ -41,10 +40,7 @@ impl QtToolRcc {
     /// application.
     pub fn compile(&self, input_file: impl AsRef<Path>) -> Initializer {
         let input_path = input_file.as_ref();
-        let output_folder = PathBuf::from(&format!(
-            "{}/qt-build-utils/qrc",
-            env::var("OUT_DIR").unwrap()
-        ));
+        let output_folder = QtTool::Rcc.writable_path();
         std::fs::create_dir_all(&output_folder).expect("Could not create qrc dir");
         let output_path = output_folder.join(format!(
             "{}.cpp",

From ec39afe0c3cd04b50fecce432c599cdcbfb6479e Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Mon, 5 May 2025 11:41:16 +0100
Subject: [PATCH 22/27] qt-build-utils: split out qmltyperegistrar into a
 separate tool

---
 crates/cxx-qt-build/src/lib.rs                |  4 +-
 crates/qt-build-utils/src/lib.rs              | 66 ++++----------
 crates/qt-build-utils/src/tool/mod.rs         |  3 +
 .../src/tool/qmltyperegistrar.rs              | 88 +++++++++++++++++++
 4 files changed, 109 insertions(+), 52 deletions(-)
 create mode 100644 crates/qt-build-utils/src/tool/qmltyperegistrar.rs

diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index e39feaced..09db9e176 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -852,8 +852,10 @@ impl CxxQtBuilder {
                 &qml_module.qml_files,
                 &qml_module.qrc_files,
             );
+            if let Some(qmltyperegistrar) = qml_module_registration_files.qmltyperegistrar {
+                cc_builder.file(qmltyperegistrar);
+            }
             cc_builder
-                .file(qml_module_registration_files.qmltyperegistrar)
                 .file(qml_module_registration_files.plugin)
                 // In comparison to the other RCC files, we don't need to link this with whole-archive or
                 // anything like that.
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 7ce231040..51a9f1c78 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -31,7 +31,7 @@ pub use installation::qmake::QtInstallationQMake;
 mod parse_cflags;
 
 mod tool;
-pub use tool::{MocArguments, MocProducts, QtTool, QtToolMoc, QtToolRcc};
+pub use tool::{MocArguments, MocProducts, QtTool, QtToolMoc, QtToolQmlTypeRegistrar, QtToolRcc};
 
 mod utils;
 
@@ -118,7 +118,7 @@ pub struct QmlModuleRegistrationFiles {
     /// Files generated by [qmlcachegen](https://doc.qt.io/qt-6/qtqml-qtquick-compiler-tech.html). Must be linked with `+whole-archive`.
     pub qmlcachegen: Vec<PathBuf>,
     /// File generated by [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) CLI tool.
-    pub qmltyperegistrar: PathBuf,
+    pub qmltyperegistrar: Option<PathBuf>,
     /// File with generated [QQmlEngineExtensionPlugin](https://doc.qt.io/qt-6/qqmlengineextensionplugin.html) that calls the function generated by qmltyperegistrar.
     pub plugin: PathBuf,
     /// Initializer that automatically registers the QQmlExtensionPlugin at startup.
@@ -210,10 +210,6 @@ impl QtBuild {
         qml_files: &[impl AsRef<Path>],
         qrc_files: &[impl AsRef<Path>],
     ) -> QmlModuleRegistrationFiles {
-        let qmltyperegistrar_executable = self
-            .qt_installation
-            .try_find_tool(QtTool::QmlTypeRegistrar)
-            .expect("Could not find qmltyperegistrar");
         // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
         let qmlcachegen_executable = if self.qt_installation.version().major >= 6 {
             Some(
@@ -384,50 +380,13 @@ prefer :/qt/qml/{qml_uri_dirs}/
         let qml_plugin_dir = PathBuf::from(format!("{out_dir}/qt-build-utils/qml_plugin"));
         std::fs::create_dir_all(&qml_plugin_dir).expect("Could not create qml_plugin dir");
 
-        // Run qmltyperegistrar
-        let qmltyperegistrar_output_path =
-            qml_plugin_dir.join(format!("{qml_uri_underscores}_qmltyperegistration.cpp"));
-
-        // Filter out empty jsons
-        let metatypes_json: Vec<_> = metatypes_json
-            .iter()
-            .filter(|f| {
-                std::fs::metadata(f)
-                    .unwrap_or_else(|_| {
-                        panic!("couldn't open json file {}", f.as_ref().to_string_lossy())
-                    })
-                    .len()
-                    > 0
-            })
-            .map(|f| f.as_ref().to_string_lossy().to_string())
-            .collect();
-
-        // Only run qmltyperegistrar if we have valid json files left out
-        if !metatypes_json.is_empty() {
-            let mut args = vec![
-                "--generate-qmltypes".to_string(),
-                qmltypes_path.to_string_lossy().to_string(),
-                "--major-version".to_string(),
-                version_major.to_string(),
-                "--minor-version".to_string(),
-                version_minor.to_string(),
-                "--import-name".to_string(),
-                uri.to_string(),
-                "-o".to_string(),
-                qmltyperegistrar_output_path.to_string_lossy().to_string(),
-            ];
-            args.extend(metatypes_json);
-            let cmd = Command::new(qmltyperegistrar_executable)
-                .args(args)
-                .output()
-                .unwrap_or_else(|_| panic!("qmltyperegistrar failed for {uri}"));
-            if !cmd.status.success() {
-                panic!(
-                    "qmltyperegistrar failed for {uri}:\n{}",
-                    String::from_utf8_lossy(&cmd.stderr)
-                );
-            }
-        }
+        // Run qmltyperegistrar over the meta types
+        let qmltyperegistrar_path = self.qmltyperegistrar().compile(
+            metatypes_json,
+            qmltypes_path,
+            uri,
+            Version::new(version_major as u64, version_minor as u64, 0),
+        );
 
         // Generate QQmlEngineExtensionPlugin
         let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
@@ -511,7 +470,7 @@ Q_IMPORT_PLUGIN({plugin_class_name});
                 // so we don't need to treat it like an initializer here.
                 rcc: rcc.file.unwrap(),
                 qmlcachegen: qmlcachegen_file_paths,
-                qmltyperegistrar: qmltyperegistrar_output_path,
+                qmltyperegistrar: qmltyperegistrar_path,
                 plugin: qml_plugin_cpp_path,
                 plugin_init,
                 include_path,
@@ -526,6 +485,11 @@ Q_IMPORT_PLUGIN({plugin_class_name});
         QtToolRcc::new(self.qt_installation.as_ref())
     }
 
+    /// Create a [QtToolQmlTypeRegistrar] for this [QtBuild]
+    pub fn qmltyperegistrar(&self) -> QtToolQmlTypeRegistrar {
+        QtToolQmlTypeRegistrar::new(self.qt_installation.as_ref())
+    }
+
     /// 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).
     /// 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).
     /// This function also returns a String that contains the name of the resource initializer
diff --git a/crates/qt-build-utils/src/tool/mod.rs b/crates/qt-build-utils/src/tool/mod.rs
index 1495ce17d..193eb8b6c 100644
--- a/crates/qt-build-utils/src/tool/mod.rs
+++ b/crates/qt-build-utils/src/tool/mod.rs
@@ -8,6 +8,9 @@ use std::{env, path::PathBuf};
 mod moc;
 pub use moc::{MocArguments, MocProducts, QtToolMoc};
 
+mod qmltyperegistrar;
+pub use qmltyperegistrar::QtToolQmlTypeRegistrar;
+
 mod rcc;
 pub use rcc::QtToolRcc;
 
diff --git a/crates/qt-build-utils/src/tool/qmltyperegistrar.rs b/crates/qt-build-utils/src/tool/qmltyperegistrar.rs
new file mode 100644
index 000000000..71e108d5f
--- /dev/null
+++ b/crates/qt-build-utils/src/tool/qmltyperegistrar.rs
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{QtInstallation, QtTool};
+use semver::Version;
+use std::{
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+/// A wrapper around the [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) tool
+pub struct QtToolQmlTypeRegistrar {
+    executable: PathBuf,
+}
+
+impl QtToolQmlTypeRegistrar {
+    /// Construct a [QtToolQmlTypeRegistrar] from a given [QtInstallation]
+    pub fn new(qt_installation: &dyn QtInstallation) -> Self {
+        let executable = qt_installation
+            .try_find_tool(QtTool::QmlTypeRegistrar)
+            .expect("Could not find qmltyperegistrar");
+
+        Self { executable }
+    }
+
+    /// Run [qmltyperegistrar](https://www.qt.io/blog/qml-type-registration-in-qt-5.15)
+    pub fn compile(
+        &self,
+        metatypes_json: &[impl AsRef<Path>],
+        qmltypes: impl AsRef<Path>,
+        uri: &str,
+        version: Version,
+    ) -> Option<PathBuf> {
+        // Filter out empty jsons
+        let metatypes_json: Vec<_> = metatypes_json
+            .iter()
+            .filter(|f| {
+                std::fs::metadata(f)
+                    .unwrap_or_else(|_| {
+                        panic!("couldn't open json file {}", f.as_ref().to_string_lossy())
+                    })
+                    .len()
+                    > 0
+            })
+            .map(|f| f.as_ref().to_string_lossy().to_string())
+            .collect();
+
+        // Only run qmltyperegistrar if we have valid json files left out
+        if metatypes_json.is_empty() {
+            return None;
+        }
+
+        let qml_uri_underscores = uri.replace('.', "_");
+        // TODO: note before this was the plugin folder
+        let output_folder = QtTool::QmlTypeRegistrar.writable_path();
+        std::fs::create_dir_all(&output_folder).expect("Could not create qmltyperegistrar dir");
+        let qmltyperegistrar_output_path =
+            output_folder.join(format!("{qml_uri_underscores}_qmltyperegistration.cpp"));
+
+        let mut args = vec![
+            "--generate-qmltypes".to_string(),
+            qmltypes.as_ref().to_string_lossy().to_string(),
+            "--major-version".to_string(),
+            version.major.to_string(),
+            "--minor-version".to_string(),
+            version.minor.to_string(),
+            "--import-name".to_string(),
+            uri.to_string(),
+            "-o".to_string(),
+            qmltyperegistrar_output_path.to_string_lossy().to_string(),
+        ];
+        args.extend(metatypes_json);
+        let cmd = Command::new(&self.executable)
+            .args(args)
+            .output()
+            .unwrap_or_else(|_| panic!("qmltyperegistrar failed for {uri}"));
+        if !cmd.status.success() {
+            panic!(
+                "qmltyperegistrar failed for {uri}:\n{}",
+                String::from_utf8_lossy(&cmd.stderr)
+            );
+        }
+
+        Some(qmltyperegistrar_output_path)
+    }
+}

From 88dad8fece7f30153b68d2dd8b74ea64e6feeedf Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Fri, 9 May 2025 15:17:51 +0100
Subject: [PATCH 23/27] qt-build-utils: split out qmlcachegen into a separate
 tool

---
 crates/qt-build-utils/src/lib.rs              | 109 +++----------
 crates/qt-build-utils/src/tool/mod.rs         |   3 +
 crates/qt-build-utils/src/tool/qmlcachegen.rs | 154 ++++++++++++++++++
 3 files changed, 179 insertions(+), 87 deletions(-)
 create mode 100644 crates/qt-build-utils/src/tool/qmlcachegen.rs

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 51a9f1c78..3cf6b0193 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -31,7 +31,10 @@ pub use installation::qmake::QtInstallationQMake;
 mod parse_cflags;
 
 mod tool;
-pub use tool::{MocArguments, MocProducts, QtTool, QtToolMoc, QtToolQmlTypeRegistrar, QtToolRcc};
+pub use tool::{
+    MocArguments, MocProducts, QmlCacheArguments, QmlCacheProducts, QtTool, QtToolMoc,
+    QtToolQmlCacheGen, QtToolQmlTypeRegistrar, QtToolRcc,
+};
 
 mod utils;
 
@@ -210,17 +213,6 @@ impl QtBuild {
         qml_files: &[impl AsRef<Path>],
         qrc_files: &[impl AsRef<Path>],
     ) -> QmlModuleRegistrationFiles {
-        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
-        let qmlcachegen_executable = if self.qt_installation.version().major >= 6 {
-            Some(
-                self.qt_installation
-                    .try_find_tool(QtTool::QmlCacheGen)
-                    .expect("Could not find qmlcachegen"),
-            )
-        } else {
-            None
-        };
-
         let qml_uri_dirs = uri.replace('.', "/");
 
         let out_dir = env::var("OUT_DIR").unwrap();
@@ -295,85 +287,28 @@ prefer :/qt/qml/{qml_uri_dirs}/
         // qmlcachegen needs to be run once for each .qml file with --resource-path,
         // then once for the module with --resource-name.
         let mut qmlcachegen_file_paths = Vec::new();
-        if let Some(qmlcachegen_executable) = &qmlcachegen_executable {
-            let qmlcachegen_dir = qt_build_utils_dir.join("qmlcachegen").join(&qml_uri_dirs);
-            std::fs::create_dir_all(&qmlcachegen_dir)
-                .expect("Could not create qmlcachegen directory for QML module");
-
-            let common_args = [
-                "-i".to_string(),
-                qmldir_file_path.to_string_lossy().to_string(),
-                "--resource".to_string(),
-                qrc_path.to_string_lossy().to_string(),
-            ];
-
-            let mut qml_file_qrc_paths = Vec::new();
+
+        // qmlcachegen has a different CLI in Qt 5, so only support Qt >= 6
+        if self.qt_installation.version().major >= 6 {
+            let qml_cache_args = QmlCacheArguments {
+                uri: uri.to_string(),
+                qmldir_path: qmldir_file_path,
+                qmldir_qrc_path: qrc_path.clone(),
+            };
+            let mut qml_resource_paths = Vec::new();
             for file in qml_files {
-                let qrc_resource_path =
-                    format!("/qt/qml/{qml_uri_dirs}/{}", file.as_ref().display());
-
-                let qml_compiled_file = qmlcachegen_dir.join(format!(
-                    "{}.cpp",
-                    file.as_ref().file_name().unwrap().to_string_lossy()
-                ));
-                qmlcachegen_file_paths.push(PathBuf::from(&qml_compiled_file));
-
-                let specific_args = vec![
-                    "--resource-path".to_string(),
-                    qrc_resource_path.clone(),
-                    "-o".to_string(),
-                    qml_compiled_file.to_string_lossy().to_string(),
-                    std::fs::canonicalize(file)
-                        .unwrap()
-                        .to_string_lossy()
-                        .to_string(),
-                ];
-
-                let cmd = Command::new(qmlcachegen_executable)
-                    .args(common_args.iter().chain(&specific_args))
-                    .output()
-                    .unwrap_or_else(|_| {
-                        panic!(
-                            "qmlcachegen failed for {} in QML module {uri}",
-                            file.as_ref().display()
-                        )
-                    });
-                if !cmd.status.success() {
-                    panic!(
-                        "qmlcachegen failed for {} in QML module {uri}:\n{}",
-                        file.as_ref().display(),
-                        String::from_utf8_lossy(&cmd.stderr)
-                    );
-                }
-                qml_file_qrc_paths.push(qrc_resource_path);
+                let result = QtToolQmlCacheGen::new(self.qt_installation.as_ref())
+                    .compile(qml_cache_args.clone(), file);
+                qmlcachegen_file_paths.push(result.qml_cache_path);
+                qml_resource_paths.push(result.qml_resource_path);
             }
 
-            let qmlcachegen_loader = qmlcachegen_dir.join("qmlcache_loader.cpp");
-            let specific_args = vec![
-                "--resource-name".to_string(),
-                format!("qmlcache_{qml_uri_underscores}"),
-                "-o".to_string(),
-                qmlcachegen_loader.to_string_lossy().to_string(),
-            ];
-
             // If there are no QML files there is nothing for qmlcachegen to run with
             if !qml_files.is_empty() {
-                let cmd = Command::new(qmlcachegen_executable)
-                    .args(
-                        common_args
-                            .iter()
-                            .chain(&specific_args)
-                            .chain(&qml_file_qrc_paths),
-                    )
-                    .output()
-                    .unwrap_or_else(|_| panic!("qmlcachegen failed for QML module {uri}"));
-                if !cmd.status.success() {
-                    panic!(
-                        "qmlcachegen failed for QML module {uri}:\n{}",
-                        String::from_utf8_lossy(&cmd.stderr)
-                    );
-                }
-                qmlcachegen_file_paths.push(PathBuf::from(&qmlcachegen_loader));
+                qmlcachegen_file_paths.push(
+                    QtToolQmlCacheGen::new(self.qt_installation.as_ref())
+                        .compile_loader(qml_cache_args.clone(), &qml_resource_paths),
+                );
             }
         }
 
@@ -407,7 +342,7 @@ prefer :/qt/qml/{qml_uri_dirs}/
                 &format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"),
             );
 
-            if !qml_files.is_empty() && qmlcachegen_executable.is_some() {
+            if !qml_files.is_empty() && !qmlcachegen_file_paths.is_empty() {
                 generate_usage(
                     "int",
                     &format!("qInitResources_qmlcache_{qml_uri_underscores}"),
diff --git a/crates/qt-build-utils/src/tool/mod.rs b/crates/qt-build-utils/src/tool/mod.rs
index 193eb8b6c..b03d6c970 100644
--- a/crates/qt-build-utils/src/tool/mod.rs
+++ b/crates/qt-build-utils/src/tool/mod.rs
@@ -8,6 +8,9 @@ use std::{env, path::PathBuf};
 mod moc;
 pub use moc::{MocArguments, MocProducts, QtToolMoc};
 
+mod qmlcachegen;
+pub use qmlcachegen::{QmlCacheArguments, QmlCacheProducts, QtToolQmlCacheGen};
+
 mod qmltyperegistrar;
 pub use qmltyperegistrar::QtToolQmlTypeRegistrar;
 
diff --git a/crates/qt-build-utils/src/tool/qmlcachegen.rs b/crates/qt-build-utils/src/tool/qmlcachegen.rs
new file mode 100644
index 000000000..a4bd3f248
--- /dev/null
+++ b/crates/qt-build-utils/src/tool/qmlcachegen.rs
@@ -0,0 +1,154 @@
+// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
+//
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+use crate::{QtInstallation, QtTool};
+use std::{
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+/// Arguments for a [QtToolQmlCacheGen]
+#[derive(Clone)]
+pub struct QmlCacheArguments {
+    /// The URI for the QML module
+    pub uri: String,
+    /// The path to the qmldir
+    pub qmldir_path: PathBuf,
+    /// The path to the qrc file that contains a qmldir
+    pub qmldir_qrc_path: PathBuf,
+}
+
+/// Paths to files generated by [QtToolQmlCacheGen]
+pub struct QmlCacheProducts {
+    /// The path of the generated cache file
+    pub qml_cache_path: PathBuf,
+    /// The Qt resource path for qml file
+    pub qml_resource_path: String,
+}
+
+/// A wrapper around the [qmlcachegen](https://www.qt.io/blog/qml-type-registration-in-qt-5.15) tool
+pub struct QtToolQmlCacheGen {
+    executable: PathBuf,
+}
+
+impl QtToolQmlCacheGen {
+    /// Construct a [QtToolQmlCacheGen] from a given [QtInstallation]
+    pub fn new(qt_installation: &dyn QtInstallation) -> Self {
+        let executable = qt_installation
+            .try_find_tool(QtTool::QmlCacheGen)
+            .expect("Could not find qmlcachegen");
+
+        Self { executable }
+    }
+
+    /// Run qmlcachegen for a given qml file
+    pub fn compile(
+        &self,
+        common_args: QmlCacheArguments,
+        file: impl AsRef<Path>,
+    ) -> QmlCacheProducts {
+        let uri = common_args.uri;
+        let qml_uri_dirs = uri.replace('.', "/");
+
+        let qmlcachegen_dir = QtTool::QmlCacheGen.writable_path().join(&qml_uri_dirs);
+        std::fs::create_dir_all(&qmlcachegen_dir)
+            .expect("Could not create qmlcachegen directory for QML module");
+
+        let common_args = [
+            "-i".to_string(),
+            common_args.qmldir_path.to_string_lossy().to_string(),
+            "--resource".to_string(),
+            common_args.qmldir_qrc_path.to_string_lossy().to_string(),
+        ];
+
+        let qml_cache_path = qmlcachegen_dir.join(format!(
+            "{}.cpp",
+            file.as_ref().file_name().unwrap().to_string_lossy()
+        ));
+
+        let qml_resource_path = format!("/qt/qml/{qml_uri_dirs}/{}", file.as_ref().display());
+
+        let specific_args = vec![
+            "--resource-path".to_string(),
+            qml_resource_path.to_string(),
+            "-o".to_string(),
+            qml_cache_path.to_string_lossy().to_string(),
+            std::fs::canonicalize(&file)
+                .unwrap()
+                .to_string_lossy()
+                .to_string(),
+        ];
+
+        let cmd = Command::new(&self.executable)
+            .args(common_args.iter().chain(&specific_args))
+            .output()
+            .unwrap_or_else(|_| {
+                panic!(
+                    "qmlcachegen failed for {} in QML module {uri}",
+                    file.as_ref().display()
+                )
+            });
+        if !cmd.status.success() {
+            panic!(
+                "qmlcachegen failed for {} in QML module {uri}:\n{}",
+                file.as_ref().display(),
+                String::from_utf8_lossy(&cmd.stderr)
+            );
+        }
+
+        QmlCacheProducts {
+            qml_cache_path,
+            qml_resource_path,
+        }
+    }
+
+    /// Compile a loader for given qml resource paths
+    pub fn compile_loader(
+        &self,
+        common_args: QmlCacheArguments,
+        qml_resource_paths: &[String],
+    ) -> PathBuf {
+        let uri = common_args.uri;
+        let qml_uri_dirs = uri.replace('.', "/");
+        let qml_uri_underscores = uri.replace('.', "_");
+
+        let qmlcachegen_dir = QtTool::QmlCacheGen.writable_path().join(qml_uri_dirs);
+        std::fs::create_dir_all(&qmlcachegen_dir)
+            .expect("Could not create qmlcachegen directory for QML module");
+
+        let common_args = [
+            "-i".to_string(),
+            common_args.qmldir_path.to_string_lossy().to_string(),
+            "--resource".to_string(),
+            common_args.qmldir_qrc_path.to_string_lossy().to_string(),
+        ];
+
+        let qmlcachegen_loader = qmlcachegen_dir.join("qmlcache_loader.cpp");
+        let specific_args = vec![
+            "--resource-name".to_string(),
+            format!("qmlcache_{qml_uri_underscores}"),
+            "-o".to_string(),
+            qmlcachegen_loader.to_string_lossy().to_string(),
+        ];
+
+        let cmd = Command::new(&self.executable)
+            .args(
+                common_args
+                    .iter()
+                    .chain(&specific_args)
+                    .chain(qml_resource_paths),
+            )
+            .output()
+            .unwrap_or_else(|_| panic!("qmlcachegen failed for QML module {uri}"));
+        if !cmd.status.success() {
+            panic!(
+                "qmlcachegen failed for QML module {uri}:\n{}",
+                String::from_utf8_lossy(&cmd.stderr)
+            );
+        }
+
+        qmlcachegen_loader
+    }
+}

From e0e407264c91baebe2dfb4938f17fe11fe574ec1 Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 13 May 2025 11:04:38 +0100
Subject: [PATCH 24/27] cxx-qt-build: use new rcc method instead of qrc

---
 crates/cxx-qt-build/src/lib.rs   |  4 ++--
 crates/qt-build-utils/src/lib.rs | 22 +---------------------
 2 files changed, 3 insertions(+), 23 deletions(-)

diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index 09db9e176..81913d34b 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -1032,12 +1032,12 @@ extern "C" bool {init_fun}() {{
             .iter()
             .map(|qrc_file| {
                 // Also ensure that each of the files in the qrc can cause a change
-                for qrc_inner_file in qtbuild.qrc_list(qrc_file) {
+                for qrc_inner_file in qtbuild.rcc().list(qrc_file) {
                     println!("cargo::rerun-if-changed={}", qrc_inner_file.display());
                 }
                 // We need to link this using an object file or +whole-achive, the static initializer of
                 // the qrc file isn't lost.
-                qtbuild.qrc(qrc_file)
+                qtbuild.rcc().compile(qrc_file)
             })
             .collect()
     }
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 3cf6b0193..6eb1d70e4 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -399,7 +399,7 @@ Q_IMPORT_PLUGIN({plugin_class_name});
                 )),
             };
 
-            let rcc = self.qrc(&qrc_path);
+            let rcc = self.rcc().compile(&qrc_path);
             QmlModuleRegistrationFiles {
                 // The rcc file is automatically initialized when importing the plugin.
                 // so we don't need to treat it like an initializer here.
@@ -424,24 +424,4 @@ Q_IMPORT_PLUGIN({plugin_class_name});
     pub fn qmltyperegistrar(&self) -> QtToolQmlTypeRegistrar {
         QtToolQmlTypeRegistrar::new(self.qt_installation.as_ref())
     }
-
-    /// 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).
-    /// 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).
-    /// This function also returns a String that contains the name of the resource initializer
-    /// function.
-    /// The build system must ensure that if the .cpp file is built into a static library, either
-    /// the `+whole-archive` flag is used, or the initializer function is called by the
-    /// application.
-    pub fn qrc(&mut self, input_file: impl AsRef<Path>) -> Initializer {
-        // TODO: later change to just have a rcc() -> QtToolRcc
-        // but keep this for compat for now
-        QtToolRcc::new(self.qt_installation.as_ref()).compile(input_file)
-    }
-
-    /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources
-    pub fn qrc_list(&mut self, input_file: impl AsRef<Path>) -> Vec<PathBuf> {
-        // TODO: later change to just have a rcc() -> QtToolRcc
-        // but keep this for compat for now
-        QtToolRcc::new(self.qt_installation.as_ref()).list(input_file)
-    }
 }

From 42b24e9c66d0e24c472e38c922d403e79aa6853b Mon Sep 17 00:00:00 2001
From: Andrew Hayzen <andrew.hayzen@kdab.com>
Date: Tue, 13 May 2025 11:08:57 +0100
Subject: [PATCH 25/27] cxx-qt-build: use moc() method to retrieve the tool not
 compile

---
 crates/cxx-qt-build/src/lib.rs   |  4 ++--
 crates/qt-build-utils/src/lib.rs | 12 +++++-------
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index 81913d34b..e11233dc8 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -702,7 +702,7 @@ impl CxxQtBuilder {
             moc_arguments,
         } in &self.qobject_headers
         {
-            let moc_products = qtbuild.moc(path, moc_arguments.clone());
+            let moc_products = qtbuild.moc().compile(path, moc_arguments.clone());
             // Include the moc folder
             if let Some(dir) = moc_products.cpp.parent() {
                 self.cc_builder.include(dir);
@@ -827,7 +827,7 @@ impl CxxQtBuilder {
                     }
 
                     cc_builder.file(&qobject);
-                    let moc_products = qtbuild.moc(
+                    let moc_products = qtbuild.moc().compile(
                         qobject_header,
                         MocArguments::default().uri(qml_module.uri.clone()),
                     );
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 6eb1d70e4..47fad8111 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -187,13 +187,11 @@ impl QtBuild {
         self.qt_installation.version()
     }
 
-    /// 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).
-    /// 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),
-    /// as well as the path to the generated metatypes.json file, which can be passed to [register_qml_module](Self::register_qml_module).
-    pub fn moc(&mut self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
-        // TODO: do we change this API to just be moc() -> QtToolMoc ?
+    /// Create a [QtToolMoc] for this [QtBuild]
+    ///
+    /// This allows for using [moc](https://doc.qt.io/qt-6/moc.html)
+    pub fn moc(&mut self) -> QtToolMoc {
         QtToolMoc::new(self.qt_installation.as_ref(), &self.qt_modules)
-            .compile(input_file, arguments)
     }
 
     /// Generate C++ files to automatically register a QML module at build time using the JSON output from [moc](Self::moc).
@@ -380,7 +378,7 @@ public:
             )
             .expect("Failed to write plugin definition");
 
-            let moc_product = self.moc(
+            let moc_product = self.moc().compile(
                 &qml_plugin_cpp_path,
                 MocArguments::default().uri(uri.to_owned()),
             );

From e4b8567cd162277911cc3499b2b517d5ecc52f63 Mon Sep 17 00:00:00 2001
From: Leon Matthes <leon.matthes@kdab.com>
Date: Thu, 22 May 2025 19:09:55 +0200
Subject: [PATCH 26/27] qt-build-utils: Return Result in try_find_tool

This now also prints out the paths that were attempted.
---
 crates/qt-build-utils/src/installation/mod.rs |  2 +-
 .../qt-build-utils/src/installation/qmake.rs  | 45 +++++++++++++------
 2 files changed, 32 insertions(+), 15 deletions(-)

diff --git a/crates/qt-build-utils/src/installation/mod.rs b/crates/qt-build-utils/src/installation/mod.rs
index 3847024f8..cdb91710f 100644
--- a/crates/qt-build-utils/src/installation/mod.rs
+++ b/crates/qt-build-utils/src/installation/mod.rs
@@ -36,7 +36,7 @@ pub trait QtInstallation {
     // }
     fn link_modules(&self, builder: &mut cc::Build, qt_modules: &[String]);
     /// Find the path to a given Qt tool for the Qt installation
-    fn try_find_tool(&self, tool: QtTool) -> Option<PathBuf>;
+    fn try_find_tool(&self, tool: QtTool) -> anyhow::Result<PathBuf>;
     /// Version of the detected Qt installation
     fn version(&self) -> Version;
 }
diff --git a/crates/qt-build-utils/src/installation/qmake.rs b/crates/qt-build-utils/src/installation/qmake.rs
index 5b3820792..920a48cf3 100644
--- a/crates/qt-build-utils/src/installation/qmake.rs
+++ b/crates/qt-build-utils/src/installation/qmake.rs
@@ -20,7 +20,13 @@ pub struct QtInstallationQMake {
     qmake_path: PathBuf,
     qmake_version: Version,
     // Internal cache of paths for tools
-    tool_cache: RefCell<HashMap<QtTool, Option<PathBuf>>>,
+    //
+    // Note that this only stores valid resolved paths.
+    // If we failed to find the tool, we will not cache the failure and instead retry if called
+    // again.
+    // This is partially because anyhow::Error is not Clone, and partially because retrying gives
+    // the caller the ability to change the environment and try again.
+    tool_cache: RefCell<HashMap<QtTool, PathBuf>>,
 }
 
 impl QtInstallationQMake {
@@ -250,18 +256,24 @@ impl QtInstallation for QtInstallationQMake {
         }
     }
 
-    fn try_find_tool(&self, tool: QtTool) -> Option<PathBuf> {
-        let find_tool_closure = |tool: &QtTool| self.try_qmake_find_tool(tool.binary_name());
+    fn try_find_tool(&self, tool: QtTool) -> anyhow::Result<PathBuf> {
+        let find_tool = || self.try_qmake_find_tool(tool.binary_name());
 
         // Attempt to use the cache
         if let Ok(mut tool_cache) = self.tool_cache.try_borrow_mut() {
             // Read the tool from the cache or insert
-            tool_cache
-                .entry(tool)
-                .or_insert_with_key(find_tool_closure)
-                .to_owned()
+            let path = tool_cache.get(&tool);
+            let path = match path {
+                Some(path) => path.clone(),
+                None => {
+                    let path = find_tool()?;
+                    tool_cache.insert(tool, path.clone());
+                    path
+                }
+            };
+            Ok(path)
         } else {
-            find_tool_closure(&tool)
+            find_tool()
         }
     }
 
@@ -372,7 +384,7 @@ impl QtInstallationQMake {
         .to_string()
     }
 
-    fn try_qmake_find_tool(&self, tool_name: &str) -> Option<PathBuf> {
+    fn try_qmake_find_tool(&self, tool_name: &str) -> anyhow::Result<PathBuf> {
         // "qmake -query" exposes a list of paths that describe where Qt executables and libraries
         // are located, as well as where new executables & libraries should be installed to.
         // We can use these variables to find any Qt tool.
@@ -405,6 +417,7 @@ impl QtInstallationQMake {
         // To check & debug all variables available on your system, simply run:
         //
         //              qmake -query
+        let mut failed_paths = vec![];
         [
             "QT_HOST_LIBEXECS/get",
             "QT_HOST_LIBEXECS",
@@ -419,11 +432,15 @@ impl QtInstallationQMake {
         // Find the first valid executable path
         .find_map(|qmake_query_var| {
             let executable_path = PathBuf::from(self.qmake_query(qmake_query_var)).join(tool_name);
-            Command::new(&executable_path)
-                .args(["-help"])
-                .output()
-                .map(|_| executable_path)
-                .ok()
+            let test_output = Command::new(&executable_path).args(["-help"]).output();
+            match test_output {
+                Err(_err) => {
+                    failed_paths.push(executable_path);
+                    None
+                }
+                Ok(_) => Some(executable_path),
+            }
         })
+        .ok_or_else(|| anyhow::anyhow!("Failed to find {tool_name}, tried: {failed_paths:?}"))
     }
 }

From 6c2ce2f342b22761ca47d3c06cee7725890336b4 Mon Sep 17 00:00:00 2001
From: Leon Matthes <leon.matthes@kdab.com>
Date: Thu, 22 May 2025 19:12:36 +0200
Subject: [PATCH 27/27] qt-build-utils: new_with_default/new ->
 new/with_installation

Also only compile in the `new` function if the `qmake` feature is
provided.
Previously this function would not run anyhow.
If we add a `cmake` feature, we can conditionally enable the function if
either `cmake` or `qmake` is enabled.
---
 crates/cxx-qt-build/src/lib.rs          |  6 ++----
 crates/cxx-qt-lib/build.rs              |  2 +-
 crates/qt-build-utils/src/lib.rs        | 21 +++++++++++----------
 tests/qt_types_standalone/rust/build.rs |  2 +-
 4 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs
index e11233dc8..ae810915f 100644
--- a/crates/cxx-qt-build/src/lib.rs
+++ b/crates/cxx-qt-build/src/lib.rs
@@ -1124,10 +1124,8 @@ extern "C" bool {init_fun}() {{
 
         let header_root = dir::header_root();
 
-        let mut qtbuild = qt_build_utils::QtBuild::new_with_default_installation(
-            qt_modules.iter().cloned().collect(),
-        )
-        .expect("Could not find Qt installation");
+        let mut qtbuild = qt_build_utils::QtBuild::new(qt_modules.iter().cloned().collect())
+            .expect("Could not find Qt installation");
         qtbuild.cargo_link_libraries(&mut self.cc_builder);
         Self::define_qt_version_cfg_variables(qtbuild.version());
 
diff --git a/crates/cxx-qt-lib/build.rs b/crates/cxx-qt-lib/build.rs
index 5a06120ed..e132c51fc 100644
--- a/crates/cxx-qt-lib/build.rs
+++ b/crates/cxx-qt-lib/build.rs
@@ -99,7 +99,7 @@ fn write_headers() {
 }
 
 fn main() {
-    let qtbuild = qt_build_utils::QtBuild::new_with_default_installation(vec!["Core".to_owned()])
+    let qtbuild = qt_build_utils::QtBuild::new(vec!["Core".to_owned()])
         .expect("Could not find Qt installation");
 
     write_headers();
diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index 47fad8111..1d0debeb9 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -136,7 +136,7 @@ pub struct QmlModuleRegistrationFiles {
 ///     .iter()
 ///     .map(|m| String::from(*m))
 ///     .collect();
-/// let qtbuild = qt_build_utils::QtBuild::new_with_default_installation(qt_modules).expect("Could not find Qt installation");
+/// let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation");
 /// ```
 pub struct QtBuild {
     qt_installation: Box<dyn QtInstallation>,
@@ -147,20 +147,21 @@ impl QtBuild {
     /// Create a [QtBuild] using the default [QtInstallation] (currently uses [QtInstallationQMake])
     /// and specify which Qt modules you are linking, ommitting the `Qt` prefix (`"Core"`
     /// rather than `"QtCore"`).
-    //
-    // TODO: is there a better name for this method or a sane way to create a default QtInstallation?
-    pub fn new_with_default_installation(qt_modules: Vec<String>) -> anyhow::Result<Self> {
-        #[cfg(feature = "qmake")]
+    ///
+    /// Currently this function is only available when the `qmake` feature is enabled.
+    /// Use [Self::with_installation] to create a [QtBuild] with a custom [QtInstallation].
+    #[cfg(feature = "qmake")]
+    pub fn new(qt_modules: Vec<String>) -> anyhow::Result<Self> {
         let qt_installation = Box::new(QtInstallationQMake::new()?);
-        #[cfg(not(feature = "qmake"))]
-        unsupported!("Only qmake feature is supported");
-
-        Ok(Self::new(qt_installation, qt_modules))
+        Ok(Self::with_installation(qt_installation, qt_modules))
     }
 
     /// Create a [QtBuild] using the given [QtInstallation] and specify which
     /// Qt modules you are linking, ommitting the `Qt` prefix (`"Core"` rather than `"QtCore"`).
-    pub fn new(qt_installation: Box<dyn QtInstallation>, mut qt_modules: Vec<String>) -> Self {
+    pub fn with_installation(
+        qt_installation: Box<dyn QtInstallation>,
+        mut qt_modules: Vec<String>,
+    ) -> Self {
         if qt_modules.is_empty() {
             qt_modules.push("Core".to_string());
         }
diff --git a/tests/qt_types_standalone/rust/build.rs b/tests/qt_types_standalone/rust/build.rs
index 72993b6ed..51f6df496 100644
--- a/tests/qt_types_standalone/rust/build.rs
+++ b/tests/qt_types_standalone/rust/build.rs
@@ -6,7 +6,7 @@
 use cxx_qt_build::CxxQtBuilder;
 
 fn main() {
-    let qtbuild = qt_build_utils::QtBuild::new_with_default_installation(vec!["Core".to_owned()])
+    let qtbuild = qt_build_utils::QtBuild::new(vec!["Core".to_owned()])
         .expect("Could not find Qt installation");
 
     let mut builder = CxxQtBuilder::new()