diff --git a/CMakeLists.txt b/CMakeLists.txt index 46c08a491..58632b7d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,9 +195,9 @@ endif() set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/${BUILD_DIR}/cargo/build") # Add CMake tests for `cargo test/clippy/fmt/doc`. -add_test(NAME cargo_tests COMMAND cargo test --all-targets --target-dir +add_test(NAME cargo_tests COMMAND cargo test --features link_qt_object_files --all-targets --target-dir ${CARGO_TARGET_DIR}) -add_test(NAME cargo_doc_tests COMMAND cargo test --doc --target-dir +add_test(NAME cargo_doc_tests COMMAND cargo test --features link_qt_object_files --doc --target-dir ${CARGO_TARGET_DIR}) add_test(NAME cargo_doc COMMAND cargo doc --workspace --target-dir ${CARGO_TARGET_DIR}) add_test(NAME cargo_clippy COMMAND cargo clippy --all-targets --target-dir diff --git a/crates/cxx-qt-build/Cargo.toml b/crates/cxx-qt-build/Cargo.toml index cfa20e682..d26fbdb2b 100644 --- a/crates/cxx-qt-build/Cargo.toml +++ b/crates/cxx-qt-build/Cargo.toml @@ -28,3 +28,4 @@ version_check = "0.9" default = ["qt_gui", "qt_qml"] qt_gui = ["cxx-qt-lib-headers/qt_gui"] qt_qml = ["cxx-qt-lib-headers/qt_qml"] +link_qt_object_files = ["qt-build-utils/link_qt_object_files"] diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs index d7cdd06b0..013903a80 100644 --- a/crates/cxx-qt-build/src/lib.rs +++ b/crates/cxx-qt-build/src/lib.rs @@ -399,7 +399,7 @@ impl CxxQtBuilder { let mut qtbuild = qt_build_utils::QtBuild::new(self.qt_modules.into_iter().collect()) .expect("Could not find Qt installation"); - qtbuild.cargo_link_libraries(); + qtbuild.cargo_link_libraries(&mut self.cc_builder); // Write cxx-qt-gen, cxx-qt-lib and cxx headers cxx_qt_gen::write_headers(format!("{header_root}/cxx-qt-common")); diff --git a/crates/cxx-qt-lib/Cargo.toml b/crates/cxx-qt-lib/Cargo.toml index 5387dadfc..5a3651980 100644 --- a/crates/cxx-qt-lib/Cargo.toml +++ b/crates/cxx-qt-lib/Cargo.toml @@ -40,3 +40,4 @@ qt_gui = ["cxx-qt-lib-headers/qt_gui"] qt_qml = ["cxx-qt-lib-headers/qt_qml"] time = ["dep:time"] url = ["dep:url"] +link_qt_object_files = ["qt-build-utils/link_qt_object_files"] diff --git a/crates/cxx-qt-lib/build.rs b/crates/cxx-qt-lib/build.rs index 8a1cb2253..1234c912d 100644 --- a/crates/cxx-qt-lib/build.rs +++ b/crates/cxx-qt-lib/build.rs @@ -20,7 +20,7 @@ fn main() { } let qtbuild = qt_build_utils::QtBuild::new(qt_modules).expect("Could not find Qt installation"); - qtbuild.cargo_link_libraries(); + // Required for tests qt_build_utils::setup_linker(); @@ -192,6 +192,8 @@ fn main() { let mut builder = cxx_build::bridges(rust_bridges.iter().map(|bridge| format!("src/{bridge}.rs"))); + qtbuild.cargo_link_libraries(&mut builder); + let mut cpp_files = vec![ "core/qbytearray", "core/qcoreapplication", diff --git a/crates/qt-build-utils/Cargo.toml b/crates/qt-build-utils/Cargo.toml index fd7777086..91e964631 100644 --- a/crates/qt-build-utils/Cargo.toml +++ b/crates/qt-build-utils/Cargo.toml @@ -13,5 +13,19 @@ description = "Build script helper for linking Qt libraries and using moc code g repository.workspace = true [dependencies] +cc = "1.0.74" versions = "4.1.0" thiserror = "1.0" + +[features] +# 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 +# to initialize Qt resources embedded within Qt libraries). +# +# CMake also links those .o files when linking Qt's targets, so this +# feature must be disabled for staticlib crates. Otherwise, linking +# will fail with duplicate symbol errors. +# +# When linking Qt dynamically, this makes no difference. +link_qt_object_files = [] diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs index c0e3566f4..aab95a0f8 100644 --- a/crates/qt-build-utils/src/lib.rs +++ b/crates/qt-build-utils/src/lib.rs @@ -306,8 +306,78 @@ impl QtBuild { .to_string() } + fn cargo_link_qt_library( + &self, + name: &str, + prefix_path: &str, + lib_path: &str, + link_lib: &str, + prl_path: &str, + builder: &mut cc::Build, + ) { + 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( + name, + line.replace(r"$$[QT_INSTALL_LIBS]", lib_path) + .replace(r"$$[QT_INSTALL_PREFIX]", prefix_path) + .as_bytes(), + builder, + ); + } + } + } + Err(e) => { + println!( + "cargo:warning=Could not open {} file to read libraries to link: {}", + &prl_path, e + ); + } + } + } + + /// Some prl files include their architecture in their naming scheme. + /// Just try all known architectures and fallback to non when they all failed. + fn find_qt_module_prl( + &self, + lib_path: &str, + prefix: &str, + version_major: u32, + qt_module: &str, + ) -> String { + for arch in ["", "_arm64-v8a", "_armeabi-v7a", "_x86", "_x86_64"] { + let prl_path = format!( + "{}/{}Qt{}{}{}.prl", + lib_path, prefix, version_major, qt_module, arch + ); + match Path::new(&prl_path).try_exists() { + Ok(exists) => { + if exists { + return prl_path; + } + } + Err(e) => { + println!( + "cargo:warning=failed checking for existence of {}: {}", + prl_path, e + ); + } + } + } + + format!( + "{}/{}Qt{}{}.prl", + lib_path, prefix, version_major, qt_module + ) + } + /// Tell Cargo to link each Qt module. - pub fn cargo_link_libraries(&self) { + pub fn cargo_link_libraries(&self, builder: &mut cc::Build) { + let prefix_path = self.qmake_query("QT_INSTALL_PREFIX"); let lib_path = self.qmake_query("QT_INSTALL_LIBS"); println!("cargo:rustc-link-search={lib_path}"); @@ -343,35 +413,35 @@ impl QtBuild { } else { ( format!("Qt{}{qt_module}", self.version.major), - format!( - "{}/{}Qt{}{}.prl", - lib_path, prefix, self.version.major, qt_module - ), + self.find_qt_module_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{}{qt_module}", self.version.major), - 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 - ); - } - } + self.cargo_link_qt_library( + &format!("Qt{}{qt_module}", self.version.major), + &prefix_path, + &lib_path, + &link_lib, + &prl_path, + builder, + ); + } + + let emscripten_targeted = match env::var("CARGO_CFG_TARGET_OS") { + Ok(val) => val == "emscripten", + Err(_) => false, + }; + if emscripten_targeted { + let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS")); + println!("cargo:rustc-link-search={platforms_path}"); + self.cargo_link_qt_library( + "qwasm", + &prefix_path, + &lib_path, + "qwasm", + &format!("{platforms_path}/libqwasm.prl"), + builder, + ); } } diff --git a/crates/qt-build-utils/src/parse_cflags.rs b/crates/qt-build-utils/src/parse_cflags.rs index 0346380af..cbb01b565 100644 --- a/crates/qt-build-utils/src/parse_cflags.rs +++ b/crates/qt-build-utils/src/parse_cflags.rs @@ -10,6 +10,12 @@ use std::env; +#[cfg(feature = "link_qt_object_files")] +use std::{collections::HashSet, sync::OnceLock}; + +#[cfg(feature = "link_qt_object_files")] +static mut LINKED_OBJECT_FILES: OnceLock> = OnceLock::new(); + /// Extract the &str to pass to cargo:rustc-link-lib from a filename (just the file name, not including directories) /// using target-specific logic. fn extract_lib_from_filename<'a>(target: &str, filename: &'a str) -> Option<&'a str> { @@ -103,7 +109,7 @@ fn split_flags(link_args: &[u8]) -> Vec { words } -pub(crate) fn parse_libs_cflags(name: &str, link_args: &[u8]) { +pub(crate) fn parse_libs_cflags(name: &str, link_args: &[u8], _builder: &mut cc::Build) { let mut is_msvc = false; let target = env::var("TARGET"); if let Ok(target) = &target { @@ -162,18 +168,40 @@ pub(crate) fn parse_libs_cflags(name: &str, link_args: &[u8]) { if path.is_file() { // Cargo doesn't have a means to directly specify a file path to link, // so split up the path into the parent directory and library name. - // TODO: pass file path directly when link-arg library type is stabilized - // https://github.com/rust-lang/rust/issues/99427 if let (Some(dir), Some(file_name), Ok(target)) = (path.parent(), path.file_name(), &target) { - match extract_lib_from_filename(target, &file_name.to_string_lossy()) { - Some(lib_basename) => { - println!("cargo:rustc-link-search={}", dir.display()); - println!("cargo:rustc-link-lib={lib_basename}"); + let file_name = file_name.to_string_lossy(); + if file_name.ends_with(".o") { + #[cfg(feature = "link_qt_object_files")] + { + let path_string = path.to_string_lossy().to_string(); + unsafe { + // Linking will fail with duplicate symbol errors if the same .o file is linked twice. + // Many of Qt's .prl files repeat listing .o files that other .prl files also list. + let already_linked_object_files = + LINKED_OBJECT_FILES.get_or_init(HashSet::new); + if !already_linked_object_files.contains(&path_string) { + // Cargo doesn't have a means to directly specify an object to link, + // so use the cc crate to specify it instead. + // TODO: pass file path directly when link-arg library type is stabilized + // https://github.com/rust-lang/rust/issues/99427#issuecomment-1562092085 + // TODO: remove builder argument when it's not used anymore to link object files. + // also remove the dependency on cc when this is done + _builder.object(path); + } + LINKED_OBJECT_FILES.get_mut().unwrap().insert(path_string); + } } - None => { - println!("cargo:warning=File path {} found in .prl file for {name}, but could not extract library base name to pass to linker command line", path.display()); + } else { + match extract_lib_from_filename(target, &file_name) { + Some(lib_basename) => { + println!("cargo:rustc-link-search={}", dir.display()); + println!("cargo:rustc-link-lib={lib_basename}"); + } + None => { + println!("cargo:warning=File path {} found in .prl file for {name}, but could not extract library base name to pass to linker command line", path.display()); + } } } } diff --git a/examples/cargo_without_cmake/Cargo.toml b/examples/cargo_without_cmake/Cargo.toml index 33a3356f1..5ee971bb8 100644 --- a/examples/cargo_without_cmake/Cargo.toml +++ b/examples/cargo_without_cmake/Cargo.toml @@ -27,5 +27,6 @@ cxx-qt-lib.workspace = true [build-dependencies] # Use `cxx-qt-build = "0.5"` here instead! -cxx-qt-build.workspace = true +# The link_qt_object_files feature is required for statically linking Qt 6. +cxx-qt-build = { workspace = true, features = [ "link_qt_object_files" ] } # ANCHOR_END: book_cargo_toml_no_cmake diff --git a/examples/demo_threading/rust/Cargo.toml b/examples/demo_threading/rust/Cargo.toml index d2a16d2a3..6bc339a2c 100644 --- a/examples/demo_threading/rust/Cargo.toml +++ b/examples/demo_threading/rust/Cargo.toml @@ -25,3 +25,6 @@ uuid = { version = "1.2", features = ["serde", "v4"] } [build-dependencies] cxx-qt-build.workspace = true + +[features] +link_qt_object_files = [ "cxx-qt-build/link_qt_object_files" ] diff --git a/examples/qml_extension_plugin/plugin/rust/Cargo.toml b/examples/qml_extension_plugin/plugin/rust/Cargo.toml index 7f3a82dee..2bb281d60 100644 --- a/examples/qml_extension_plugin/plugin/rust/Cargo.toml +++ b/examples/qml_extension_plugin/plugin/rust/Cargo.toml @@ -22,3 +22,6 @@ serde_json.workspace = true [build-dependencies] cxx-qt-build.workspace = true + +[features] +link_qt_object_files = [ "cxx-qt-build/link_qt_object_files" ] diff --git a/examples/qml_features/rust/Cargo.toml b/examples/qml_features/rust/Cargo.toml index 7282ec160..62a0c5f46 100644 --- a/examples/qml_features/rust/Cargo.toml +++ b/examples/qml_features/rust/Cargo.toml @@ -22,3 +22,6 @@ serde_json.workspace = true [build-dependencies] cxx-qt-build.workspace = true + +[features] +link_qt_object_files = [ "cxx-qt-build/link_qt_object_files" ] diff --git a/examples/qml_minimal/rust/Cargo.toml b/examples/qml_minimal/rust/Cargo.toml index 5be6184ba..008ebfa91 100644 --- a/examples/qml_minimal/rust/Cargo.toml +++ b/examples/qml_minimal/rust/Cargo.toml @@ -38,6 +38,10 @@ cxx-qt-lib.workspace = true [build-dependencies] # Use `cxx-qt-build = "0.5"` here instead! cxx-qt-build.workspace = true + +[features] +# This feature must be enabled for `cargo test` when linking Qt 6 statically. +link_qt_object_files = [ "cxx-qt-build/link_qt_object_files" ] # ANCHOR_END: book_build_dependencies # ANCHOR_END: book_all