diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc01dc4..eeb4421 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,12 @@ jobs: - name: Build vanilla Qt plugin test run: nix build '.#checks.x86_64-linux.vanilla-plugin' + # The check that instantiates cmake/LogosView*.in. It existed but was + # never listed here, so the templates had no CI coverage at all — which + # is part of why a duplicate of them could sit in the fixture unnoticed. + - name: Build .rep replica factory test + run: nix build '.#checks.x86_64-linux.rep-file-plugin' + - name: Header generator guard test run: nix build '.#checks.x86_64-linux.header-generator-guard' diff --git a/cmake/LogosViewPluginBase.cpp.in b/cmake/LogosViewPluginBase.cpp.in new file mode 100644 index 0000000..c3c9357 --- /dev/null +++ b/cmake/LogosViewPluginBase.cpp.in @@ -0,0 +1,18 @@ +// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT. +#include "LogosViewPluginBase.h" +#include "rep_@LOGOS_REP_BASE@_source.h" + +#include + +bool @LOGOS_REP_CLASS@ViewPluginBase::enableRemoting(QRemoteObjectHostBase* host) +{ + if (!host || !m_backend) return false; + // The backend must inherit @LOGOS_REP_CLASS@SimpleSource (which the rep + // compiler generates from @LOGOS_REP_BASE@.rep). Using the templated + // enableRemoting<@LOGOS_REP_CLASS@SourceAPI> overload publishes the + // typed source signature so typed replicas on the client side reach the + // Valid state and start receiving property updates. + auto* src = qobject_cast<@LOGOS_REP_CLASS@SimpleSource*>(m_backend); + if (!src) return false; + return host->enableRemoting<@LOGOS_REP_CLASS@SourceAPI>(src); +} diff --git a/cmake/LogosViewPluginBase.h.in b/cmake/LogosViewPluginBase.h.in new file mode 100644 index 0000000..2ed1be4 --- /dev/null +++ b/cmake/LogosViewPluginBase.h.in @@ -0,0 +1,44 @@ +// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT. +#pragma once + +#include + +class QObject; +class QRemoteObjectHostBase; + +// The module side of the LogosViewPlugin interface. ui-host declares it too, +// in logos-view-module-runtime/include/LogosViewPlugin.h; the two never share +// a header on purpose — a module plugin must compile against Qt alone, without +// the host runtime on its include path. They meet at runtime through the IID +// string, the standard Qt plugin-interface pattern. +// +// The agreement is CHECKED, not requested: logos-module-builder's +// `view-interface-abi` check (tests/test-view-interface-abi.nix, run by that +// repo's CI) compares the IID and the pure-virtual signature list on both +// sides and fails the build if they differ. +class LogosViewPlugin { +public: + virtual ~LogosViewPlugin() = default; + virtual QObject* viewObject() = 0; + virtual bool enableRemoting(QRemoteObjectHostBase* host) = 0; +}; + +#define LogosViewPlugin_iid "logos.view.plugin/1.0" +Q_DECLARE_INTERFACE(LogosViewPlugin, LogosViewPlugin_iid) + +// Concrete base generated per-module. Plugins inherit from this and call +// setBackend() once their backend QObject is constructed; enableRemoting() +// is implemented in the generated .cpp using the repc-generated +// @LOGOS_REP_CLASS@SourceAPI type, so typed replicas on the client side +// match signatures. +class @LOGOS_REP_CLASS@ViewPluginBase : public LogosViewPlugin { +public: + QObject* viewObject() override { return m_backend; } + bool enableRemoting(QRemoteObjectHostBase* host) override; + +protected: + void setBackend(QObject* backend) { m_backend = backend; } + +private: + QObject* m_backend = nullptr; +}; diff --git a/tests/rep-file-plugin/cmake/LogosViewReplicaFactory.cpp.in b/cmake/LogosViewReplicaFactory.cpp.in similarity index 100% rename from tests/rep-file-plugin/cmake/LogosViewReplicaFactory.cpp.in rename to cmake/LogosViewReplicaFactory.cpp.in diff --git a/cmake/LogosViewReplicaFactory.h.in b/cmake/LogosViewReplicaFactory.h.in new file mode 100644 index 0000000..4bb8e9e --- /dev/null +++ b/cmake/LogosViewReplicaFactory.h.in @@ -0,0 +1,41 @@ +// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT. +#pragma once + +#include +#include + +class QRemoteObjectNode; +class QMetaObject; + +// The module side of the LogosViewReplicaFactory plugin interface. The host +// side declares it too, in logos-view-module-runtime/include/ +// LogosViewReplicaFactory.h; the two never share a header on purpose — a +// module plugin must compile against Qt alone, without the host runtime on its +// include path. They meet at runtime through the IID string, the standard Qt +// plugin-interface pattern, and a mismatch is silent: qobject_cast returns +// nullptr and the view simply never appears. +// +// So the agreement is CHECKED, not requested. logos-module-builder's +// `view-interface-abi` check (tests/test-view-interface-abi.nix, run by that +// repo's CI) extracts the IID and the pure-virtual signature list from this +// template and from the runtime header and fails the build if they differ. +// Edit one side and that check goes red — do not add a "keep in sync" note +// here instead, one lived here for a long time and never stopped a drift. +class LogosViewReplicaFactory { +public: + virtual ~LogosViewReplicaFactory() = default; + virtual QObject* acquire(QRemoteObjectNode* node) = 0; + virtual const QMetaObject* replicaMetaObject() const = 0; +}; + +#define LogosViewReplicaFactory_iid "logos.view.replica_factory/1.0" +Q_DECLARE_INTERFACE(LogosViewReplicaFactory, LogosViewReplicaFactory_iid) + +class @LOGOS_FACTORY_CLASS@ : public QObject, public LogosViewReplicaFactory { + Q_OBJECT + Q_PLUGIN_METADATA(IID LogosViewReplicaFactory_iid) + Q_INTERFACES(LogosViewReplicaFactory) +public: + QObject* acquire(QRemoteObjectNode* node) override; + const QMetaObject* replicaMetaObject() const override; +}; diff --git a/cmake/README.md b/cmake/README.md new file mode 100644 index 0000000..7974bab --- /dev/null +++ b/cmake/README.md @@ -0,0 +1,64 @@ +# `LogosView*.in` — the view-plugin templates, and why they live here + +These four files are `configure_file` templates instantiated per module by +`logos_module(REP_FILE ...)`: + +| template | produces | linked into | +|---|---|---| +| `LogosViewPluginBase.{h,cpp}.in` | `ViewPluginBase` | the module plugin | +| `LogosViewReplicaFactory.{h,cpp}.in` | `ReplicaFactoryPlugin` | `_replica_factory` | + +They are pure Qt: `Q_OBJECT`, `Q_PLUGIN_METADATA`, `repc`-generated +`*SimpleSource`/`*Replica` types, `qmlRegisterUncreatableMetaObject`. Nothing +outside the Qt backend can use them. + +## The rule + +**There is exactly one copy of each, and it is this one.** No consumer keeps a +local copy — not even a test fixture. + +## Why this repo and not logos-module-builder + +`LogosModule.cmake` — the code that instantiates these templates — lives in +logos-module-builder, so that repo looks like the obvious home, and the +templates did live next to it. But two consumers need them: + +1. logos-module-builder's `LogosModule.cmake`, for every real `ui_qml` module; +2. this repo's `tests/rep-file-plugin` fixture, which is the check that proves + the repc + factory pipeline still produces a loadable plugin. + +The dependency runs **logos-module-builder → logos-plugin-qt**, one way. A +fixture inside logos-plugin-qt therefore cannot reach logos-module-builder, and +that is exactly how the fixture ended up holding a byte-identical second copy +with nothing comparing the two. This repo is the only place both consumers can +read from, so ownership moves to the direction that actually works: the +templates are published here, and logos-module-builder receives the directory +as `LOGOS_VIEW_TEMPLATE_DIR`. + +`LogosModule.cmake` itself does **not** move back here — that file is +logos-module-builder's and stays there. Only the Qt-specific templates it +instantiates are published from this side of the edge. + +## How consumers get them + +* nix: `packages..logos-view-templates`, and + `lib.buildPlugin` / `lib.generate` set `LOGOS_VIEW_TEMPLATE_DIR` in the + build environment automatically, so a module build needs no extra wiring. +* CMake: `logos_module()` resolves `LOGOS_VIEW_TEMPLATE_DIR` (cache variable + first, then environment) and hard-errors when it is unset. There is no + "look next to LogosModule.cmake" fallback — a silent fallback to a second + copy is the failure this layout exists to remove. + +## The interface declared inside the templates + +`LogosViewPlugin` and `LogosViewReplicaFactory` are also declared, separately, +by logos-view-module-runtime — the host that loads these plugins. That +duplication is deliberate and cannot be collapsed: a module plugin must build +against Qt alone, and logos-view-module-runtime depends on *this* repo, so the +include could only ever go the wrong way. The two declarations meet at runtime +via the IID string, where a mismatch is silent. + +That pair is enforced instead of documented: logos-module-builder's +`view-interface-abi` check (it is the one repo that can see both sides) extracts +the IID and the pure-virtual signature list from each and fails on any +difference. diff --git a/flake.nix b/flake.nix index 7db6251..6cc9562 100644 --- a/flake.nix +++ b/flake.nix @@ -71,7 +71,24 @@ # invisible until something depended on it. The CMake module is the # builder's build-system contract — it reads LOGOS_API_STYLE, # LOGOS_MODULE_GO_STATIC_LIBS, generated_code/ — so it lives there, once. + # + # cmake/ came back for a narrower reason: the four LogosView*.in + # templates. Those had the mirror-image problem — they sat next to + # LogosModule.cmake in the builder, but this repo's rep-file-plugin + # fixture also instantiates them and cannot reach the builder, so it kept + # a byte-identical second copy with nothing comparing the two. See + # cmake/README.md. packages = forAllSystems ({ pkgs, system, ... }: { + # The LogosView*.in templates, as a nameable output. `logos_module()` + # gets the same directory through LOGOS_VIEW_TEMPLATE_DIR; this output + # exists so a consumer (logos-module-builder's view-interface-abi + # check) can refer to the templates without depending on the layout of + # this repo's source tree. + logos-view-templates = pkgs.runCommand "logos-view-templates" { } '' + mkdir -p $out + cp ${./cmake}/LogosView*.in $out/ + ''; + logos-qt-host = import ./nix/qt-host.nix { inherit pkgs; src = ./.; diff --git a/lib/common.nix b/lib/common.nix index b014a8a..433c6cc 100644 --- a/lib/common.nix +++ b/lib/common.nix @@ -47,12 +47,21 @@ rec { pkgs.qt6.qtremoteobjects ]; + # The ONE copy of the LogosView*.in templates that logos_module(REP_FILE ...) + # instantiates. See ../cmake/README.md for why they are owned here rather + # than next to LogosModule.cmake: logos-module-builder depends on this repo + # and not the reverse, so this is the only directory both consumers of the + # templates — that repo's LogosModule.cmake, and this repo's + # tests/rep-file-plugin fixture — can read from. + viewTemplateDir = ../cmake; + # CMake flags for Qt plugin builds. # Only includes logosModule (for interface.h). # SDK flags are added by the builder layer, not here. commonCmakeFlags = { logosModule }: [ "-GNinja" "-DLOGOS_MODULE_ROOT=${logosModule}" + "-DLOGOS_VIEW_TEMPLATE_DIR=${viewTemplateDir}" ]; # Platform-specific post-build commands for library path fixing diff --git a/lib/default.nix b/lib/default.nix index f2abb4a..60e59ee 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -55,6 +55,15 @@ in { # whatever this repo happens to contain. env = { LOGOS_MODULE_ROOT = "${logosModule}"; + # The LogosView*.in templates logos_module(REP_FILE ...) instantiates. + # They are owned by THIS repo (see ../cmake/README.md) because + # logos-module-builder depends on it and not the reverse — that + # direction is what lets the one copy serve both that repo's + # LogosModule.cmake and this repo's tests/rep-file-plugin fixture. + # Passed as an env var as well as a cmake flag so a module that drives + # cmake itself still resolves it; LogosModule.cmake hard-errors when + # neither is set rather than falling back to a sibling directory. + LOGOS_VIEW_TEMPLATE_DIR = "${common.viewTemplateDir}"; } // extraEnv; meta = with lib; { description = config.description; @@ -104,6 +113,15 @@ in { # whatever this repo happens to contain. env = { LOGOS_MODULE_ROOT = "${logosModule}"; + # The LogosView*.in templates logos_module(REP_FILE ...) instantiates. + # They are owned by THIS repo (see ../cmake/README.md) because + # logos-module-builder depends on it and not the reverse — that + # direction is what lets the one copy serve both that repo's + # LogosModule.cmake and this repo's tests/rep-file-plugin fixture. + # Passed as an env var as well as a cmake flag so a module that drives + # cmake itself still resolves it; LogosModule.cmake hard-errors when + # neither is set rather than falling back to a sibling directory. + LOGOS_VIEW_TEMPLATE_DIR = "${common.viewTemplateDir}"; } // extraEnv; meta = with lib; { description = config.description; @@ -162,6 +180,10 @@ in { buildInputs = common.commonBuildInputs pkgs; shellHook = '' ${if logosModule != null then ''export LOGOS_MODULE_ROOT="${logosModule}"'' else ""} + # Same directory the nix build passes in, so a hand-run `cmake` inside a + # module's dev shell resolves the templates the way logos_module() does. + # Without it a ui_qml module would hit LogosModule.cmake's hard error. + export LOGOS_VIEW_TEMPLATE_DIR="${common.viewTemplateDir}" ''; }; diff --git a/tests/rep-file-plugin/CMakeLists.txt b/tests/rep-file-plugin/CMakeLists.txt index a80777b..a96a58c 100644 --- a/tests/rep-file-plugin/CMakeLists.txt +++ b/tests/rep-file-plugin/CMakeLists.txt @@ -7,8 +7,22 @@ set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Core RemoteObjects Qml) -# ── Locate templates (injected by the nix test harness into cmake/) ──────── -set(TEMPLATE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +# ── Locate templates ────────────────────────────────────────────────────── +# The fixture does NOT carry its own copy. It used to: the templates were +# moved in here when this repo's duplicate LogosModule.cmake was deleted, and +# they then sat byte-identical to logos-module-builder's with nothing comparing +# them. They are owned by this repo now (../../cmake) and the harness passes +# that directory in, the same way logos_module() receives it in a real build. +if(NOT LOGOS_VIEW_TEMPLATE_DIR AND DEFINED ENV{LOGOS_VIEW_TEMPLATE_DIR}) + set(LOGOS_VIEW_TEMPLATE_DIR "$ENV{LOGOS_VIEW_TEMPLATE_DIR}") +endif() +if(NOT LOGOS_VIEW_TEMPLATE_DIR) + message(FATAL_ERROR + "rep-file-plugin: LOGOS_VIEW_TEMPLATE_DIR is not set. Pass the " + "repository's cmake/ directory; this fixture has no copy to fall " + "back to, deliberately.") +endif() +set(TEMPLATE_DIR "${LOGOS_VIEW_TEMPLATE_DIR}") # ── .rep file under test ────────────────────────────────────────────────── set(REP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/rep_test.rep") diff --git a/tests/rep-file-plugin/cmake/LogosViewReplicaFactory.h.in b/tests/rep-file-plugin/cmake/LogosViewReplicaFactory.h.in deleted file mode 100644 index a4bee58..0000000 --- a/tests/rep-file-plugin/cmake/LogosViewReplicaFactory.h.in +++ /dev/null @@ -1,31 +0,0 @@ -// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT. -#pragma once - -#include -#include - -class QRemoteObjectNode; -class QMetaObject; - -// Local copy of the LogosViewReplicaFactory interface. The host side has its -// own copy in logos-view-module-runtime; they match by IID string, which is -// the standard Qt plugin-interface pattern. Keep the method signatures in -// sync with logos-view-module-runtime/src/LogosViewReplicaFactory.h. -class LogosViewReplicaFactory { -public: - virtual ~LogosViewReplicaFactory() = default; - virtual QObject* acquire(QRemoteObjectNode* node) = 0; - virtual const QMetaObject* replicaMetaObject() const = 0; -}; - -#define LogosViewReplicaFactory_iid "logos.view.replica_factory/1.0" -Q_DECLARE_INTERFACE(LogosViewReplicaFactory, LogosViewReplicaFactory_iid) - -class @LOGOS_FACTORY_CLASS@ : public QObject, public LogosViewReplicaFactory { - Q_OBJECT - Q_PLUGIN_METADATA(IID LogosViewReplicaFactory_iid) - Q_INTERFACES(LogosViewReplicaFactory) -public: - QObject* acquire(QRemoteObjectNode* node) override; - const QMetaObject* replicaMetaObject() const override; -}; diff --git a/tests/test-rep-file-plugin.nix b/tests/test-rep-file-plugin.nix index 3a030dc..18588da 100644 --- a/tests/test-rep-file-plugin.nix +++ b/tests/test-rep-file-plugin.nix @@ -4,6 +4,11 @@ let pluginSrc = ./rep-file-plugin; + # The ONE copy of the templates, straight from this repo's cmake/. The + # fixture is handed the same directory logos_module() is handed in a real + # build, so this check now exercises the file every ui_qml module compiles — + # not a private duplicate of it that could pass while the real one is broken. + viewTemplates = ../cmake; in pkgs.stdenv.mkDerivation { pname = "logos-plugin-qt-rep-file-test"; @@ -18,19 +23,19 @@ pkgs.stdenv.mkDerivation { dontUseCmakeConfigure = true; - # The templates live in the fixture's own cmake/ (tests/rep-file-plugin/cmake). - # They used to be copied in from this repo's cmake/ — the same directory that - # held the duplicate LogosModule.cmake. That directory is gone: the CMake - # module and the templates it instantiates are logos-module-builder's, and - # exist once, there. What is left here is a test fixture, and it says so by - # living under tests/. + # The templates are this repo's, and there is exactly one copy of them: + # cmake/. LogosModule.cmake still belongs to logos-module-builder — only the + # Qt-specific templates it instantiates are published from this side, because + # logos-module-builder depends on this repo and not the reverse, so this is + # the only directory both it and this fixture can read. See cmake/README.md. + LOGOS_VIEW_TEMPLATE_DIR = "${viewTemplates}"; buildPhase = '' runHook preBuild mkdir -p build cd build - cmake .. -GNinja + cmake .. -GNinja -DLOGOS_VIEW_TEMPLATE_DIR="$LOGOS_VIEW_TEMPLATE_DIR" ninja cd ..