From dfe97190ddda89d663e926e60c741e6f34df8e57 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 4 Oct 2026 22:14:54 +0200 Subject: [PATCH 1/3] build: build with clang 23 -- suppress its lifetime-safety analysis Clang 23 adds a lifetime-safety analysis that -Weverything enables and -Werror makes fatal. It raises suggestions to annotate declarations (including in Qt's generated moc code), reports that existing MORPH_LIFETIMEBOUND contracts cannot be verified, and false invalidations (push_back on a vector& parameter). None is a defect, and the analysis does not yet follow this code. The three groups are suppressed through _morph_clang_suppression_if_supported, so older clang, CI's included, is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2 --- cmake/compiler_options.cmake | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmake/compiler_options.cmake b/cmake/compiler_options.cmake index 00de1f525..59e63b60a 100644 --- a/cmake/compiler_options.cmake +++ b/cmake/compiler_options.cmake @@ -230,6 +230,20 @@ elseif(MORPH_COMPILER_FAMILY STREQUAL "Clang") # headers. -Wno-thread-safety-negative ) + # Clang 23's lifetime-safety analysis, which -Weverything enables. It + # raises three kinds of diagnostic here, none a defect: suggestions to + # annotate a declaration (including in Qt's generated moc code, which + # we cannot annotate), reports that an existing MORPH_LIFETIMEBOUND + # cannot be verified, and false invalidations -- `out.push_back(...)` + # on a `std::vector& out` parameter is reported as using `out` + # after invalidating it. The analysis is new and does not yet follow + # this code; MORPH_LIFETIMEBOUND keeps stating the contracts for the + # call-site checks that predate it. Older clang has none of these + # groups. -Wno-lifetime-safety alone is not enough: the suggestions + # and the lifetimebound check are separate groups under -Weverything. + _morph_clang_suppression_if_supported(-Wno-lifetime-safety) + _morph_clang_suppression_if_supported(-Wno-lifetime-safety-suggestions) + _morph_clang_suppression_if_supported(-Wno-lifetime-safety-lifetimebound-violation) # (g) AppleClang only, deliberately: the one place where the macOS # toolchain's -Weverything set diverges from the Clang the CI legs use, From f441a6fc709b109e5c6ff151deb361a477ed610f Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 4 Oct 2026 22:14:54 +0200 Subject: [PATCH 2/3] build: give every QML module an output directory ending in its URI; set QTP0004 Qt warns at configure time when a QML module's OUTPUT_DIRECTORY does not end in its target path, because qmllint then cannot use the build tree as an import path. Each module (the ladder rungs, MorphForms, the forms demo) now writes to /; the install step reads the location from the target property, so it follows. Modules whose .qml files sit in a subdirectory set QTP0004 to NEW, guarded for Qt versions that predate it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2 --- cmake/morph_add_rung.cmake | 3 +++ examples/bank/gui/CMakeLists.txt | 7 +++++++ examples/forms/gui_qml/CMakeLists.txt | 10 ++++++++++ src/qt/forms/CMakeLists.txt | 10 ++++++++++ 4 files changed, 30 insertions(+) diff --git a/cmake/morph_add_rung.cmake b/cmake/morph_add_rung.cmake index 5083bc813..bb566ffa1 100644 --- a/cmake/morph_add_rung.cmake +++ b/cmake/morph_add_rung.cmake @@ -264,9 +264,12 @@ function(morph_add_rung) set_source_files_properties("${_qml_file}" PROPERTIES QT_RESOURCE_ALIAS "${_qml_name}") endforeach() qt_add_library(ladder_${_rung}_qml STATIC) + # The output directory ends in the URI so the build tree is a valid + # QML import path for this module, which qmllint relies on. qt_add_qml_module(ladder_${_rung}_qml URI ${_qml_uri} VERSION 1.0 + OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${_qml_uri}" QML_FILES ${_qml_files} ) target_link_libraries(ladder_${_rung}_qml PUBLIC morph_forms_moduleplugin Qt6::Quick Qt6::Qml) diff --git a/examples/bank/gui/CMakeLists.txt b/examples/bank/gui/CMakeLists.txt index 25b0a74c7..1724b30c8 100644 --- a/examples/bank/gui/CMakeLists.txt +++ b/examples/bank/gui/CMakeLists.txt @@ -53,6 +53,13 @@ qt_add_executable(bank_gui main.cpp ) +# The module's .qml files live in qml/, so it needs a qmldir there too for +# them to resolve the module's own types; Qt versions that predate the policy +# do not know it. +if(QT_KNOWN_POLICY_QTP0004) + qt_policy(SET QTP0004 NEW) +endif() + qt_add_qml_module(bank_gui URI BankGui VERSION 1.0 diff --git a/examples/forms/gui_qml/CMakeLists.txt b/examples/forms/gui_qml/CMakeLists.txt index 0f1a38584..dd59f60cc 100644 --- a/examples/forms/gui_qml/CMakeLists.txt +++ b/examples/forms/gui_qml/CMakeLists.txt @@ -12,9 +12,19 @@ qt_add_library(lab_forms_demo_module STATIC) +# The module's .qml files live in qml/, so it needs a qmldir there too for +# them to resolve the module's own types; Qt versions that predate the policy +# do not know it. +if(QT_KNOWN_POLICY_QTP0004) + qt_policy(SET QTP0004 NEW) +endif() + +# The output directory ends in the URI so the build tree is a valid QML import +# path for this module, which qmllint relies on. qt_add_qml_module(lab_forms_demo_module URI LabFormsDemo VERSION 1.0 + OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/LabFormsDemo" QML_FILES qml/Main.qml qml/AppShell.qml diff --git a/src/qt/forms/CMakeLists.txt b/src/qt/forms/CMakeLists.txt index 382e6d87e..668b5a335 100644 --- a/src/qt/forms/CMakeLists.txt +++ b/src/qt/forms/CMakeLists.txt @@ -16,9 +16,19 @@ qt_add_library(morph_forms_module STATIC) +# The module's .qml files live in qml/, so it needs a qmldir there too for +# them to resolve the module's own types; Qt versions that predate the policy +# do not know it. +if(QT_KNOWN_POLICY_QTP0004) + qt_policy(SET QTP0004 NEW) +endif() + +# The output directory ends in the URI so the build tree is a valid QML import +# path for this module, which qmllint relies on. qt_add_qml_module(morph_forms_module URI MorphForms VERSION 1.0 + OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/MorphForms" QML_FILES qml/JsonExact.js qml/DynamicForm.qml From 308d1b852a3ea6f273f34632a609bc380067e732 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 4 Oct 2026 22:42:02 +0200 Subject: [PATCH 3/3] lims: give the desktop client a session, so its actions are not all refused Every mutating lims action refuses an empty principal, and the desktop client built its bridges without a session, so RegisterClient and every other write failed with "mutating action dispatched with an empty principal". The presenter tests installed a principal of their own and the GUI smoke test dispatches nothing, so nothing exercised the client as it ships. installDesktopSession() now lives in gui_lib, called by main.cpp in local mode and by a test that registers a client and a sample through a bridge set up the same way; without the session that test fails with the error above. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0141t3QdB4FkiHqiCX2M2eF2 --- examples/lims/gui/main.cpp | 4 +++ examples/lims/gui_lib/desktop_session.hpp | 34 ++++++++++++++++++++ examples/lims/tests/test_lims_presenters.cpp | 14 ++++++++ 3 files changed, 52 insertions(+) create mode 100644 examples/lims/gui_lib/desktop_session.hpp diff --git a/examples/lims/gui/main.cpp b/examples/lims/gui/main.cpp index 0cf94ce79..df2cbb2e4 100644 --- a/examples/lims/gui/main.cpp +++ b/examples/lims/gui/main.cpp @@ -42,6 +42,7 @@ #include #include +#include "desktop_session.hpp" #include "gui/app_context.hpp" #include "lims/db/database.hpp" #include "result_qml_bridge.hpp" @@ -88,6 +89,9 @@ int main(int argc, char** argv) { // between them — are built here, once, and live until the process // exits. A `Remote` context is not usable the line after its // constructor returns, which is what `onReady` is for. + if (!serverUrl) { + ::lims::gui::installDesktopSession(ctx.bridge()); + } sampleBridge = std::make_unique(ctx.bridge(), ctx.executor()); resultBridge = std::make_unique(ctx.bridge(), ctx.executor()); diff --git a/examples/lims/gui_lib/desktop_session.hpp b/examples/lims/gui_lib/desktop_session.hpp new file mode 100644 index 000000000..dfdbdc244 --- /dev/null +++ b/examples/lims/gui_lib/desktop_session.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +/// @file +/// The session the single-user desktop client dispatches under. +/// +/// Every mutating lims action refuses an empty principal +/// (`lims::requirePrincipal`), so a client that builds its bridges without a +/// session can do nothing but read. In local mode no authorizer runs, so a +/// bare principal is the whole session; a fixed one is enough because a +/// single-process desktop deployment has no second user to tell apart. +/// +/// It lives here rather than in `gui/main.cpp` so the tests can install the +/// same session the executable does, and an action dispatched through it is +/// checked end to end. + +namespace lims::gui { + +/// @brief The principal the local desktop client dispatches under. +inline constexpr std::string_view kDesktopPrincipal = "demo"; + +/// @brief Installs the local desktop client's session on @p bridge, before +/// any handler is built against it. +/// @param bridge The bridge every desktop handler dispatches through. +inline void installDesktopSession(::morph::bridge::Bridge& bridge) { + bridge.setDefaultSession(::morph::session::Context{.principal = std::string{kDesktopPrincipal}}); +} + +} // namespace lims::gui diff --git a/examples/lims/tests/test_lims_presenters.cpp b/examples/lims/tests/test_lims_presenters.cpp index ee3d0e7b6..11d749b2a 100644 --- a/examples/lims/tests/test_lims_presenters.cpp +++ b/examples/lims/tests/test_lims_presenters.cpp @@ -18,6 +18,7 @@ #include #include +#include "desktop_session.hpp" #include "result_presenter.hpp" #include "sample_presenter.hpp" #include "testkit/backend_rig.hpp" @@ -130,6 +131,19 @@ TEST_CASE("SamplePresenter routes registration and every lifecycle transition", CHECK_FALSE(presenter.busy()); } +TEST_CASE("The local desktop session registers a client and a sample", "[lims][gui][presenter]") { + // The bridge is set up exactly as gui/main.cpp sets up its own in local + // mode, so a client that ships without a session fails here. + DbFixture fixture; + auto rig = std::make_unique(Mode::Local, 1); + lims::gui::installDesktopSession(rig->bridge(0)); + lims::gui::SamplePresenter presenter{rig->bridge(0), rig->executor()}; + + const auto registered = registerSampleVia(presenter); + CHECK(registered.state == lims::SampleState::Registered); + CHECK(registered.id.hasValue()); +} + TEST_CASE("SamplePresenter surfaces a model refusal as a displayable message", "[lims][gui][presenter]") { DbFixture fixture; auto rig = makeAuthedRig("alice");