Skip to content

qt-build-utils: build system changes to have a QtInstallation #1230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/qt-build-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ 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

[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
Expand All @@ -31,6 +35,7 @@ thiserror.workspace = true
#
# When linking Qt dynamically, this makes no difference.
link_qt_object_files = []
qmake = []
serde = ["dep:serde"]

[lints]
Expand Down
43 changes: 43 additions & 0 deletions crates/qt-build-utils/src/installation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

#[cfg(feature = "qmake")]
pub(crate) mod qmake;

use semver::Version;
use std::path::{Path, PathBuf};

use crate::{Initializer, MocArguments, MocProducts, QmlModuleRegistrationFiles};

/// 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: Vec<String>) -> Vec<PathBuf>;
/// Configure the given cc::Build and cargo to link to the given Qt modules
fn link_modules(&self, builder: &mut cc::Build, qt_modules: Vec<String>);
/// 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
fn moc(&self, input_file: &Path, arguments: MocArguments) -> MocProducts;
/// TODO
fn qml_cache_gen(&self) -> PathBuf;
/// TODO
fn qml_type_registrar(
&self,
qml_types: &Path,
version_major: u64,
version_minor: u64,
uri: &str,
) -> PathBuf;
/// TODO
/// TODO: instead just return the object file?
fn qrc(&self, input_file: &Path) -> Initializer;
/// Version of the detected Qt installation
fn version(&self) -> Version;
}
209 changes: 209 additions & 0 deletions crates/qt-build-utils/src/installation/qmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use semver::Version;
use std::{
env,
io::ErrorKind,
path::{Path, PathBuf},
process::Command,
};

use crate::{Initializer, MocArguments, MocProducts, QtBuildError, QtInstallation};

/// 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 Err so try to create installation if currently an error
|prev: anyhow::Result<Self>| {
prev.or_else(|_| 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),
// TODO: do we need to trim the input?
Ok(output) => Ok(Version::parse(&String::from_utf8_lossy(&output.stdout))?),
}?;

// 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: Vec<String>) -> Vec<PathBuf> {
todo!()
}

fn link_modules(&self, builder: &mut cc::Build, qt_modules: Vec<String>) {
todo!()
}

fn moc(&self, input_file: &Path, arguments: MocArguments) -> MocProducts {
// TODO: do we need to cache these like before, or is this generally fast?
let moc_executable = self.qmake_find_tool("moc").expect("Could not find moc");
todo!()
}

fn qml_cache_gen(&self) -> PathBuf {
todo!()
}

fn qml_type_registrar(
&self,
qml_types: &Path,
version_major: u64,
version_minor: u64,
uri: &str,
) -> PathBuf {
todo!()
}

fn qrc(&self, input_file: &Path) -> Initializer {
// TODO: do we need to cache these like before, or is this generally fast?
let rcc_executable = self.qmake_find_tool("rcc").expect("Could not find rcc");
todo!()

// TODO: this should also scan the qrc file and call rerun-if-changed?
}

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 qmake_find_tool(&self, tool_name: &str) -> Option<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
[
"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 = format!("{}/{tool_name}", self.qmake_query(qmake_query_var));
Command::new(&executable_path)
.args(["-help"])
.output()
.map(|_| executable_path)
.ok()
})
}
}
Loading
Loading