1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// 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::{
cell::RefCell, collections::HashMap, env, io::ErrorKind, path::PathBuf, process::Command,
};
use crate::{QtBuildError, QtInstallation, QtTool};
/// A implementation of [QtInstallation] using qmake
pub struct QtInstallationQMake {
qmake_path: PathBuf,
qmake_version: Version,
// Internal cache of paths for tools
//
// 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 {
/// 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
if let Some(result) = Self::try_from_qmake_env() {
return result;
}
// Try variable candidates within the patch
Self::try_from_path()
}
pub(crate) fn try_from_path() -> anyhow::Result<Self> {
["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| {
if let Ok(qmake_path) = which::which(qmake_path) {
Some(acc.map_or_else(
// Value is None so try to create installation
|| QtInstallationQMake::try_from(qmake_path.clone()),
// 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(qmake_path.clone()))
},
))
} else {
acc
}
})
.unwrap_or_else(|| Err(QtBuildError::QtMissing.into()))
}
pub(crate) fn try_from_qmake_env() -> Option<anyhow::Result<Self>> {
println!("cargo::rerun-if-env-changed=QMAKE");
if let Ok(qmake_env_var) = env::var("QMAKE") {
return Some(
QtInstallationQMake::try_from(PathBuf::from(&qmake_env_var)).map_err(|err| {
QtBuildError::QMakeSetQtMissing {
qmake_env_var,
error: err.into(),
}
.into()
}),
);
}
None
}
}
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"])
// Binaries should work without environment and this prevents
// LD_LIBRARY_PATH from causing different Qt version clashes
.env_clear()
.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,
tool_cache: HashMap::default().into(),
})
}
}
impl QtInstallation for QtInstallationQMake {
fn framework_paths(&self, qt_modules: &[String]) -> Vec<PathBuf> {
let path_lib = PathBuf::from(self.qmake_query("QT_INSTALL_LIBS"));
super::shared::framework_paths_for_qt_modules(qt_modules, path_lib)
}
fn include_paths(&self, qt_modules: &[String]) -> Vec<PathBuf> {
let path_include = PathBuf::from(self.qmake_query("QT_INSTALL_HEADERS"));
let path_lib = PathBuf::from(self.qmake_query("QT_INSTALL_LIBS"));
super::shared::include_paths_for_qt_modules(qt_modules, path_include, path_lib)
}
fn link_modules(&self, builder: &mut cc::Build, qt_modules: &[String]) {
let path_frameworks = self.framework_paths(qt_modules);
let path_lib = PathBuf::from(self.qmake_query("QT_INSTALL_LIBS"));
let path_prefix = PathBuf::from(self.qmake_query("QT_INSTALL_PREFIX"));
let path_plugins = PathBuf::from(self.qmake_query("QT_INSTALL_PLUGINS"));
let qt_version = self.qmake_version.clone();
super::shared::link_for_qt_modules(
builder,
qt_modules,
path_frameworks,
path_lib,
path_prefix,
path_plugins,
qt_version,
);
}
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
let Ok(mut tool_cache) = self.tool_cache.try_borrow_mut() else {
return find_tool();
};
// Read the tool from the cache or insert
if let Some(path) = tool_cache.get(&tool) {
return Ok(path.clone());
}
let path = find_tool()?;
tool_cache.insert(tool, path.clone());
Ok(path)
}
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])
// Binaries should work without environment and this prevents
// LD_LIBRARY_PATH from causing different Qt version clashes
.env_clear()
.output()
.unwrap()
.stdout,
)
.trim()
.to_owned()
}
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.
//
// 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
let mut failed_paths = vec![];
[
"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);
let test_output = Command::new(&executable_path)
.args(["-help"])
// Binaries should work without environment and this prevents
// LD_LIBRARY_PATH from causing different Qt version clashes
.env_clear()
.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:?}"))
}
}