Skip to content

Commit a89e9fa

Browse files
committed
qt-build-utils: add qmake implementation of QtInstallation
1 parent 708a788 commit a89e9fa

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed

crates/qt-build-utils/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ versions = "6.3"
2222
thiserror.workspace = true
2323

2424
[features]
25+
# TODO: should we default to qmake or let downstream crates specify, such as cxx-qt-build
26+
default = ["qmake"]
2527
# When Cargo links an executable, whether a bin crate or test executable,
2628
# and Qt 6 is linked statically, this feature must be enabled to link
2729
# unarchived .o files with static symbols that Qt ships (for example
@@ -33,6 +35,7 @@ thiserror.workspace = true
3335
#
3436
# When linking Qt dynamically, this makes no difference.
3537
link_qt_object_files = []
38+
qmake = []
3639
serde = ["dep:serde"]
3740

3841
[lints]

crates/qt-build-utils/src/installation/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
//
44
// SPDX-License-Identifier: MIT OR Apache-2.0
55

6+
#[cfg(feature = "qmake")]
7+
pub(crate) mod qmake;
8+
69
use semver::Version;
710
use std::path::{Path, PathBuf};
811

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
2+
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
3+
//
4+
// SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
use semver::Version;
7+
use std::{
8+
env,
9+
io::ErrorKind,
10+
path::{Path, PathBuf},
11+
process::Command,
12+
};
13+
14+
use crate::{Initializer, MocArguments, MocProducts, QtBuildError, QtInstallation};
15+
16+
/// TODO
17+
pub struct QtInstallationQMake {
18+
qmake_path: PathBuf,
19+
qmake_version: Version,
20+
}
21+
22+
impl QtInstallationQMake {
23+
/// TODO
24+
pub fn new() -> anyhow::Result<Self> {
25+
// Try the QMAKE variable first
26+
println!("cargo::rerun-if-env-changed=QMAKE");
27+
if let Ok(qmake_env_var) = env::var("QMAKE") {
28+
return QtInstallationQMake::try_from(PathBuf::from(&qmake_env_var)).map_err(|err| {
29+
QtBuildError::QMakeSetQtMissing {
30+
qmake_env_var,
31+
error: err.into(),
32+
}
33+
.into()
34+
});
35+
}
36+
37+
// Try variable candidates within the patch
38+
["qmake6", "qmake-qt5", "qmake"]
39+
.iter()
40+
// Use the first non-errored installation
41+
// If there are no valid installations we display the last error
42+
.fold(None, |acc, qmake_path| {
43+
Some(acc.map_or_else(
44+
// Value is None so try to create installation
45+
|| QtInstallationQMake::try_from(PathBuf::from(qmake_path)),
46+
// Value is Err so try to create installation if currently an error
47+
|prev: anyhow::Result<Self>| {
48+
prev.or_else(|_| QtInstallationQMake::try_from(PathBuf::from(qmake_path)))
49+
},
50+
))
51+
})
52+
.unwrap_or_else(|| Err(QtBuildError::QtMissing.into()))
53+
}
54+
}
55+
56+
impl TryFrom<PathBuf> for QtInstallationQMake {
57+
type Error = anyhow::Error;
58+
59+
fn try_from(qmake_path: PathBuf) -> anyhow::Result<Self> {
60+
// Attempt to read the QT_VERSION from qmake
61+
let qmake_version = match Command::new(&qmake_path)
62+
.args(["-query", "QT_VERSION"])
63+
.output()
64+
{
65+
Err(e) if e.kind() == ErrorKind::NotFound => Err(QtBuildError::QtMissing),
66+
Err(e) => Err(QtBuildError::QmakeFailed(e)),
67+
Ok(output) if !output.status.success() => Err(QtBuildError::QtMissing),
68+
// TODO: do we need to trim the input?
69+
Ok(output) => Ok(Version::parse(&String::from_utf8_lossy(&output.stdout))?),
70+
}?;
71+
72+
// Check QT_VERSION_MAJOR is the same as the qmake version
73+
println!("cargo::rerun-if-env-changed=QT_VERSION_MAJOR");
74+
if let Ok(env_qt_version_major) = env::var("QT_VERSION_MAJOR") {
75+
// Parse to an integer
76+
let env_qt_version_major = env_qt_version_major.trim().parse::<u64>().map_err(|e| {
77+
QtBuildError::QtVersionMajorInvalid {
78+
qt_version_major_env_var: env_qt_version_major,
79+
source: e,
80+
}
81+
})?;
82+
83+
// Ensure the version major is the same
84+
if qmake_version.major != env_qt_version_major {
85+
return Err(QtBuildError::QtVersionMajorDoesNotMatch {
86+
qmake_version: qmake_version.major,
87+
qt_version_major: env_qt_version_major,
88+
}
89+
.into());
90+
}
91+
}
92+
93+
Ok(Self {
94+
qmake_path,
95+
qmake_version,
96+
})
97+
}
98+
}
99+
100+
impl QtInstallation for QtInstallationQMake {
101+
fn include_paths(&self, qt_modules: Vec<String>) -> Vec<PathBuf> {
102+
todo!()
103+
}
104+
105+
fn link_modules(&self, builder: &mut cc::Build, qt_modules: Vec<String>) {
106+
todo!()
107+
}
108+
109+
fn moc(&self, input_file: &Path, arguments: MocArguments) -> MocProducts {
110+
// TODO: do we need to cache these like before, or is this generally fast?
111+
let moc_executable = self.qmake_find_tool("moc").expect("Could not find moc");
112+
todo!()
113+
}
114+
115+
fn qml_cache_gen(&self) -> PathBuf {
116+
todo!()
117+
}
118+
119+
fn qml_type_registrar(
120+
&self,
121+
qml_types: &Path,
122+
version_major: u64,
123+
version_minor: u64,
124+
uri: &str,
125+
) -> PathBuf {
126+
todo!()
127+
}
128+
129+
fn qrc(&self, input_file: &Path) -> Initializer {
130+
// TODO: do we need to cache these like before, or is this generally fast?
131+
let rcc_executable = self.qmake_find_tool("rcc").expect("Could not find rcc");
132+
todo!()
133+
134+
// TODO: this should also scan the qrc file and call rerun-if-changed?
135+
}
136+
137+
fn version(&self) -> semver::Version {
138+
self.qmake_version.clone()
139+
}
140+
}
141+
142+
impl QtInstallationQMake {
143+
fn qmake_query(&self, var_name: &str) -> String {
144+
String::from_utf8_lossy(
145+
&Command::new(&self.qmake_path)
146+
.args(["-query", var_name])
147+
.output()
148+
.unwrap()
149+
.stdout,
150+
)
151+
.trim()
152+
.to_string()
153+
}
154+
155+
fn qmake_find_tool(&self, tool_name: &str) -> Option<String> {
156+
// "qmake -query" exposes a list of paths that describe where Qt executables and libraries
157+
// are located, as well as where new executables & libraries should be installed to.
158+
// We can use these variables to find any Qt tool.
159+
//
160+
// The order is important here.
161+
// First, we check the _HOST_ variables.
162+
// In cross-compilation contexts, these variables should point to the host toolchain used
163+
// for building. The _INSTALL_ directories describe where to install new binaries to
164+
// (i.e. the target directories).
165+
// We still use the _INSTALL_ paths as fallback.
166+
//
167+
// The _LIBEXECS variables point to the executable Qt-internal tools (i.e. moc and
168+
// friends), whilst _BINS point to the developer-facing executables (qdoc, qmake, etc.).
169+
// As we mostly use the Qt-internal tools in this library, check _LIBEXECS first.
170+
//
171+
// Furthermore, in some contexts these variables include a `/get` variant.
172+
// This is important for contexts where qmake and the Qt build tools do not have a static
173+
// location, but are moved around during building.
174+
// This notably happens with yocto builds.
175+
// For each package, yocto builds a `sysroot` folder for both the host machine, as well
176+
// as the target. This is done to keep package builds reproducable & separate.
177+
// As a result the qmake executable is copied into each host sysroot for building.
178+
//
179+
// In this case the variables compiled into qmake still point to the paths relative
180+
// from the host sysroot (e.g. /usr/bin).
181+
// The /get variant in comparison will "get" the right full path from the current environment.
182+
// Therefore prefer to use the `/get` variant when available.
183+
// See: https://github.com/KDAB/cxx-qt/pull/430
184+
//
185+
// To check & debug all variables available on your system, simply run:
186+
//
187+
// qmake -query
188+
[
189+
"QT_HOST_LIBEXECS/get",
190+
"QT_HOST_LIBEXECS",
191+
"QT_HOST_BINS/get",
192+
"QT_HOST_BINS",
193+
"QT_INSTALL_LIBEXECS/get",
194+
"QT_INSTALL_LIBEXECS",
195+
"QT_INSTALL_BINS/get",
196+
"QT_INSTALL_BINS",
197+
]
198+
.iter()
199+
// Find the first valid executable path
200+
.find_map(|qmake_query_var| {
201+
let executable_path = format!("{}/{tool_name}", self.qmake_query(qmake_query_var));
202+
Command::new(&executable_path)
203+
.args(["-help"])
204+
.output()
205+
.map(|_| executable_path)
206+
.ok()
207+
})
208+
}
209+
}

crates/qt-build-utils/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
mod installation;
1919
pub use installation::QtInstallation;
2020

21+
#[cfg(feature = "qmake")]
22+
pub use installation::qmake::QtInstallationQMake;
23+
2124
mod parse_cflags;
2225

2326
use std::{

0 commit comments

Comments
 (0)