Skip to content

Commit

Permalink
Allow cxx-qt-build to deal with build scripts running in parallel (#999)
Browse files Browse the repository at this point in the history
* refactor: Rename CXXQT_* variables to CXX_QT_*

This is more in line with our crate naming (cxx-qt, not cxxqt) and
is more consistent, as before we were using both.

Fix #996

* refactor: Rename cxxqt_ CMake functions to cxx_qt_

This is more consistent with all our other naming.

* fix: build: Use export_dir on final crate only

Cargo can sometimes build different configurations of the same crate in
parallel.
This caused some failures with our new build system.
However, we can solve this by reverting to the OUT_DIR again.
Using the OUT_DIR is the only supported way to write artifacts anyway.

However, we still need to export data for CMake.
Therefore, we define an additional flag for each exact crate that CMake
is trying to import.
Only this exact crate will then export its data.
As CMake should only build a single configuration at a time, this should
not conflict.

* book: Document user-facing changes to CMake build

* cmake: Resolve ${CRATE} and ${APP_NAME} variables

At least do so in `qml_minimal`. I got the feedback from Milian that
this is rather hard to read otherwise, which I agree with.
Especially as that's the code we include in our getting-started guide it
should be as simple as possible.

* book: Update internals documentation

This is now slightly different, as we're no longer using a shared export
directory
  • Loading branch information
LeonMatthesKDAB authored Jul 16, 2024
1 parent c7922e4 commit 02e5be6
Show file tree
Hide file tree
Showing 19 changed files with 115 additions and 104 deletions.
5 changes: 3 additions & 2 deletions book/src/concepts/build_systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ CXX-Qt can be integrated into existing CMake projects or built with only cargo.
- [Cargo Integration](../getting-started/4-cargo-executable.md)
- [CMake Integration](../getting-started/5-cmake-integration.md)

CXX-Qt could work with any C++ build system so long as the `QMAKE` and `CXXQT_EXPORT_DIR` environment variables are set before calling Cargo,
as documented in [CMake integration](../getting-started/5-cmake-integration.md). However, using C++ build systems besides CMake with CXX-Qt is untested.
CXX-Qt could work with any C++ build system so long as the `QMAKE`, `CXX_QT_EXPORT_DIR` and `CXX_QT_EXPORT_CRATE_<CRATE-NAME>` environment variables are set before calling Cargo.
Take a look at our CMake code for how this can be used.
However, using C++ build systems besides Cargo or CMake with CXX-Qt is untested and the use of these environment variables is SemVer-exempt!

## `CxxQtBuilder`

Expand Down
4 changes: 2 additions & 2 deletions book/src/getting-started/2-our-first-cxx-qt-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT OR Apache-2.0
As with all things Rust, we'll want to create a cargo project, run the following command inside the `tutorial` folder to initialize the Rust part of the project.

```shell
$ cargo new cxx-qt-tutorial
$ cd cxx-qt-tutorial
$ cargo new qml_minimal
$ cd qml_minimal
```

> If you want to skip building with Cargo and try building with CMake directly
Expand Down
1 change: 1 addition & 0 deletions book/src/getting-started/4-cargo-executable.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Add the dependencies to the `Cargo.toml` file.
We'll need `cxx`, `cxx-qt`, `cxx-qt-lib` and `cxx-qt-build`:

```toml,ignore
{{#include ../../../examples/qml_minimal/rust/Cargo.toml:book_package_name}}
{{#include ../../../examples/cargo_without_cmake/Cargo.toml:book_cargo_toml_no_cmake}}
cxx = "1.0.95"
cxx-qt = "0.6"
Expand Down
43 changes: 26 additions & 17 deletions book/src/getting-started/5-cmake-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,20 @@ You can add as much C++ code as you want in addition to this.
## Using Rust QObjects in C++

For every `#[cxx_qt::bridge]` that we define in Rust, CXX-Qt will generate a corresponding C++ header file.
They will always be in the `cxx-qt-gen/` include path and use the snake_case naming convention.
To include any of the generated files, use the crates name as the include directory.
The name of the header file will be the name of the Rust module of your `#[cxx_qt::bridge]`, followed by `.cxxqt.h`.
So in our case: `#include cxx-qt-gen/qobject.cxxqt.h`
So in our case: `#include <qml_minimal/qobject.cxxqt.h>`

> Note that the [`cxx_file_stem`](../bridge/index.md#cxx_file_stem) option can be specified in the bridge macro to choose the file name.
Including the generated header allows accessing the `MyObject` C++ class, just like any other C++ class.
Including the generated header allows us to access the `MyObject` C++ class, just like any other C++ class.
Inherit from it, connect signals and slots to it, put it in a QVector, do whatever you want with it.
That's the power of CXX-Qt.

## Cargo setup

Before we can get started on building Qt with CMake, we first need to make our Cargo build ready for it.
If you've generated your project with the `cargo new --lib` or `cargo init --lib [folder]` command, your `Cargo.toml` should look something like this:
If you've generated your project with e.g. `cargo new --lib qml_minimal` or `cargo init --lib [folder]` command, your `Cargo.toml` should look something like this:

```toml,ignore
[package]
Expand Down Expand Up @@ -107,7 +107,9 @@ The files and resources in the module are then exposed in the same way as the [q

> Note that in order for CXX-Qt to work, the `qmake` executable must be located. This is because CXX-Qt relies on `qmake` to locate the necessary Qt libraries and header files on your system.
>
> This will be done in the `CMakeLists.txt` file by setting the `QMAKE` environment variable from CMake, ensuring that CMake and Cargo use the same Qt binaries.
> Usually, the CMake code that CXX-Qt provides you to import a crate should already take care of this.
>
> To overwrite the path to qmake, you may pass the `QMAKE` option to cxx_qt_import_crate, ensuring that CMake and Cargo use the same Qt binaries.
We'll also need to remove the `src/main.rs` and replace it with a `src/lib.rs` file.
This file only needs to include a single line:
Expand All @@ -130,27 +132,34 @@ For this example, we are [supporting both Qt5 and Qt6 with CMake](https://doc.qt
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_setup}}
```

Locate [Corrosion](https://github.com/corrosion-rs/corrosion), a tool for integrating Rust libraries into CMake.
If Corrosion is not installed, automatically download it:
Download CXX-Qts CMake code with FetchContent:

```cmake,ignore
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_find_corrosion}}
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_find_cxx_qt}}
```

To ensure that cxx-qt-build uses the same version of Qt as your CMake targets, use the `Qt` CMake target to locate the qmake executable. Then, pass `qmake` executable path to `build.rs` with the environment variable `QMAKE` using `corrosion_set_env_vars`.
This provides you with a few wrappers around [Corrosion](https://github.com/corrosion-rs/corrosion), a tool for integrating Rust libraries into CMake:

1. `cxx_qt_import_crate` - A wrapper around [corrosion_import_crate](https://corrosion-rs.github.io/corrosion/usage.html). It supports the same arguments as corrosion_import_crate, with two new optional arguments:
- `CXX_QT_EXPORT_DIR` - Manually specify the path where CXX-Qt artifacts will be exported to.
- This is usually not necessary. However, if you're importing the same crate with different feature sets in the same CMake build configuration, you will need to specify seperate `CXX_QT_EXPORT_DIR`s to avoid multiple versions of the crate exporting to the same directory.
- `QMAKE` - Override the path to the QMAKE executable
2. `cxx_qt_import_qml_module` - This function imports a QML modules as a new target. It requires the following arguments:
- TARGET_NAME - Specify the name of the CMake target that this function will create
- `URI` - The URI of the qml module to import - this needs to exactly match the URI in the `CxxQtBuilder::qml_module` call in your build script.
- `SOURCE_CRATE` The crate that exports the QML module (this crate must have been imported with `cxx_qt_import_crate`).

```cmake,ignore
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_find_qmake}}
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_use_cxx_qt}}
```

Use Corrosion to create a CMake library target for the Rust library. CXX-Qt requires a few more steps beyond using
a typical Rust library with Corrosion:
This will create two new CMake targets:

```cmake,ignore
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_use_corrosion}}
```
1. `qml_minimal` - The static library exported by our crate
2. `qml_minimal_qml_module` - The QML Module exported by our crate
- The `_qml_module` target will automatically link to the `qml_minimal` target, so linking to the `_qml_module` is sufficient for our executable target

Finally, create the CMake executable target and link it to the Rust library:
Finally, we can create the CMake executable target and link it to our crate:

```cmake,ignore
{{#include ../../../examples/qml_minimal/CMakeLists.txt:book_cmake_executable}}
Expand Down Expand Up @@ -189,7 +198,7 @@ This should now configure and compile our project.
If this was successful, you can now run our little project.

```shell
$ build/examples/qml_minimal/example_qml_minimal
$ ./build/examples/qml_minimal/example_qml_minimal
```

You should now see the two Labels that display the state of our `MyObject`, as well as the two buttons to call our two Rust functions.
Expand Down
33 changes: 14 additions & 19 deletions book/src/internals/build-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,38 +39,33 @@ Somehow, all of this should be compatible with both CMake, and Cargo-only builds

## The plan (for now)

After many rounds of refactoring this, we believe the best way to go is to not rely on cargos OUT_DIR exclusivly.
Not being able to share artifacts between crates is very limiting.
After many rounds of refactoring this, we believe that we need to be able to share data between build scripts for this to work halfway ergonomically.

We want to use a similar approach to CXX, which stores its data within either:
We want to use a similar approach to CXX, which uses Cargos `links` key to ensure a correct build order (see the documentation [here](https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key)).
When building with cxx-qt-build, you may simply specify that your code depends on another crate.
Cargo will then make sure that the build scripts of the dependencies have run **before** the build script of this crate.

- Cargos `target/` directory under the `cxxbridge` subfolder.
- A custom `scratch` directory
We can additionally pass metadata between build scripts, which we use to find the `manifest.json` of each crate and the path to their "target" directory.

See: [https://github.com/dtolnay/cxx/blob/afd4aa3f3d4e5d5e9a3a41d09df3408f5f86a469/gen/build/src/target.rs#L10](https://github.com/dtolnay/cxx/blob/afd4aa3f3d4e5d5e9a3a41d09df3408f5f86a469/gen/build/src/target.rs#L10) \
and: [https://github.com/dtolnay/cxx/blob/afd4aa3f3d4e5d5e9a3a41d09df3408f5f86a469/gen/build/src/lib.rs#L178](https://github.com/dtolnay/cxx/blob/afd4aa3f3d4e5d5e9a3a41d09df3408f5f86a469/gen/build/src/lib.rs#L178).
## The "target" directory

We will likely mirror this and use the `cxxqtbridge` naming.
Each build script can export artifacts into a folder with a well-known layout.
It is also required to export a `manifest.json` file that tells downstream dependencies which of these artifacts to include and how to configure their own build.

## Contents of `cxxqtbridge`
This "target" directory is usually in the OUT_DIR, but can be exported using `CXX_QT_EXPORT_DIR` and `CXX_QT_EXPORT_CRATE_[crate-name]` environment variables.
Which is used by CMake to import the artifacts. (See: [Integration with CMake](#integration-with-cmake))

### `crates` directory

Inside cxxqtbridge, there should be a `crates` folder with one subfolder per crate.
Inside the target directory, there should be a `crates` folder with one subfolder per crate.
Each crates subfolder should contain the following:

- `include/`
- `crate-name` - A folder for all headers that are exported by this crate
- `cxx-qt-lib -> ../../cxx-qt-lib/include` - Symbolic links for every dependency
- `cxx-qt-lib -> <path-to-dependency>/include/cxx-qt-lib` - Symbolic links for every dependency
- `manifest.json` - This file describes which headers this library makes available, if it needs any Qt modules, etc.
- `initializers.o` - The initializers of this crate + all it's dependencies to be linked in by CMake

When building with cxx-qt-build, you may simply specify that your code depends on another crate.
However, we also need to make sure that the order in which the build scripts are run works out, as e.g. the build script of cxx-qt-lib needs to run **before** any dependents build scripts run.

For this use-case, Cargo has the `links` key (see the documentation [here](https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key)).
It allows us to ensure the correct order, and also to pass metadata between build scripts.
We specifically use this to let downstream dependents know where to find our manifest.json.
Via the `manifest.json`, we are then able to figure out which header paths of this dependency to include, which Qt modules to link, etc.

To make sure the correct data ends up in the manifest.json, we provide the `cxx_qt_build::Interface` struct which uses the builder pattern to specify all the necessary data.
Expand All @@ -83,14 +78,14 @@ Each module should include a `plugin_init.o`, `.qmltypes`, `qmldir`, and any oth

## Integration with CMake

Via the `CXXQT_EXPORT_DIR` environment variable CMake should be able to change the location of the `cxxqtbridge` directory.
Via the `CXXQT_EXPORT_DIR` environment variable CMake should be able to change the location of the "target" directory.
CMake can then expect required artifacts to exist at pre-defined locations, which can be added as dependency, include directories, objects, etc. to the Crate target.

We will rely on Corrosion to import the crate and provide targets for it.

However, we also want to provide some custom functions that wrap corrosion and set up the import of our own artifacts.

Currently we plan to provide two functions:
Currently we provide two functions:

- cxxqt_import_crate
- A wrapper over corrosion_import_crate that defines the `CXXQT_EXPORT_DIR`, imports the initializers object files, etc.
Expand Down
45 changes: 24 additions & 21 deletions cmake/CxxQt.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,60 @@ if(NOT Corrosion_FOUND)
FetchContent_MakeAvailable(Corrosion)
endif()

function(cxxqt_import_crate)
cmake_parse_arguments(IMPORT_CRATE "" "CXXQT_EXPORT_DIR;QMAKE" "" ${ARGN})
function(cxx_qt_import_crate)
cmake_parse_arguments(IMPORT_CRATE "" "CXX_QT_EXPORT_DIR;QMAKE" "" ${ARGN})

corrosion_import_crate(IMPORTED_CRATES __cxxqt_imported_crates ${IMPORT_CRATE_UNPARSED_ARGUMENTS})
corrosion_import_crate(IMPORTED_CRATES __cxx_qt_imported_crates ${IMPORT_CRATE_UNPARSED_ARGUMENTS})

message(STATUS "CXX-Qt Found crate(s): ${__cxxqt_imported_crates}")
message(STATUS "CXX-Qt Found crate(s): ${__cxx_qt_imported_crates}")

if (NOT DEFINED IMPORT_CRATE_CXXQT_EXPORT_DIR)
set(IMPORT_CRATE_CXXQT_EXPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/cxxqt/")
if (NOT DEFINED IMPORT_CRATE_CXX_QT_EXPORT_DIR)
set(IMPORT_CRATE_CXX_QT_EXPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/cxxqt/")
endif()
message(VERBOSE "CXX-Qt EXPORT_DIR: ${IMPORT_CRATE_CXXQT_EXPORT_DIR}")
message(VERBOSE "CXX-Qt EXPORT_DIR: ${IMPORT_CRATE_CXX_QT_EXPORT_DIR}")

if (NOT DEFINED IMPORT_CRATE_QMAKE)
get_target_property(QMAKE Qt::qmake IMPORTED_LOCATION)
if (NOT QMAKE STREQUAL "QMAKE-NOTFOUND")
set(IMPORT_CRATE_QMAKE "${QMAKE}")
else()
message(FATAL_ERROR "cxxqt_import_crate: QMAKE is not defined and could not be queried from the Qt::qmake target!\nPlease use the QMAKE argument to specify the path to the qmake executable or use find_package(Qt) before calling this function.")
message(FATAL_ERROR "cxx_qt_import_crate: QMAKE is not defined and could not be queried from the Qt::qmake target!\nPlease use the QMAKE argument to specify the path to the qmake executable or use find_package(Qt) before calling cxx_qt_import_crate.")
endif()
endif()

foreach(CRATE ${__cxxqt_imported_crates})
foreach(CRATE ${__cxx_qt_imported_crates})
corrosion_set_env_vars(${CRATE}
"CXXQT_EXPORT_DIR=${IMPORT_CRATE_CXXQT_EXPORT_DIR}"
# Tell cxx-qt-build where to export the data
"CXX_QT_EXPORT_DIR=${IMPORT_CRATE_CXX_QT_EXPORT_DIR}"
# Tell cxx-qt-build which crate to export
"CXX_QT_EXPORT_CRATE_${CRATE}=1"
"QMAKE=${IMPORT_CRATE_QMAKE}"
$<$<BOOL:${CMAKE_RUSTC_WRAPPER}>:RUSTC_WRAPPER=${CMAKE_RUSTC_WRAPPER}>)

file(MAKE_DIRECTORY "${IMPORT_CRATE_CXXQT_EXPORT_DIR}/crates/${CRATE}/include/")
target_include_directories(${CRATE} INTERFACE "${IMPORT_CRATE_CXXQT_EXPORT_DIR}/crates/${CRATE}/include/")
file(MAKE_DIRECTORY "${IMPORT_CRATE_CXX_QT_EXPORT_DIR}/crates/${CRATE}/include/")
target_include_directories(${CRATE} INTERFACE "${IMPORT_CRATE_CXX_QT_EXPORT_DIR}/crates/${CRATE}/include/")

set_target_properties(${CRATE}
PROPERTIES
CXXQT_EXPORT_DIR "${IMPORT_CRATE_CXXQT_EXPORT_DIR}")
CXX_QT_EXPORT_DIR "${IMPORT_CRATE_CXX_QT_EXPORT_DIR}")

# cxx-qt-build generates object files that need to be linked to the final target.
# These are the static initializers that would be removed as an optimization if they're not referenced.
# So add them to an object library instead.
file(MAKE_DIRECTORY "${IMPORT_CRATE_CXXQT_EXPORT_DIR}/crates/${CRATE}/")
file(MAKE_DIRECTORY "${IMPORT_CRATE_CXX_QT_EXPORT_DIR}/crates/${CRATE}/")
# When using the Ninja generator, we need to provide **some** way to generate the object file
# Unfortunately I'm not able to tell corrosion that this obj file is indeed a byproduct, so
# create a fake target for it.
# This target doesn't need to do anything, because the file should already exist after building the crate.
add_custom_target(${CRATE}_mock_initializers
COMMAND ${CMAKE_COMMAND} -E true
DEPENDS ${CRATE}
BYPRODUCTS "${IMPORT_CRATE_CXXQT_EXPORT_DIR}/crates/${CRATE}/initializers.o")
BYPRODUCTS "${IMPORT_CRATE_CXX_QT_EXPORT_DIR}/crates/${CRATE}/initializers.o")

add_library(${CRATE}_initializers OBJECT IMPORTED)
set_target_properties(${CRATE}_initializers
PROPERTIES
IMPORTED_OBJECTS "${IMPORT_CRATE_CXXQT_EXPORT_DIR}/crates/${CRATE}/initializers.o")
IMPORTED_OBJECTS "${IMPORT_CRATE_CXX_QT_EXPORT_DIR}/crates/${CRATE}/initializers.o")
# Note that we need to link using TARGET_OBJECTS, so that the object files are included **transitively**, otherwise
# Only the linker flags from the object library would be included, but not the actual object files.
# See also the "Linking Object Libraries" and "Linking Object Libraries via $<TARGET_OBJECTS>" sections:
Expand All @@ -77,22 +80,22 @@ function(cxxqt_import_crate)
endfunction()


function(cxxqt_import_qml_module target)
function(cxx_qt_import_qml_module target)
cmake_parse_arguments(QML_MODULE "" "URI;SOURCE_CRATE" "" ${ARGN})

if (NOT DEFINED QML_MODULE_URI)
message(FATAL_ERROR "cxxqt_import_qml_module: URI must be specified!")
message(FATAL_ERROR "cxx_qt_import_qml_module: URI must be specified!")
endif()

if (NOT DEFINED QML_MODULE_SOURCE_CRATE)
message(FATAL_ERROR "cxxqt_import_qml_module: SOURCE_CRATE must be specified!")
message(FATAL_ERROR "cxx_qt_import_qml_module: SOURCE_CRATE must be specified!")
endif()

get_target_property(QML_MODULE_EXPORT_DIR ${QML_MODULE_SOURCE_CRATE} CXXQT_EXPORT_DIR)
get_target_property(QML_MODULE_EXPORT_DIR ${QML_MODULE_SOURCE_CRATE} CXX_QT_EXPORT_DIR)
get_target_property(QML_MODULE_CRATE_TYPE ${QML_MODULE_SOURCE_CRATE} TYPE)

if (${QML_MODULE_EXPORT_DIR} STREQUAL "QML_MODULE_EXPORT_DIR-NOTFOUND")
message(FATAL_ERROR "cxxqt_import_qml_module: SOURCE_CRATE must be a valid target that has been imported with cxxqt_import_crate!")
message(FATAL_ERROR "cxx_qt_import_qml_module: SOURCE_CRATE must be a valid target that has been imported with cxx_qt_import_crate!")
endif()

# Note: This needs to match the URI conversion in cxx-qt-build
Expand Down
1 change: 0 additions & 1 deletion crates/cxx-qt-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ quote.workspace = true
qt-build-utils.workspace = true
codespan-reporting = "0.11"
version_check = "0.9"
scratch = "1.0"
serde = { version = "1.0", features = ["default", "derive"] }
serde_json = "1.0"

Expand Down
Loading

0 comments on commit 02e5be6

Please sign in to comment.