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, 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/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"); 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