qt_build_utils/installation/
qmake.rs

1// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6use semver::Version;
7use std::{
8    cell::RefCell,
9    collections::HashMap,
10    env,
11    io::ErrorKind,
12    path::{Path, PathBuf},
13    process::Command,
14};
15
16use crate::{parse_cflags, utils, QtBuildError, QtInstallation, QtTool};
17
18/// A implementation of [QtInstallation] using qmake
19pub struct QtInstallationQMake {
20    qmake_path: PathBuf,
21    qmake_version: Version,
22    // Internal cache of paths for tools
23    //
24    // Note that this only stores valid resolved paths.
25    // If we failed to find the tool, we will not cache the failure and instead retry if called
26    // again.
27    // This is partially because anyhow::Error is not Clone, and partially because retrying gives
28    // the caller the ability to change the environment and try again.
29    tool_cache: RefCell<HashMap<QtTool, PathBuf>>,
30}
31
32impl QtInstallationQMake {
33    /// The directories specified by the `PATH` environment variable are where qmake is
34    /// searched for. Alternatively, the `QMAKE` environment variable may be set to specify
35    /// an explicit path to qmake.
36    ///
37    /// If multiple major versions (for example, `5` and `6`) of Qt could be installed, set
38    /// the `QT_VERSION_MAJOR` environment variable to force which one to use. When using Cargo
39    /// as the build system for the whole build, prefer using `QT_VERSION_MAJOR` over the `QMAKE`
40    /// environment variable because it will account for different names for the qmake executable
41    /// that some Linux distributions use.
42    ///
43    /// However, when building a Rust staticlib that gets linked to C++ code by a C++ build
44    /// system, it is best to use the `QMAKE` environment variable to ensure that the Rust
45    /// staticlib is linked to the same installation of Qt that the C++ build system has
46    /// detected.
47    /// With CMake, this will automatically be set up for you when using cxxqt_import_crate.
48    ///
49    /// Alternatively, you can get this from the `Qt::qmake` target's `IMPORTED_LOCATION`
50    /// property, for example:
51    /// ```cmake
52    /// find_package(Qt6 COMPONENTS Core)
53    /// if(NOT Qt6_FOUND)
54    ///     find_package(Qt5 5.15 COMPONENTS Core REQUIRED)
55    /// endif()
56    /// get_target_property(QMAKE Qt::qmake IMPORTED_LOCATION)
57    ///
58    /// execute_process(
59    ///     COMMAND cmake -E env
60    ///         "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}/cargo"
61    ///         "QMAKE=${QMAKE}"
62    ///         cargo build
63    ///     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
64    /// )
65    /// ```
66    pub fn new() -> anyhow::Result<Self> {
67        // Try the QMAKE variable first
68        println!("cargo::rerun-if-env-changed=QMAKE");
69        if let Ok(qmake_env_var) = env::var("QMAKE") {
70            return QtInstallationQMake::try_from(PathBuf::from(&qmake_env_var)).map_err(|err| {
71                QtBuildError::QMakeSetQtMissing {
72                    qmake_env_var,
73                    error: err.into(),
74                }
75                .into()
76            });
77        }
78
79        // Try variable candidates within the patch
80        ["qmake6", "qmake-qt5", "qmake"]
81            .iter()
82            // Use the first non-errored installation
83            // If there are no valid installations we display the last error
84            .fold(None, |acc, qmake_path| {
85                Some(acc.map_or_else(
86                    // Value is None so try to create installation
87                    || QtInstallationQMake::try_from(PathBuf::from(qmake_path)),
88                    // Value is Some so pass through or create if Err
89                    |prev: anyhow::Result<Self>| {
90                        prev.or_else(|_|
91                            // Value is Err so try to create installation
92                            QtInstallationQMake::try_from(PathBuf::from(qmake_path)))
93                    },
94                ))
95            })
96            .unwrap_or_else(|| Err(QtBuildError::QtMissing.into()))
97    }
98}
99
100impl TryFrom<PathBuf> for QtInstallationQMake {
101    type Error = anyhow::Error;
102
103    fn try_from(qmake_path: PathBuf) -> anyhow::Result<Self> {
104        // Attempt to read the QT_VERSION from qmake
105        let qmake_version = match Command::new(&qmake_path)
106            .args(["-query", "QT_VERSION"])
107            .output()
108        {
109            Err(e) if e.kind() == ErrorKind::NotFound => Err(QtBuildError::QtMissing),
110            Err(e) => Err(QtBuildError::QmakeFailed(e)),
111            Ok(output) if !output.status.success() => Err(QtBuildError::QtMissing),
112            Ok(output) => Ok(Version::parse(
113                String::from_utf8_lossy(&output.stdout).trim(),
114            )?),
115        }?;
116
117        // Check QT_VERSION_MAJOR is the same as the qmake version
118        println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
119        if let Ok(env_qt_version_major) = env::var("QT_VERSION_MAJOR") {
120            // Parse to an integer
121            let env_qt_version_major = env_qt_version_major.trim().parse::<u64>().map_err(|e| {
122                QtBuildError::QtVersionMajorInvalid {
123                    qt_version_major_env_var: env_qt_version_major,
124                    source: e,
125                }
126            })?;
127
128            // Ensure the version major is the same
129            if qmake_version.major != env_qt_version_major {
130                return Err(QtBuildError::QtVersionMajorDoesNotMatch {
131                    qmake_version: qmake_version.major,
132                    qt_version_major: env_qt_version_major,
133                }
134                .into());
135            }
136        }
137
138        Ok(Self {
139            qmake_path,
140            qmake_version,
141            tool_cache: HashMap::default().into(),
142        })
143    }
144}
145
146impl QtInstallation for QtInstallationQMake {
147    fn framework_paths(&self, _qt_modules: &[String]) -> Vec<PathBuf> {
148        let mut framework_paths = vec![];
149
150        if utils::is_apple_target() {
151            // Note that this adds the framework path which allows for
152            // includes such as <QtCore/QObject> to be resolved correctly
153            let framework_path = self.qmake_query("QT_INSTALL_LIBS");
154            framework_paths.push(framework_path);
155        }
156
157        framework_paths
158            .iter()
159            .map(PathBuf::from)
160            // Only add paths if they exist
161            .filter(|path| path.exists())
162            .collect()
163    }
164
165    fn include_paths(&self, qt_modules: &[String]) -> Vec<PathBuf> {
166        let root_path = self.qmake_query("QT_INSTALL_HEADERS");
167        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
168        let mut paths = Vec::new();
169        for qt_module in qt_modules {
170            // Add the usual location for the Qt module
171            paths.push(format!("{root_path}/Qt{qt_module}"));
172
173            // Ensure that we add any framework's headers path
174            //
175            // Note that the individual Qt modules should in theory work
176            // by giving `-framework QtCore` to the cc builder. However these
177            // appear to be lost in flag_if_supported.
178            //
179            // Also note we still need these include directs even with the -F / framework paths
180            // as otherwise only <QtCore/QtGlobal> works but <QtGlobal> does not.
181            let header_path = format!("{lib_path}/Qt{qt_module}.framework/Headers");
182            if utils::is_apple_target() && Path::new(&header_path).exists() {
183                paths.push(header_path);
184            }
185        }
186
187        // Add the QT_INSTALL_HEADERS itself
188        paths.push(root_path);
189
190        paths
191            .iter()
192            .map(PathBuf::from)
193            // Only add paths if they exist
194            .filter(|path| path.exists())
195            .collect()
196    }
197
198    fn link_modules(&self, builder: &mut cc::Build, qt_modules: &[String]) {
199        let prefix_path = self.qmake_query("QT_INSTALL_PREFIX");
200        let lib_path = self.qmake_query("QT_INSTALL_LIBS");
201        println!("cargo::rustc-link-search={lib_path}");
202
203        let target = env::var("TARGET");
204
205        // Add the QT_INSTALL_LIBS as a framework link search path as well
206        //
207        // Note that leaving the kind empty should default to all,
208        // but this doesn't appear to find frameworks in all situations
209        // https://github.com/KDAB/cxx-qt/issues/885
210        //
211        // Note this doesn't have an adverse affect running all the time
212        // as it appears that all rustc-link-search are added
213        //
214        // Note that this adds the framework path which allows for
215        // includes such as <QtCore/QObject> to be resolved correctly
216        if utils::is_apple_target() {
217            println!("cargo::rustc-link-search=framework={lib_path}");
218
219            // Ensure that any framework paths are set to -F
220            for framework_path in self.framework_paths(qt_modules) {
221                builder.flag_if_supported(format!("-F{}", framework_path.display()));
222                // Also set the -rpath otherwise frameworks can not be found at runtime
223                println!(
224                    "cargo::rustc-link-arg=-Wl,-rpath,{}",
225                    framework_path.display()
226                );
227            }
228        }
229
230        let prefix = match &target {
231            Ok(target) => {
232                if target.contains("windows") {
233                    ""
234                } else {
235                    "lib"
236                }
237            }
238            Err(_) => "lib",
239        };
240
241        for qt_module in qt_modules {
242            let framework = if utils::is_apple_target() {
243                Path::new(&format!("{lib_path}/Qt{qt_module}.framework")).exists()
244            } else {
245                false
246            };
247
248            let (link_lib, prl_path) = if framework {
249                (
250                    format!("framework=Qt{qt_module}"),
251                    format!("{lib_path}/Qt{qt_module}.framework/Resources/Qt{qt_module}.prl"),
252                )
253            } else {
254                (
255                    format!("Qt{}{qt_module}", self.qmake_version.major),
256                    self.find_qt_module_prl(&lib_path, prefix, self.qmake_version.major, qt_module),
257                )
258            };
259
260            self.link_qt_library(
261                &format!("Qt{}{qt_module}", self.qmake_version.major),
262                &prefix_path,
263                &lib_path,
264                &link_lib,
265                &prl_path,
266                builder,
267            );
268        }
269
270        if utils::is_emscripten_target() {
271            let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
272            println!("cargo::rustc-link-search={platforms_path}");
273            self.link_qt_library(
274                "qwasm",
275                &prefix_path,
276                &lib_path,
277                "qwasm",
278                &format!("{platforms_path}/libqwasm.prl"),
279                builder,
280            );
281        }
282    }
283
284    fn try_find_tool(&self, tool: QtTool) -> anyhow::Result<PathBuf> {
285        let find_tool = || self.try_qmake_find_tool(tool.binary_name());
286        // Attempt to use the cache
287        let Ok(mut tool_cache) = self.tool_cache.try_borrow_mut() else {
288            return find_tool();
289        };
290        // Read the tool from the cache or insert
291        if let Some(path) = tool_cache.get(&tool) {
292            return Ok(path.clone());
293        }
294        let path = find_tool()?;
295        tool_cache.insert(tool, path.clone());
296        Ok(path)
297    }
298
299    fn version(&self) -> semver::Version {
300        self.qmake_version.clone()
301    }
302}
303
304impl QtInstallationQMake {
305    /// Some prl files include their architecture in their naming scheme.
306    /// Just try all known architectures and fallback to non when they all failed.
307    fn find_qt_module_prl(
308        &self,
309        lib_path: &str,
310        prefix: &str,
311        version_major: u64,
312        qt_module: &str,
313    ) -> String {
314        for arch in ["", "_arm64-v8a", "_armeabi-v7a", "_x86", "_x86_64"] {
315            let prl_path = format!("{lib_path}/{prefix}Qt{version_major}{qt_module}{arch}.prl");
316            match Path::new(&prl_path).try_exists() {
317                Ok(exists) => {
318                    if exists {
319                        return prl_path;
320                    }
321                }
322                Err(e) => {
323                    println!("cargo::warning=failed checking for existence of {prl_path}: {e}");
324                }
325            }
326        }
327
328        format!("{lib_path}/{prefix}Qt{version_major}{qt_module}.prl")
329    }
330
331    fn link_qt_library(
332        &self,
333        name: &str,
334        prefix_path: &str,
335        lib_path: &str,
336        link_lib: &str,
337        prl_path: &str,
338        builder: &mut cc::Build,
339    ) {
340        println!("cargo::rustc-link-lib={link_lib}");
341
342        match std::fs::read_to_string(prl_path) {
343            Ok(prl) => {
344                for line in prl.lines() {
345                    if let Some(line) = line.strip_prefix("QMAKE_PRL_LIBS = ") {
346                        parse_cflags::parse_libs_cflags(
347                            name,
348                            line.replace(r"$$[QT_INSTALL_LIBS]", lib_path)
349                                .replace(r"$$[QT_INSTALL_PREFIX]", prefix_path)
350                                .as_bytes(),
351                            builder,
352                        );
353                    }
354                }
355            }
356            Err(e) => {
357                println!(
358                    "cargo::warning=Could not open {} file to read libraries to link: {}",
359                    &prl_path, e
360                );
361            }
362        }
363    }
364
365    fn qmake_query(&self, var_name: &str) -> String {
366        String::from_utf8_lossy(
367            &Command::new(&self.qmake_path)
368                .args(["-query", var_name])
369                .output()
370                .unwrap()
371                .stdout,
372        )
373        .trim()
374        .to_owned()
375    }
376
377    fn try_qmake_find_tool(&self, tool_name: &str) -> anyhow::Result<PathBuf> {
378        // "qmake -query" exposes a list of paths that describe where Qt executables and libraries
379        // are located, as well as where new executables & libraries should be installed to.
380        // We can use these variables to find any Qt tool.
381        //
382        // The order is important here.
383        // First, we check the _HOST_ variables.
384        // In cross-compilation contexts, these variables should point to the host toolchain used
385        // for building. The _INSTALL_ directories describe where to install new binaries to
386        // (i.e. the target directories).
387        // We still use the _INSTALL_ paths as fallback.
388        //
389        // The _LIBEXECS variables point to the executable Qt-internal tools (i.e. moc and
390        // friends), whilst _BINS point to the developer-facing executables (qdoc, qmake, etc.).
391        // As we mostly use the Qt-internal tools in this library, check _LIBEXECS first.
392        //
393        // Furthermore, in some contexts these variables include a `/get` variant.
394        // This is important for contexts where qmake and the Qt build tools do not have a static
395        // location, but are moved around during building.
396        // This notably happens with yocto builds.
397        // For each package, yocto builds a `sysroot` folder for both the host machine, as well
398        // as the target. This is done to keep package builds reproducable & separate.
399        // As a result the qmake executable is copied into each host sysroot for building.
400        //
401        // In this case the variables compiled into qmake still point to the paths relative
402        // from the host sysroot (e.g. /usr/bin).
403        // The /get variant in comparison will "get" the right full path from the current environment.
404        // Therefore prefer to use the `/get` variant when available.
405        // See: https://github.com/KDAB/cxx-qt/pull/430
406        //
407        // To check & debug all variables available on your system, simply run:
408        //
409        //              qmake -query
410        let mut failed_paths = vec![];
411        [
412            "QT_HOST_LIBEXECS/get",
413            "QT_HOST_LIBEXECS",
414            "QT_HOST_BINS/get",
415            "QT_HOST_BINS",
416            "QT_INSTALL_LIBEXECS/get",
417            "QT_INSTALL_LIBEXECS",
418            "QT_INSTALL_BINS/get",
419            "QT_INSTALL_BINS",
420        ]
421        .iter()
422        // Find the first valid executable path
423        .find_map(|qmake_query_var| {
424            let executable_path = PathBuf::from(self.qmake_query(qmake_query_var)).join(tool_name);
425            let test_output = Command::new(&executable_path).args(["-help"]).output();
426            match test_output {
427                Err(_err) => {
428                    failed_paths.push(executable_path);
429                    None
430                }
431                Ok(_) => Some(executable_path),
432            }
433        })
434        .ok_or_else(|| anyhow::anyhow!("Failed to find {tool_name}, tried: {failed_paths:?}"))
435    }
436}