mod parse_cflags;
use std::{
env,
path::{Path, PathBuf},
process::Command,
};
pub use versions::SemVer;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum QtBuildError {
#[error("QMAKE environment variable specified as {qmake_env_var} but could not detect Qt: {error:?}")]
QMakeSetQtMissing {
qmake_env_var: String,
error: Box<QtBuildError>,
},
#[error("Could not find Qt")]
QtMissing,
#[error("Executing `qmake -query` failed: {0:?}")]
QmakeFailed(#[from] std::io::Error),
#[error("QT_VERSION_MAJOR environment variable specified as {qt_version_major_env_var} but could not parse as integer: {source:?}")]
QtVersionMajorInvalid {
qt_version_major_env_var: String,
source: std::num::ParseIntError,
},
#[error("qmake version ({qmake_version}) does not match version specified by QT_VERISON_MAJOR ({qt_version_major})")]
QtVersionMajorDoesNotMatch {
qmake_version: u32,
qt_version_major: u32,
},
}
pub struct QtBuild {
version: SemVer,
qmake_executable: String,
moc_executable: Option<String>,
rcc_executable: Option<String>,
qt_modules: Vec<String>,
}
impl QtBuild {
pub fn new(mut qt_modules: Vec<String>) -> Result<Self, QtBuildError> {
if qt_modules.is_empty() {
qt_modules.push("Core".to_string());
}
println!("cargo:rerun-if-env-changed=QMAKE");
println!("cargo:rerun-if-env-changed=QT_VERSION_MAJOR");
fn verify_candidate(candidate: &str) -> Result<(&str, versions::SemVer), QtBuildError> {
match Command::new(candidate)
.args(["-query", "QT_VERSION"])
.output()
{
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(QtBuildError::QtMissing),
Err(e) => Err(QtBuildError::QmakeFailed(e)),
Ok(output) => {
if output.status.success() {
let version_string = std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.to_string();
let qmake_version = versions::SemVer::new(&version_string).unwrap();
if let Ok(env_version) = env::var("QT_VERSION_MAJOR") {
let env_version = match env_version.trim().parse::<u32>() {
Err(e) if *e.kind() == std::num::IntErrorKind::Empty => {
println!(
"cargo:warning=QT_VERSION_MAJOR environment variable defined but empty"
);
return Ok((candidate, qmake_version));
}
Err(e) => {
return Err(QtBuildError::QtVersionMajorInvalid {
qt_version_major_env_var: env_version,
source: e,
})
}
Ok(int) => int,
};
if env_version == qmake_version.major {
return Ok((candidate, qmake_version));
} else {
return Err(QtBuildError::QtVersionMajorDoesNotMatch {
qmake_version: qmake_version.major,
qt_version_major: env_version,
});
}
}
Ok((candidate, qmake_version))
} else {
Err(QtBuildError::QtMissing)
}
}
}
}
if let Ok(qmake_env_var) = env::var("QMAKE") {
match verify_candidate(qmake_env_var.trim()) {
Ok((executable_name, version)) => {
return Ok(Self {
qmake_executable: executable_name.to_string(),
moc_executable: None,
rcc_executable: None,
version,
qt_modules,
});
}
Err(e) => {
return Err(QtBuildError::QMakeSetQtMissing {
qmake_env_var,
error: Box::new(e),
})
}
}
}
let candidate_executable_names = ["qmake6", "qmake-qt5", "qmake"];
for (index, executable_name) in candidate_executable_names.iter().enumerate() {
match verify_candidate(executable_name) {
Ok((executable_name, version)) => {
return Ok(Self {
qmake_executable: executable_name.to_string(),
moc_executable: None,
rcc_executable: None,
version,
qt_modules,
});
}
Err(QtBuildError::QtVersionMajorDoesNotMatch {
qmake_version,
qt_version_major,
}) => {
if index == candidate_executable_names.len() - 1 {
return Err(QtBuildError::QtVersionMajorDoesNotMatch {
qmake_version,
qt_version_major,
});
}
eprintln!("Candidate qmake executable `{executable_name}` is for Qt{qmake_version} but QT_VERISON_MAJOR environment variable specified as {qt_version_major}. Trying next candidate executable name `{}`...", candidate_executable_names[index + 1]);
continue;
}
Err(QtBuildError::QtMissing) => continue,
Err(e) => return Err(e),
}
}
Err(QtBuildError::QtMissing)
}
pub fn qmake_query(&self, var_name: &str) -> String {
std::str::from_utf8(
&Command::new(&self.qmake_executable)
.args(["-query", var_name])
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_string()
}
pub fn cargo_link_libraries(&self) {
let lib_path = self.qmake_query("QT_INSTALL_LIBS");
println!("cargo:rustc-link-search={}", lib_path);
let target = env::var("TARGET");
let prefix = match &target {
Ok(target) => {
if target.contains("msvc") {
""
} else {
"lib"
}
}
Err(_) => "lib",
};
for qt_module in &self.qt_modules {
let framework = match &target {
Ok(target) => {
if target.contains("apple") {
Path::new(&format!("{}/Qt{}.framework", lib_path, qt_module)).exists()
} else {
false
}
}
Err(_) => false,
};
let (link_lib, prl_path) = if framework {
(
format!("framework=Qt{}", qt_module),
format!(
"{}/Qt{}.framework/Resources/Qt{}.prl",
lib_path, qt_module, qt_module
),
)
} else {
(
format!("Qt{}{}", self.version.major, qt_module),
format!(
"{}/{}Qt{}{}.prl",
lib_path, prefix, self.version.major, qt_module
),
)
};
println!("cargo:rustc-link-lib={}", link_lib);
match std::fs::read_to_string(&prl_path) {
Ok(prl) => {
for line in prl.lines() {
if let Some(line) = line.strip_prefix("QMAKE_PRL_LIBS = ") {
parse_cflags::parse_libs_cflags(
&format!("Qt{}{}", self.version.major, qt_module),
line.replace(r"$$[QT_INSTALL_LIBS]", &lib_path)
.replace(r"$$[QT_INSTALL_PREFIX]", &lib_path)
.as_bytes(),
);
}
}
}
Err(e) => {
println!(
"cargo:warning=Could not open {} file to read libraries to link: {}",
&prl_path, e
);
}
}
}
}
pub fn include_paths(&self) -> Vec<PathBuf> {
let root_path = self.qmake_query("QT_INSTALL_HEADERS");
let mut paths = Vec::new();
for qt_module in &self.qt_modules {
paths.push(format!("{}/Qt{}", root_path, qt_module));
}
paths.push(root_path);
paths.iter().map(PathBuf::from).collect()
}
pub fn version(&self) -> &SemVer {
&self.version
}
fn get_qt_tool(&self, tool_name: &str) -> Result<String, ()> {
for qmake_query_var in [
"QT_HOST_LIBEXECS",
"QT_HOST_BINS",
"QT_INSTALL_LIBEXECS",
"QT_INSTALL_BINS",
] {
let executable_path = format!("{}/{}", self.qmake_query(qmake_query_var), tool_name);
match Command::new(&executable_path).args(["-help"]).output() {
Ok(_) => return Ok(executable_path),
Err(_) => continue,
}
}
Err(())
}
pub fn moc(&mut self, input_file: impl AsRef<Path>) -> PathBuf {
if self.moc_executable.is_none() {
self.moc_executable = Some(self.get_qt_tool("moc").expect("Could not find moc"));
}
let input_path = input_file.as_ref();
let output_path = PathBuf::from(&format!(
"{}/moc_{}.cpp",
env::var("OUT_DIR").unwrap(),
input_path.file_name().unwrap().to_str().unwrap()
));
let _ = Command::new(self.moc_executable.as_ref().unwrap())
.args([
input_path.to_str().unwrap(),
"-o",
output_path.to_str().unwrap(),
])
.output()
.unwrap_or_else(|_| panic!("moc failed for {}", input_path.display()));
output_path
}
pub fn qrc(&mut self, input_file: &impl AsRef<Path>) -> PathBuf {
if self.rcc_executable.is_none() {
self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc"));
}
let input_path = input_file.as_ref();
let output_path = PathBuf::from(&format!(
"{}/{}.cpp",
env::var("OUT_DIR").unwrap(),
input_path.file_name().unwrap().to_str().unwrap()
));
let _ = Command::new(self.rcc_executable.as_ref().unwrap())
.args([
input_path.to_str().unwrap(),
"-o",
output_path.to_str().unwrap(),
])
.output()
.unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));
output_path
}
}