From 8962fad37aed2d173cf32ffdaffec177da00bd35 Mon Sep 17 00:00:00 2001 From: yaraslau Date: Thu, 24 Sep 2026 22:44:01 +0200 Subject: [PATCH] cmake+forms: install the MorphForms QML module as the forms_qml component, and ship the header qt_forms includes `cmake --install` of a MORPH_BUILD_FORMS_QML=ON build installed the controller-core header and nothing of the QML module itself, so a packaged morph -- a vcpkg port, say -- could not be used to render a form: there was no target to link and no module to import. And the one piece that was installed could not be compiled from the install: measured against a Windows install built with MORPH_BUILD_FORMS_QML alone, forms_controller_core.hpp(32): fatal error C1083: Cannot open include file: 'morph/qt/qt_executor.hpp' because qt_executor.hpp belongs to the `qt` component (MORPH_BUILD_QT, which needs Qt WebSockets). The in-tree build and the header-set verification both read the source tree, so neither could see it. - `qt_forms` now lists qt_executor.hpp in its own header set (it needs only QtCore), so the component installs what it includes. - The QML module installs and exports as `forms_qml`: `morph::forms_qml` (backing library) and `morph::forms_qmlplugin`, plus the object libraries `qt_add_qml_module(... OUTPUT_TARGETS)` reports for a static module -- its compiled resources and the plugin's static initialiser. Without those an application links and then finds no MorphForms at run time. Each gets an EXPORT_NAME so nothing arrives as morph::morph_*. - qmldir, the .qmltypes and the sources go to MORPH_INSTALL_QMLDIR (default /qml) for tooling, exported to consumers as `morph_QML_IMPORT_PATH`. The installed qmldir's `linktarget` is rewritten to `morph::forms_qmlplugin`, the name a static-Qt build's QML plugin import looks the plugin up by. - morphConfig.cmake finds Qt6 Core/Gui/Qml/Quick for the component. scripts/check_forms_qml_install.sh measures it the way check_install_export.sh measures the C++ side: install to a scratch prefix, then build and *run* an application that instantiates DynamicForm from `import MorphForms` via find_package(morph COMPONENTS forms_qml qt_forms). Its vacuity guard runs the same application linked against morph::forms_qml without the plugin, which must fail (`module "MorphForms" plugin "morph_forms_moduleplugin" not found`), so the check is shown to measure the plugin. Run locally on Windows (MSVC 14.51, Qt 6.11.1): all four steps ok. New CI job install-export-forms-qml runs it on ubuntu-24.04 with Qt 6.8.1. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 35 ++++++ CHANGELOG.md | 20 +++ CMakeLists.txt | 55 +++++++++ README.md | 24 +++- cmake/morphConfig.cmake.in | 8 ++ docs/spec/forms/forms.md | 18 ++- scripts/check_forms_qml_install.sh | 188 +++++++++++++++++++++++++++++ src/qt/forms/CMakeLists.txt | 5 + 8 files changed, 348 insertions(+), 5 deletions(-) create mode 100755 scripts/check_forms_qml_install.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c7e6879e..44b8f0836 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3462,3 +3462,38 @@ jobs: CC: gcc-15 CXX: g++-15 run: bash scripts/check_install_export.sh + + install-export-forms-qml: + name: MorphForms QML module consumability + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + # GCC 15 for the same reason install-export uses it (); the GL and + # xcb libraries are what the offscreen QPA plugin still links against. + - name: Install GCC 15, ninja and the Qt runtime libraries + run: | + sudo apt-get update -q + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update -q + sudo apt-get install -y gcc-15 g++-15 ninja-build \ + libgl1-mesa-dev libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 \ + libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 + + # Not the distro's Qt: MORPH_BUILD_FORMS_QML needs 6.5+ and Ubuntu 24.04 + # ships 6.4.2 (see linux-all-features' identical step). + - name: Install Qt ${{ env.QT_VERSION }} + uses: jurplel/install-qt-action@v4.3.1 + with: + version: ${{ env.QT_VERSION }} + cache: true + + # Install MorphForms to a scratch prefix, then build and *run* an + # application that renders a DynamicForm from it -- plus the vacuity + # guard that the same application without the plugin cannot. + - name: Install MorphForms and render a form from the prefix + env: + CC: gcc-15 + CXX: g++-15 + run: bash scripts/check_forms_qml_install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 711190e4f..c93098182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,17 @@ API surface). `resolve()` takes it as an optional sixth argument, consulted after unit and before type. See `docs/spec/forms/forms.md`, "Theming / component-override registry" (fixes #812). +- **The MorphForms QML module installs, as the `forms_qml` component.** + `cmake --install` of a `MORPH_BUILD_FORMS_QML=ON` build installed the + controller-core header and nothing of the QML module, so a packaged morph + (e.g. a vcpkg port) could not be used to render a form. It now installs and + exports `morph::forms_qml` / `morph::forms_qmlplugin` together with the + object libraries a static QML module needs (compiled resources, the plugin's + static initialiser), and `qmldir`/`.qmltypes`/sources under + `MORPH_INSTALL_QMLDIR` (exported as `morph_QML_IMPORT_PATH`). A consumer + links `morph::forms_qmlplugin` and imports `MorphForms`; + `scripts/check_forms_qml_install.sh` (CI `install-export-forms-qml`) builds + and runs one against an install. - **A locale numeric entry accepts an explicit `+`.** `morph::render::normalizeLocaleNumber` had no notion of a positive sign: a @@ -272,6 +283,15 @@ API surface). ### Fixed +- **An installed `qt_forms` component compiles.** `forms_controller_core.hpp` + includes `morph/qt/qt_executor.hpp`, which was installed only by the `qt` + component (`MORPH_BUILD_QT`, which needs Qt WebSockets), so an install built + with `MORPH_BUILD_FORMS_QML` alone shipped a header that could not be + included: `fatal error C1083: Cannot open include file: + 'morph/qt/qt_executor.hpp'`. In-tree builds and the header-set verification + both read the source tree and could not see it. `qt_forms` now ships the + header too; it needs only QtCore. + - **The QML renderer's numeric-entry mirror matched the decimal and group separators as one UTF-16 code unit while the C++ edge matched whole strings.** `src/qt/forms/qml/DynamicForm.qml`'s `normalizeLocaleNumber` tested diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f1be1ed0..0b937c84e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -514,12 +514,17 @@ if(MORPH_BUILD_FORMS_QML) # stops being self-contained must fail here, not silently pass (morph#230). set_target_properties(morph_qt_forms PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON) target_link_libraries(morph_qt_forms INTERFACE morph Qt6::Core) + # qt_executor.hpp is listed here too, not only under morph::qt: the + # controller core includes it, and morph::qt (MORPH_BUILD_QT, which needs + # Qt WebSockets) is not part of every install that ships this header. It + # needs nothing beyond QtCore. target_sources(morph_qt_forms INTERFACE FILE_SET HEADERS BASE_DIRS include FILES include/morph/qt/forms/forms_controller_core.hpp + include/morph/qt/qt_executor.hpp ) endif() @@ -834,6 +839,55 @@ if(MORPH_INSTALL) ) endif() + # The MorphForms QML module (MORPH_BUILD_FORMS_QML): its backing library, + # its plugin, and the object libraries a static QML module carries -- + # without the latter an installed module links but registers no QML types + # and embeds no .qml files. The qmldir/qmltypes/sources go to a QML import + # directory for tooling (qmllint, qmlls, a consumer's qmlcachegen); at run + # time the module resolves from its embedded resources. + set(MORPH_INSTALL_QMLDIR "${CMAKE_INSTALL_LIBDIR}/qml" CACHE STRING + "Import directory (relative to the prefix) the MorphForms QML module is installed under") + if(TARGET morph_forms_module) + set_target_properties(morph_forms_module PROPERTIES EXPORT_NAME forms_qml) + set_target_properties(morph_forms_moduleplugin PROPERTIES EXPORT_NAME forms_qmlplugin) + # A generated target's name starts with morph_forms_module(plugin)_; + # exporting it under that name would read morph::morph_... downstream. + foreach(_morph_forms_target IN LISTS MORPH_FORMS_QML_OUTPUT_TARGETS) + string(REGEX REPLACE "^morph_" "" _morph_forms_export_name "${_morph_forms_target}") + set_target_properties(${_morph_forms_target} PROPERTIES EXPORT_NAME ${_morph_forms_export_name}) + endforeach() + install(TARGETS morph_forms_module morph_forms_moduleplugin ${MORPH_FORMS_QML_OUTPUT_TARGETS} + EXPORT morphTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + OBJECTS DESTINATION "${CMAKE_INSTALL_LIBDIR}" + ) + list(APPEND MORPH_INSTALLED_COMPONENTS forms_qml) + + get_target_property(_morph_forms_dir morph_forms_module QT_QML_MODULE_OUTPUT_DIRECTORY) + set(_morph_forms_qml_dest "${MORPH_INSTALL_QMLDIR}/MorphForms") + install(FILES "${_morph_forms_dir}/morph_forms_module.qmltypes" + DESTINATION "${_morph_forms_qml_dest}") + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/qt/forms/qml/" + DESTINATION "${_morph_forms_qml_dest}/qml" + FILES_MATCHING PATTERN "*.qml" PATTERN "*.js") + # qmldir names the plugin by its build-tree target; a consumer sees the + # exported name, which is what a static-Qt build's QML plugin import + # looks the plugin up by. + install(CODE " + file(READ \"${_morph_forms_dir}/qmldir\" _morph_qmldir) + string(REPLACE \"linktarget morph_forms_moduleplugin\" \"linktarget morph::forms_qmlplugin\" + _morph_qmldir \"\${_morph_qmldir}\") + file(WRITE \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${_morph_forms_qml_dest}/qmldir\" \"\${_morph_qmldir}\") + message(STATUS \"Installing: \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${_morph_forms_qml_dest}/qmldir\") + ") + unset(_morph_forms_dir) + unset(_morph_forms_qml_dest) + unset(_morph_forms_target) + unset(_morph_forms_export_name) + endif() + install(EXPORT morphTargets FILE morphTargets.cmake NAMESPACE morph:: @@ -844,6 +898,7 @@ if(MORPH_INSTALL) "${CMAKE_CURRENT_SOURCE_DIR}/cmake/morphConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/morphConfig.cmake" INSTALL_DESTINATION "${MORPH_CMAKE_INSTALL_DIR}" + PATH_VARS MORPH_INSTALL_QMLDIR ) # SameMajorVersion follows docs/spec/VERSIONING.md, which is what decides diff --git a/README.md b/README.md index 78c852bbe..a835cd1ef 100644 --- a/README.md +++ b/README.md @@ -461,9 +461,27 @@ target_link_libraries(your_app PRIVATE morph::net morph::offline_sqlite) ``` The components are `net` (`MORPH_BUILD_NET`, POSIX only), `offline_sqlite` -(`MORPH_BUILD_OFFLINE_SQLITE`), `qt` (`MORPH_BUILD_QT`) and `qt_forms` -(`MORPH_BUILD_FORMS_QML`). Asking for one that was not installed fails at -`find_package` and says which. +(`MORPH_BUILD_OFFLINE_SQLITE`), `qt` (`MORPH_BUILD_QT`), and `qt_forms` and +`forms_qml` (both `MORPH_BUILD_FORMS_QML`). Asking for one that was not +installed fails at `find_package` and says which. + +`forms_qml` is the `MorphForms` QML module (`DynamicForm`, `SlotRegistry`, +`CollectionView`, …), built as a static QML module. Link its plugin and import +it; `qt_forms` is the header-only `FormsControllerCore` an app's controller +wraps: + +```cmake +find_package(morph CONFIG REQUIRED COMPONENTS forms_qml qt_forms) +target_link_libraries(your_app PRIVATE morph::forms_qmlplugin morph::qt_forms) +# For qmllint / qmlls / your own qmlcachegen to see MorphForms' types: +list(APPEND QML_IMPORT_PATH "${morph_QML_IMPORT_PATH}") +``` + +The module resolves from resources embedded in the plugin, so nothing has to +be deployed next to the application; the installed +`/qml/MorphForms` (`MORPH_INSTALL_QMLDIR`) directory is for tooling. +`scripts/check_forms_qml_install.sh` builds and runs such an application +against an install. **Or vendor the repo.** `add_subdirectory(morph)` or `FetchContent` works and gives you the same `morph::morph` target. morph's own install rules turn diff --git a/cmake/morphConfig.cmake.in b/cmake/morphConfig.cmake.in index 307204b65..c5ea65f6c 100644 --- a/cmake/morphConfig.cmake.in +++ b/cmake/morphConfig.cmake.in @@ -65,6 +65,14 @@ endif() if("qt_forms" IN_LIST morph_KNOWN_COMPONENTS) find_dependency(Qt6 6.5 COMPONENTS Core) endif() +# The MorphForms QML module: morph::forms_qml (backing library) and +# morph::forms_qmlplugin, which is what an application links. The import +# directory holding its qmldir/qmltypes is exported for tooling, e.g. +# list(APPEND QML_IMPORT_PATH "${morph_QML_IMPORT_PATH}"). +if("forms_qml" IN_LIST morph_KNOWN_COMPONENTS) + find_dependency(Qt6 6.5 COMPONENTS Core Gui Qml Quick) + set_and_check(morph_QML_IMPORT_PATH "@PACKAGE_MORPH_INSTALL_QMLDIR@") +endif() if(_morph_added_prefix) list(REMOVE_ITEM CMAKE_PREFIX_PATH "${PACKAGE_PREFIX_DIR}") diff --git a/docs/spec/forms/forms.md b/docs/spec/forms/forms.md index 66eb2bbfc..f936def73 100644 --- a/docs/spec/forms/forms.md +++ b/docs/spec/forms/forms.md @@ -1146,8 +1146,22 @@ renderer for it, Qt/QML, as a reusable component rather than example code. structurally, not by name). It builds whenever `-DMORPH_BUILD_FORMS_QML=ON`, independent of `MORPH_BUILD_EXAMPLES` — an app depends on it directly (`target_link_libraries(... morph_forms_moduleplugin)` plus `import - MorphForms` in its own QML) instead of copying or forking it. -- **`include/morph/qt/forms/forms_controller_core.hpp`** ships + MorphForms` in its own QML) instead of copying or forking it. **Installed**, + it is the `forms_qml` component: `find_package(morph CONFIG REQUIRED + COMPONENTS forms_qml qt_forms)` and `morph::forms_qmlplugin`. The install + carries the backing library, the plugin and the object libraries a static + QML module needs (its compiled resources and the plugin's static + initialiser — without them an application links and then finds no + `MorphForms` at run time), plus `qmldir`/`.qmltypes`/sources under + `MORPH_INSTALL_QMLDIR` for tooling, exported as `morph_QML_IMPORT_PATH`. The + installed `qmldir`'s `linktarget` names `morph::forms_qmlplugin`, the name a + static-Qt build's QML plugin import resolves. + `scripts/check_forms_qml_install.sh` (CI job `install-export-forms-qml`) + builds and runs an application against an install, and proves the plugin is + what it measured by running the same application without it. +- **`include/morph/qt/forms/forms_controller_core.hpp`** (component + `qt_forms`, which also ships the `qt_executor.hpp` it includes, so an install + without `MORPH_BUILD_QT` still compiles it) ships `morph::qt::forms::FormsControllerCore`, a header-only, model-agnostic template (no `Q_OBJECT` — Qt cannot register a class *template* for QML) that owns or composes over the `Bridge`/`BridgeHandler`/`QtExecutor` diff --git a/scripts/check_forms_qml_install.sh b/scripts/check_forms_qml_install.sh new file mode 100755 index 000000000..58220e78a --- /dev/null +++ b/scripts/check_forms_qml_install.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# Usage: bash scripts/check_forms_qml_install.sh [REPO_ROOT] +# +# Fails unless an application outside the tree can render a DynamicForm from +# the *installed* MorphForms QML module: `find_package(morph CONFIG REQUIRED +# COMPONENTS forms_qml qt_forms)`, link `morph::forms_qmlplugin`, `import +# MorphForms`, instantiate a form. +# +# The sibling check_install_export.sh stops at C++ headers, and Qt is not on +# its job. This one needs Qt 6.5+ (found the usual way: Qt6_DIR or +# CMAKE_PREFIX_PATH in the environment) and measures the part a header check +# cannot see: a static QML module is a backing library, a plugin *and* the +# object libraries carrying its compiled resources and static initialiser. +# Leave the last out and everything links -- the module is then simply not +# there at run time. So the consumer is run, not just built, and a second +# consumer that links the backing library without the plugin must fail at run +# time, or this check is not measuring the plugin at all. +set -euo pipefail + +repo_root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +repo_root="$(cd "$repo_root" && pwd)" +if command -v cygpath >/dev/null 2>&1; then + repo_root="$(cygpath -m "$repo_root")" +fi +readonly repo_root + +failures=0 +note() { printf 'ok: %s\n' "$*"; } +fail() { printf 'error: %s\n' "$*" >&2; failures=$((failures + 1)); } + +run_step() { + local description="$1"; shift + local output + if output="$("$@" 2>&1)"; then + return 0 + fi + fail "${description}" + printf '%s\n' "$output" >&2 + return 1 +} + +finish() { + if [ "$failures" -ne 0 ]; then + printf '\n%d forms QML install check(s) failed.\n' "$failures" >&2 + exit 1 + fi +} + +workspace="$(mktemp -d)" +trap 'rm -rf "$workspace" || true' EXIT +# Git Bash on Windows: hand CMake native paths, not /tmp/... ones. +if command -v cygpath >/dev/null 2>&1; then + workspace="$(cygpath -m "$workspace")" +fi +readonly build_dir="${workspace}/build" +readonly prefix="${workspace}/prefix" + +generator_args=() +if command -v ninja >/dev/null 2>&1; then + generator_args=(-G Ninja) +fi + +# ── 1. Build and install the module ───────────────────────────────────────── +run_step "the library did not configure with MORPH_BUILD_FORMS_QML=ON" \ + cmake -S "$repo_root" -B "$build_dir" ${generator_args[@]+"${generator_args[@]}"} \ + -DCMAKE_BUILD_TYPE=Release \ + -DMORPH_BUILD_TESTS=OFF \ + -DMORPH_BUILD_EXAMPLES=OFF \ + -DMORPH_BUILD_FORMS_QML=ON || finish +run_step "the forms QML module did not build" cmake --build "$build_dir" || finish +run_step "cmake --install failed" cmake --install "$build_dir" --prefix "$prefix" || finish + +# ── 2. The prefix must hold the module, not just morph ────────────────────── +libdir="lib" +if [ ! -f "${prefix}/lib/cmake/morph/morphConfig.cmake" ] && [ -d "${prefix}/lib64/cmake/morph" ]; then + libdir="lib64" +fi +for required in \ + "${libdir}/cmake/morph/morphTargets.cmake" \ + "${libdir}/qml/MorphForms/qmldir" \ + "${libdir}/qml/MorphForms/morph_forms_module.qmltypes" \ + "${libdir}/qml/MorphForms/qml/DynamicForm.qml" \ + "include/morph/qt/forms/forms_controller_core.hpp" \ + "include/morph/qt/qt_executor.hpp"; do + if [ ! -f "${prefix}/${required}" ]; then + fail "the install has no ${required}" + fi +done + +readonly targets_file="${prefix}/${libdir}/cmake/morph/morphTargets.cmake" +if [ -f "$targets_file" ]; then + for exported in morph::forms_qml morph::forms_qmlplugin; do + grep -q "^add_library(${exported} " "$targets_file" || fail "${exported} is not exported" + done + if grep -q '^add_library(morph::morph_' "$targets_file"; then + fail "a target is exported as '$(grep -o 'morph::morph_[a-z0-9_]*' "$targets_file" | head -1)' -- give it an EXPORT_NAME" + fi +fi +readonly qmldir_file="${prefix}/${libdir}/qml/MorphForms/qmldir" +if [ -f "$qmldir_file" ] && ! grep -q '^linktarget morph::forms_qmlplugin$' "$qmldir_file"; then + fail "the installed qmldir does not name the exported plugin target: $(grep '^linktarget' "$qmldir_file" || echo 'no linktarget line')" +fi +finish +note "the prefix holds the MorphForms module, its plugin and the controller core headers" + +# ── 3. A consumer renders a form from the installed module ────────────────── +write_consumer() { + local dir="$1" link="$2" + mkdir -p "$dir" + cat > "${dir}/CMakeLists.txt" < "${dir}/main.cpp" <<'EOF' +#include + +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + QGuiApplication app(argc, argv); + QQmlEngine engine; + QQmlComponent component(&engine); + component.setData(R"( + import QtQuick + import MorphForms + DynamicForm { + actionType: "Probe" + controller: null + slotRegistry: SlotRegistry {} + schema: ({ properties: { n: { type: "integer", "x-order": 0 } }, required: ["n"] }) + property int fieldCount: fields.length + } + )", QUrl{}); + std::unique_ptr form{component.create()}; + if (!form) { + std::fprintf(stderr, "%s\n", qPrintable(component.errorString())); + return 1; + } + int const fieldCount = form->property("fieldCount").toInt(); + std::printf("DynamicForm from the installed MorphForms module: %d field(s)\n", fieldCount); + return fieldCount == 1 ? 0 : 2; +} +EOF +} + +build_consumer() { + local dir="$1" + cmake -S "$dir" -B "${dir}/build" ${generator_args[@]+"${generator_args[@]}"} \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$prefix" && + cmake --build "${dir}/build" +} + +readonly consumer="${workspace}/consumer" +write_consumer "$consumer" morph::forms_qmlplugin +if run_step "a consumer linking morph::forms_qmlplugin did not build" build_consumer "$consumer"; then + if output="$(QT_QPA_PLATFORM=offscreen "${consumer}/build/consumer" 2>&1)"; then + note "$(printf '%s\n' "$output" | grep 'DynamicForm from the installed' || echo 'consumer ran')" + else + fail "the consumer built but could not instantiate DynamicForm from the installed module" + printf '%s\n' "$output" >&2 + fi +fi + +# ── 4. Vacuity guard: without the plugin, the module must be missing ──────── +readonly unplugged="${workspace}/unplugged" +write_consumer "$unplugged" morph::forms_qml +if run_step "a consumer linking only morph::forms_qml did not build" build_consumer "$unplugged"; then + if QT_QPA_PLATFORM=offscreen "${unplugged}/build/consumer" >/dev/null 2>&1; then + fail "a consumer without morph::forms_qmlplugin still loaded MorphForms, so step 3 did not measure the plugin" + else + note "without the plugin the module is not found, so step 3 measured it" + fi +fi + +finish +note "an application renders DynamicForm from the installed MorphForms module" diff --git a/src/qt/forms/CMakeLists.txt b/src/qt/forms/CMakeLists.txt index c5f9963a9..4afeb9e73 100644 --- a/src/qt/forms/CMakeLists.txt +++ b/src/qt/forms/CMakeLists.txt @@ -29,7 +29,12 @@ qt_add_qml_module(morph_forms_module SOURCES I18nCatalog.hpp I18nCatalog.cpp + # The object libraries a static QML module carries (its compiled + # resources, the plugin's static initialiser): an installed module is + # unusable without them, so the root CMakeLists.txt installs them with it. + OUTPUT_TARGETS morph_forms_output_targets ) +set(MORPH_FORMS_QML_OUTPUT_TARGETS ${morph_forms_output_targets} PARENT_SCOPE) target_link_libraries(morph_forms_module PUBLIC morph::morph Qt6::Quick Qt6::Qml) target_compile_features(morph_forms_module PUBLIC cxx_std_23)