From 58993d93e38700e4a4edee83d0d31cbab6fe7aba Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Mon, 25 Jul 2022 02:16:54 -0500 Subject: [PATCH 1/5] add qt-build helper crate for build.rs scripts This is intended to be shared between build.rs scripts regardless of whether cc, cpp_build, or cxx_build is used. --- CMakeLists.txt | 1 + qt-build/Cargo.toml | 20 ++++ qt-build/src/lib.rs | 282 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 qt-build/Cargo.toml create mode 100644 qt-build/src/lib.rs diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a2312e7d..2e6e21461 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,7 @@ add_test(NAME cxx_qt_gen_test_inputs_gen COMMAND rustfmt --check ${CXX_QT_GEN_TE add_test(NAME cxx_qt_gen_test_outputs_gen COMMAND rustfmt --check ${CXX_QT_GEN_TEST_OUTPUTS}) # QML example has add_test in it's CMakeLists, so just add the cargo tests here +add_test_cargo(qt-build "${CMAKE_CURRENT_SOURCE_DIR}/qt-build/Cargo.toml" DOCTESTS_ON) add_test_cargo(demo_threading "${CMAKE_CURRENT_SOURCE_DIR}/examples/demo_threading/Cargo.toml" DOCTESTS_OFF) add_test_cargo(qml_features "${CMAKE_CURRENT_SOURCE_DIR}/examples/qml_features/Cargo.toml" DOCTESTS_OFF) add_test_cargo(qml_extension_plugin "${CMAKE_CURRENT_SOURCE_DIR}/examples/qml_extension_plugin/core/Cargo.toml" DOCTESTS_OFF) diff --git a/qt-build/Cargo.toml b/qt-build/Cargo.toml new file mode 100644 index 000000000..2c98f8624 --- /dev/null +++ b/qt-build/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Be Wilson +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +[package] +name = "qt-build" +version = "0.3.0" +edition = "2018" +authors = ["Be Wilson "] +license = "MIT OR Apache-2.0" +description = "Build script helper for linking Qt libraries and using moc code generator. Intended to be used together with cc, cpp_build, or cxx_build" +repository = "https://github.com/KDAB/cxx-qt/" + +[dependencies] +versions = "4.1.0" +pkg-config = { git = "https://github.com/Be-ing/pkg-config-rs.git", branch = "library_pub" } +regex = "1.6.0" +lazy_static = "1.0" +thiserror = "1.0" diff --git a/qt-build/src/lib.rs b/qt-build/src/lib.rs new file mode 100644 index 000000000..587975780 --- /dev/null +++ b/qt-build/src/lib.rs @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Be Wilson +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! This crate provides information about the Qt installation and can invoke Qt's +//! [moc](https://doc.qt.io/qt-6/moc.html) code generator. This crate does not build +//! any C++ code on its own. It is intended to be used in [build.rs scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) +//! together with +//! [cc](https://docs.rs/cc/latest/cc/), +//! [cxx_build](https://docs.rs/cxx-build/latest/cxx_build/), or +//! [cpp_build](https://docs.rs/cpp_build/latest/cpp_build/). + +use std::{env, 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, + }, + #[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, + }, +} + +/// Helper for build.rs scripts using Qt +/// ``` +/// let qt_modules = vec!["Core", "Gui"] +/// .iter() +/// .map(|m| String::from(*m)) +/// .collect(); +/// let qtbuild = qt_build::QtBuild::new(qt_modules).expect("Could not find Qt installation"); +/// ``` +pub struct QtBuild { + version: SemVer, + qmake_executable: String, + qt_modules: Vec, +} + +impl QtBuild { + /// Search for where Qt is installed using qmake. Specify the Qt modules you are + /// linking with the `qt_modules` parameter, ommitting the `Qt` prefix (`"Core"` + /// rather than `"QtCore"`). After construction, use the [QtBuild::qmake_query] + /// method to get information about the Qt installation. + /// + /// 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, you can get this from the `Qt${QT_VERSION_MAJOR}::qmake` + /// target's `IMPORTED_LOCATION` property, for example: + /// ```cmake + /// find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) + /// find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) + /// get_target_property(QMAKE Qt${QT_VERSION_MAJOR}::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(mut qt_modules: Vec) -> Result { + 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::() { + Err(e) if *e.kind() == std::num::IntErrorKind::Empty => { + eprintln!( + "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(), + version, + qt_modules, + }); + } + Err(e) => { + return Err(QtBuildError::QMakeSetQtMissing { + qmake_env_var, + error: Box::new(e), + }) + } + } + } + + // Fedora 36 renames Qt5's qmake to qmake-qt5 + 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(), + version, + qt_modules, + }); + } + // If QT_VERSION_MAJOR is specified, it is expected that one of the versioned + // executable names will not match, so the unversioned `qmake` needs to be + // attempted last and QtVersionMajorDoesNotMatch should only be returned if + // none of the candidate executable names match. + 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) + } + + /// Get the output of running `qmake -query var_name` + 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() + } + + /// Tell Cargo to link each Qt module. + pub fn cargo_link_libraries(&self) { + lazy_static::lazy_static! { + static ref QMAKE_PRL_LIBS: regex::Regex = regex::RegexBuilder::new(r"^QMAKE_PRL_LIBS = (.*)$").multi_line(true).build().unwrap(); + } + let lib_path = self.qmake_query("QT_INSTALL_LIBS"); + println!("cargo:rustc-link-search={}", lib_path); + + // The needed information is in qmake's .prl files, so using pkgconfig is not necessary. + // There is no guarantee that pkgconfig is installed if Qt is installed, particularly on + // Windows. However, the pkg_config crate provides a useful function for parsing the + // information from the .prl files into linking instructions for Cargo. + let pkg_config = pkg_config::Config::new(); + + #[cfg(windows)] + let prefix = ""; + #[cfg(not(windows))] + let prefix = "lib"; + + for qt_module in &self.qt_modules { + let prl_path = format!( + "{}/{}Qt{}{}.prl", + lib_path, prefix, self.version.major, qt_module + ); + match std::fs::read_to_string(&prl_path) { + Ok(prl) => { + if let Some(captures) = QMAKE_PRL_LIBS.captures(&prl) { + let link_args = captures + .get(1) + .unwrap() + .as_str() + .replace(r"$$[QT_INSTALL_PREFIX]", &lib_path); + let mut lib = pkg_config::Library::new(); + lib.parse_libs_cflags( + &format!("Qt{}{}", self.version.major, qt_module), + link_args.as_bytes(), + &pkg_config, + ); + } else { + // When Qt is linked dynamically, the .prl files do not have a QT_PRL_LIBS line. + // This expected, not an error. + println!("cargo:rustc-link-lib=Qt{}{}", self.version.major, qt_module); + } + } + Err(e) => { + eprintln!( + "Could not open {} file to read libraries to link: {}", + &prl_path, e + ); + println!("cargo:rustc-link-lib=Qt{}{}", self.version.major, qt_module); + } + } + } + } + + /// Get 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. + pub fn include_paths(&self) -> Vec { + 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 + } +} From a45f3d754c2b556240bde119f25f8b256859179a Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Mon, 25 Jul 2022 02:24:11 -0500 Subject: [PATCH 2/5] fix UpdateRequestor member initialization order --- cxx-qt-lib/src/qt_types.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cxx-qt-lib/src/qt_types.cpp b/cxx-qt-lib/src/qt_types.cpp index b56732a1d..7f67bddef 100644 --- a/cxx-qt-lib/src/qt_types.cpp +++ b/cxx-qt-lib/src/qt_types.cpp @@ -12,8 +12,8 @@ namespace rust { namespace cxxqtlib1 { UpdateRequester::UpdateRequester(QPointer obj, const char* method) - : m_obj(obj) - , m_method(method) + : m_method(method) + , m_obj(obj) { } From c1891dada25a2c136fa5271b7a292899423e7a97 Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Tue, 26 Jul 2022 05:51:18 -0500 Subject: [PATCH 3/5] GH Actions: disable cxx_qt_lib_cargo_tests on Windows --- .github/workflows/github-cxx-qt-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-cxx-qt-tests.yml b/.github/workflows/github-cxx-qt-tests.yml index d4084606d..18870c31a 100644 --- a/.github/workflows/github-cxx-qt-tests.yml +++ b/.github/workflows/github-cxx-qt-tests.yml @@ -76,7 +76,8 @@ jobs: qt_version: 5 # FIXME: many tests fail to link # https://github.com/KDAB/cxx-qt/issues/111 - ctest_args: --exclude-regex '^(cxx_qt_gen_cargo_tests|demo_threading_cargo_tests|example_qml.*|qml.*tests|test.*|reuse_lint|cpp_clang_format|.*valgrind)$' + # cxx_qt_lib_cargo_tests fails to run with a missing DLL error. + ctest_args: --exclude-regex '^(cxx_qt_gen_cargo_tests|demo_threading_cargo_tests|example_qml.*|qml.*tests|test.*|reuse_lint|cpp_clang_format|.*valgrind|cxx_qt_lib_cargo_tests)$' exe_suffix: .exe qt_qpa_platform: windows compiler_cache_path: C:\Users\runneradmin\AppData\Local\Mozilla\sccache\cache @@ -90,7 +91,8 @@ jobs: qt_version: 6 # FIXME: many tests fail to link # https://github.com/KDAB/cxx-qt/issues/111 - ctest_args: --exclude-regex '^(cxx_qt_gen_cargo_tests|demo_threading_cargo_tests|example_qml.*|qml.*tests|test.*|reuse_lint|cpp_clang_format|.*valgrind)$' + # cxx_qt_lib_cargo_tests fails to run with a missing DLL error. + ctest_args: --exclude-regex '^(cxx_qt_gen_cargo_tests|demo_threading_cargo_tests|example_qml.*|qml.*tests|test.*|reuse_lint|cpp_clang_format|.*valgrind|cxx_qt_lib_cargo_tests)$' exe_suffix: .exe qt_qpa_platform: windows compiler_cache_path: C:\Users\runneradmin\AppData\Local\Mozilla\sccache\cache From 5b23fcf9958e97e296b565ce823808365869adeb Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Mon, 25 Jul 2022 03:02:12 -0500 Subject: [PATCH 4/5] cxx-qt-lib: use cxx_build instead of CMake for C++ This is a step towards obviating the need for CMake to build autogenerated code, which will simplify the build process by moving the invocation of Cargo to CMake build time rather than CMake configure time. --- CMakeLists.txt | 13 +++ cmake/CxxQt.cmake | 24 +++++- cxx-qt-build/src/lib.rs | 63 +------------- cxx-qt-lib/Cargo.toml | 7 +- cxx-qt-lib/build.rs | 181 +++++++++++++--------------------------- cxx-qt-lib/src/lib.rs | 32 ------- 6 files changed, 94 insertions(+), 226 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e6e21461..f4f5a4c7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,19 +149,32 @@ endfunction() add_subdirectory(book) add_subdirectory(examples) +# CARGO_TARGET_DIR is needed so the generated headers can be added to the +# include paths. +# QMAKE environment variable is needed by qt-build to ensure that Cargo +# uses the same installation of Qt as CMake does. +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui Test REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Test REQUIRED) +get_target_property(QMAKE Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION) +set(CARGO_ENV "QMAKE=${QMAKE};CARGO_TARGET_DIR=${CMAKE_CURRENT_SOURCE_DIR}/target") + # Create helper method which adds relevent cargo tests for a given manifest function(add_test_cargo TEST_NAME_PREFIX MANIFEST_PATH ADD_DOCTESTS) # Add cargo as a test add_test(NAME ${TEST_NAME_PREFIX}_cargo_tests COMMAND cargo test --all-targets --manifest-path ${MANIFEST_PATH}) + set_property(TEST ${TEST_NAME_PREFIX}_cargo_tests PROPERTY ENVIRONMENT ${CARGO_ENV}) # Check if we should enable doc tests if (${ADD_DOCTESTS} STREQUAL "DOCTESTS_ON") # Add cargo docs as a test add_test(NAME ${TEST_NAME_PREFIX}_cargo_doc_tests COMMAND cargo test --doc --manifest-path ${MANIFEST_PATH}) + set_property(TEST ${TEST_NAME_PREFIX}_cargo_doc_tests PROPERTY ENVIRONMENT ${CARGO_ENV}) endif() # Add clippy as a test add_test(NAME ${TEST_NAME_PREFIX}_cargo_clippy COMMAND cargo clippy --all-targets --manifest-path ${MANIFEST_PATH} -- -D warnings) + set_property(TEST ${TEST_NAME_PREFIX}_cargo_clippy PROPERTY ENVIRONMENT ${CARGO_ENV}) # Add rustfmt as a test add_test(NAME ${TEST_NAME_PREFIX}_cargo_fmt COMMAND cargo fmt --manifest-path ${MANIFEST_PATH} -- --check) + set_property(TEST ${TEST_NAME_PREFIX}_cargo_fmt PROPERTY ENVIRONMENT ${CARGO_ENV}) endfunction() # Add cargo tests for all our manifests diff --git a/cmake/CxxQt.cmake b/cmake/CxxQt.cmake index 292c74f61..833f95cc6 100644 --- a/cmake/CxxQt.cmake +++ b/cmake/CxxQt.cmake @@ -41,9 +41,18 @@ function(cxx_qt_generate_cpp GEN_SOURCES) file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/target/cxx-qt-gen") + get_target_property(QMAKE Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION) + # Run cargo during config to ensure the cpp source file list is created + # CARGO_TARGET_DIR is needed so the generated headers can be added to the + # include paths. + # QMAKE environment variable is needed by qt-build to ensure that Cargo + # uses the same installation of Qt as CMake does. execute_process( - COMMAND ${CARGO_CMD} + COMMAND cmake -E env + "CARGO_TARGET_DIR=${CMAKE_CURRENT_SOURCE_DIR}/target" + "QMAKE=${QMAKE}" + ${CARGO_CMD} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -62,6 +71,7 @@ function(cxx_qt_include APP_NAME) target_include_directories(${APP_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") # Include the target folder so that cxx-qt-gen and cxx-qt-lib can be included target_include_directories(${APP_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/target") + target_include_directories(${APP_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/target/cxxbridge") # Our cxx_qt and cxx headers are in this folder and need to be included target_include_directories(${APP_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/target/cxx-qt-gen/statics") endfunction() @@ -100,9 +110,19 @@ function(cxx_qt_link_rustlib APP_NAME) else() set(RUST_PART_LIB "${CMAKE_CURRENT_SOURCE_DIR}/target/${TARGET_DIR}/librust.a") endif() + + get_target_property(QMAKE Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION) + + # CARGO_TARGET_DIR is needed so the generated headers can be added to the + # include paths. + # QMAKE environment variable is needed by qt-build to ensure that Cargo + # uses the same installation of Qt as CMake does. add_custom_target( "${APP_NAME}_rustlib" - COMMAND ${CARGO_CMD} + COMMAND cmake -E env + "CARGO_TARGET_DIR=${CMAKE_CURRENT_SOURCE_DIR}/target" + "QMAKE=${QMAKE}" + ${CARGO_CMD} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_dependencies(${APP_NAME} "${APP_NAME}_rustlib") diff --git a/cxx-qt-build/src/lib.rs b/cxx-qt-build/src/lib.rs index 6cc7385a2..9ddbdaa01 100644 --- a/cxx-qt-build/src/lib.rs +++ b/cxx-qt-build/src/lib.rs @@ -233,61 +233,6 @@ fn write_cpp_sources_list(paths: &[PathBuf]) { } } -/// Write our a given cxx-qt-lib header and source set to the given folder -fn write_cxx_qt_lib_set( - file_name: &str, - target_dir: &str, - header: &str, - source: &str, -) -> Vec { - let mut paths = vec![]; - let path_h = PathBuf::from(format!("{}/include/{}.h", target_dir, file_name)); - let path_cpp = PathBuf::from(format!("{}/src/{}.cpp", target_dir, file_name)); - - let mut file = std::fs::File::create(&path_h).expect("Could not create header file"); - file.write_all(header.as_bytes()) - .expect("Could not write header file"); - paths.push(path_h); - - let mut file = std::fs::File::create(&path_cpp).expect("Could not create source file"); - file.write_all(source.as_bytes()) - .expect("Could not write source file"); - paths.push(path_cpp); - - paths -} - -/// Find all the cxx-qt-lib sources and write them to the target directory -fn write_cxx_qt_lib_sources() -> Vec { - let cxx_qt_lib_target_dir = format!("{}/target/cxx-qt-lib", manifest_dir()); - let cxx_qt_lib_include_dir = format!("{}/include", cxx_qt_lib_target_dir); - let cxx_qt_lib_src_dir = format!("{}/src", cxx_qt_lib_target_dir); - std::fs::create_dir_all(&cxx_qt_lib_include_dir).unwrap(); - std::fs::create_dir_all(&cxx_qt_lib_src_dir).unwrap(); - - let mut paths = vec![]; - // Add the hand written qt_types file - paths.append(&mut write_cxx_qt_lib_set( - "qt_types", - &cxx_qt_lib_target_dir, - cxx_qt_lib::QT_TYPES_HEADER, - cxx_qt_lib::QT_TYPES_SOURCE, - )); - // Add the generated CXX files - let generated: Vec = - serde_json::from_str(cxx_qt_lib::QT_TYPES_CXX_JSON).unwrap(); - for gen in generated { - paths.append(&mut write_cxx_qt_lib_set( - &gen.name, - &cxx_qt_lib_target_dir, - &gen.header, - &gen.source, - )); - } - - paths -} - /// Write out the static header file for both the cxx fn write_cxx_static_header() { let manifest_dir = manifest_dir(); @@ -353,19 +298,13 @@ impl CxxQtBuilder { // TODO: later use the module::object to turn into module/object.h // Generate files - let mut cpp_paths = write_cxx_generated_files_for_cargo(&self.rust_sources); + let cpp_paths = write_cxx_generated_files_for_cargo(&self.rust_sources); // TODO: in large projects where where CXX-Qt is used in multiple individual // components that end up being linked together, having these same static // files in each one could cause issues. write_cxx_static_header(); - // Check if we have Qt support enabled - if self.qt_enabled { - // Write the cxx-qt-lib sources into the folder - cpp_paths.append(&mut write_cxx_qt_lib_sources()); - } - // TODO: find a way to only do this when cargo is called during the config stage of CMake write_cpp_sources_list(&cpp_paths); } diff --git a/cxx-qt-lib/Cargo.toml b/cxx-qt-lib/Cargo.toml index 5004f7cc1..5b99abcad 100644 --- a/cxx-qt-lib/Cargo.toml +++ b/cxx-qt-lib/Cargo.toml @@ -19,8 +19,5 @@ links = "cxx-qt-lib" cxx = "1.0" [build-dependencies] -cxx-gen = "0.7" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -syn = { version = "1.0", features = ["printing"] } -quote = "1.0" +cxx-build = "1.0" +qt-build = { path = "../qt-build" } diff --git a/cxx-qt-lib/build.rs b/cxx-qt-lib/build.rs index e472dfc79..65c10ce26 100644 --- a/cxx-qt-lib/build.rs +++ b/cxx-qt-lib/build.rs @@ -3,138 +3,69 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 -use quote::ToTokens; -use serde::{Deserialize, Serialize}; -use std::{fs::File, io::Write, path::PathBuf}; - -/// Representation of a generated CXX header, source, and name -#[derive(Serialize, Deserialize)] -struct GeneratedType { - header: String, - name: String, - source: String, -} - -/// Generate a CXX header, source, name for a given Rust file -fn gen_cxx_sources(folder: &str, file_stem: &str) -> GeneratedType { - // Read the rust source files - let path = format!("{}/{}.rs", folder, file_stem); - println!("cargo:rerun-if-changed={}", path); - let content = std::fs::read_to_string(path).expect("Could not read Rust file"); - let file = syn::parse_file(&content).unwrap(); - - // Generate the CXX header and cc for the file - let opt = cxx_gen::Opt::default(); - let generated = cxx_gen::generate_header_and_cc(file.into_token_stream(), &opt) - .expect("Could not generate C++ from Rust file"); - - GeneratedType { - header: String::from_utf8(generated.header).unwrap(), - name: format!("{}_cxx", file_stem), - source: String::from_utf8(generated.implementation).unwrap(), - } -} - -/// Write generates types to a given file as JSON -fn write_cxx_sources(gen: &Vec, path: &str) { - let file = std::fs::File::create(path).expect("Could not create generated file"); - serde_json::to_writer(file, &gen).unwrap(); -} - -fn create_and_write_file(path: &impl AsRef, file_contents: &str) { - let path = path.as_ref(); - File::create(&path) - .unwrap_or_else(|_| panic!("Could not create file {}", path.display())) - .write_all(file_contents.as_bytes()) - .unwrap_or_else(|_| panic!("Could not write file {}", path.display())); -} +use std::env; fn main() { - // Read the cargo folder and out folder - let mut dir_manifest = std::env::var("CARGO_MANIFEST_DIR").expect("Could not get manifest dir"); - if cfg!(windows) { - dir_manifest = dir_manifest.replace('\\', "/"); + let qt_modules = vec!["Core", "Gui"] + .iter() + .map(|m| String::from(*m)) + .collect(); + let qtbuild = qt_build::QtBuild::new(qt_modules).expect("Could not find Qt installation"); + qtbuild.cargo_link_libraries(); + + // Copy qt_types.h so C++ build systems can #include it. + // By design, CARGO_TARGET_DIR is not set by cargo when running build scripts. + // Copying the header is only needed for making the header available to a C++ + // build system, in which case CARGO_TARGET_DIR will be set by + // the C++ build system. + println!("cargo:rerun-if-changed=include/qt_types.h"); + println!("cargo:rerun-if-env-changed=CARGO_TARGET_DIR"); + if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + std::fs::create_dir_all(&format!("{}/cxxbridge/cxx-qt-lib/include", target_dir)).unwrap(); + std::fs::copy( + &format!("{}/include/qt_types.h", manifest_dir), + &format!("{}/cxxbridge/cxx-qt-lib/include/qt_types.h", target_dir), + ) + .unwrap(); } - println!("cargo:rerun-if-env-changed=CARGO_MANIFEST_DIR"); - let mut dir_out = std::env::var("OUT_DIR").expect("Could not get out dir"); - if cfg!(windows) { - dir_out = dir_out.replace('\\', "/"); + let bridge_files = [ + "src/types/qcolor.rs", + "src/types/qdate.rs", + "src/types/qdatetime.rs", + "src/types/qpoint.rs", + "src/types/qpointf.rs", + "src/types/qrect.rs", + "src/types/qrectf.rs", + "src/types/qsize.rs", + "src/types/qsizef.rs", + "src/types/qstring.rs", + "src/types/qtime.rs", + "src/types/qurl.rs", + "src/types/qvariant.rs", + "src/types/update_requester.rs", + ]; + for bridge_file in bridge_files { + println!("cargo:rerun-if-changed={}", bridge_file); } - println!("cargo:rerun-if-env-changed=OUT_DIR"); - - // Prepare cxx-qt-lib dir we'll write to - let path = format!("{}/cxx-qt-lib", dir_out); - std::fs::create_dir_all(&path).expect("Could not create cxx-qt-lib dir"); - // Read the types directory for CXX objects - let types_dir = format!("{}/src/types/", dir_manifest); - // If any of the files in this directory change, then we need to re-run - println!("cargo:rerun-if-changed={}", types_dir); - - let mut generated = vec![]; - - for entry in std::fs::read_dir(&types_dir).expect("Could not open types folder") { - let path = entry.expect("Could not open file").path(); - let file_stem = path - .file_stem() - .expect("Could not find file name") - .to_str() - .expect("Could not convert to unicode"); - - // Check we are a file and not the mod.rs - if path.is_file() && file_stem != "mod" { - generated.push(gen_cxx_sources(&types_dir, file_stem)); - } + for include_path in qtbuild.include_paths() { + cxx_build::CFG + .exported_header_dirs + .push(include_path.as_path()); } - // Write the generated sources to a qt_types_cxx.json file - write_cxx_sources(&generated, &format!("{}/qt_types_cxx.json", path)); - - // Write the generated sources to CXX_QT_LIB_OUT_DIR if set - println!("cargo:rerun-if-env-changed=CXX_QT_LIB_OUT_DIR"); - if let Ok(env_var) = std::env::var("CXX_QT_LIB_OUT_DIR") { - let directory = PathBuf::from(env_var); - if !directory.is_dir() { - panic!( - "CXX_QT_LIB_OUT_DIR {} is not a directory", - directory.display() - ); - } - - let include_directory_path = PathBuf::from(format!("{}/include", &directory.display())); - std::fs::create_dir_all(&include_directory_path) - .expect("Could not create CXX_QT_LIB_OUT_DIR include dir"); - let source_directory_path = PathBuf::from(format!("{}/src", &directory.display())); - std::fs::create_dir_all(&source_directory_path) - .expect("Could not create CXX_QT_LIB_OUT_DIR source dir"); - - std::fs::copy( - format!("{}/include/qt_types.h", dir_manifest), - format!("{}/qt_types.h", include_directory_path.display()), - ) - .expect("Could not copy qt_types.h to CXX_QT_LIB_OUT_DIR"); - std::fs::copy( - format!("{}/src/qt_types.cpp", dir_manifest), - format!("{}/qt_types.cpp", source_directory_path.display()), - ) - .expect("Could not copy qt_types.cpp to CXX_QT_LIB_OUT_DIR"); - - create_and_write_file( - &format!("{}/cxx.h", include_directory_path.display()), - cxx_gen::HEADER, - ); - - for class in generated { - create_and_write_file( - &format!("{}/{}.h", include_directory_path.display(), class.name), - &class.header, - ); - - create_and_write_file( - &format!("{}/{}.cpp", source_directory_path.display(), class.name), - &class.source, - ); - } + let mut builder = cxx_build::bridges(&bridge_files); + for cpp_file in ["src/qt_types.cpp"] { + builder.file(cpp_file); + println!("cargo:rerun-if-changed={}", cpp_file); } + // MSVC + builder.flag_if_supported("/std:c++17"); + builder.flag_if_supported("/Zc:__cplusplus"); + builder.flag_if_supported("/permissive-"); + // GCC + Clang + builder.flag_if_supported("-std=c++17"); + builder.compile("cxx-qt-lib"); } diff --git a/cxx-qt-lib/src/lib.rs b/cxx-qt-lib/src/lib.rs index eb78c9f90..d8f26a7a6 100644 --- a/cxx-qt-lib/src/lib.rs +++ b/cxx-qt-lib/src/lib.rs @@ -7,38 +7,6 @@ mod types; pub use types::*; -// Provide a separate depending on the platform -// this is because include_str requires th correct and non-mixed path separators -// -// https://github.com/rust-lang/rust/issues/75075 -#[cfg(not(windows))] -macro_rules! sep { - () => { - "/" - }; -} - -#[cfg(windows)] -macro_rules! sep { - () => { - "\\" - }; -} - -/// JSON representation of the generated CXX sources for qt_types -pub const QT_TYPES_CXX_JSON: &str = include_str!(concat!( - env!("OUT_DIR"), - sep!(), - "cxx-qt-lib", - sep!(), - "qt_types_cxx.json" -)); -/// The header for qt_types -pub const QT_TYPES_HEADER: &str = - include_str!(concat!("..", sep!(), "include", sep!(), "qt_types.h")); -/// The source for qt_types -pub const QT_TYPES_SOURCE: &str = include_str!("qt_types.cpp"); - pub trait UpdateRequestHandler { fn handle_update_request(&mut self, cpp: &mut C); } From 39823a3c0f1f5fa90124a3451048fc5cd318a965 Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Tue, 2 Aug 2022 07:02:13 -0500 Subject: [PATCH 5/5] cxx-qt-lib: hack around Windows build failure --- cxx-qt-lib/build.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/cxx-qt-lib/build.rs b/cxx-qt-lib/build.rs index 65c10ce26..9f23df6c3 100644 --- a/cxx-qt-lib/build.rs +++ b/cxx-qt-lib/build.rs @@ -23,11 +23,22 @@ fn main() { if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); std::fs::create_dir_all(&format!("{}/cxxbridge/cxx-qt-lib/include", target_dir)).unwrap(); - std::fs::copy( - &format!("{}/include/qt_types.h", manifest_dir), - &format!("{}/cxxbridge/cxx-qt-lib/include/qt_types.h", target_dir), - ) - .unwrap(); + // FIXME: Horrible hack around this sometimes failing because + // Windows doesn't allow multiple processes to access a file by default. + loop { + match std::fs::copy( + &format!("{}/include/qt_types.h", manifest_dir), + &format!("{}/cxxbridge/cxx-qt-lib/include/qt_types.h", target_dir), + ) { + Ok(_) => break, + #[cfg(windows)] + Err(e) if e.raw_os_error() == Some(32) => { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + Err(e) => panic!("Error copying qt_types.h: {:?}", e), + } + } } let bridge_files = [