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}