feat: the view templates exist once, and this repo is where

The four LogosView*.in templates had two byte-identical copies:

  logos-module-builder/cmake/                         (LIVE — LogosModule.cmake)
  logos-plugin-qt/tests/rep-file-plugin/cmake/        (fixture — rep-file-plugin)

Nothing compared them. This is the fifth defect of that exact shape in this
refactor, and it was created by the fix for the fourth: when this repo's
duplicate LogosModule.cmake was deleted, the templates it read were MOVED into
the fixture rather than removed, because the fixture still needed them.

It needed them because it cannot reach where they went. The edge runs
logos-module-builder -> logos-plugin-qt, one way, so a fixture inside this repo
can never consume logos-module-builder's copy, and this repo cannot re-export
it either. "Own them in the builder" is not reachable; a copy in the fixture is
what "own them in the builder" degrades into.

So ownership follows the direction that works. The templates are this repo's:

  * cmake/ holds the one copy, with cmake/README.md stating the rule and the
    argument for it.
  * packages.<sys>.logos-view-templates publishes them as a nameable output.
  * lib.buildPlugin / lib.generate set LOGOS_VIEW_TEMPLATE_DIR (cmake flag and
    env var) on every plugin build, so logos-module-builder's LogosModule.cmake
    receives the directory without either repo growing a new input.
  * devShellInputs exports it too, so a hand-run cmake in a module dev shell
    resolves it the same way.

LogosModule.cmake itself does NOT come back here. That file is the builder's
build-system contract and stays there; only the Qt-specific templates it
instantiates are published from this side of the edge.

The fixture now takes the directory from the harness and has nothing to fall
back to — a missing LOGOS_VIEW_TEMPLATE_DIR is a FATAL_ERROR, not a quiet
second copy. Proof it reads the owned file and not a leftover: changing the IID
in cmake/LogosViewReplicaFactory.h.in makes rep-file-plugin fail its
"IID found in binary" assertion.

rep-file-plugin is also added to CI. It is the check that instantiates these
templates and it was never listed in the workflow, so the templates had no CI
coverage at all — part of why a duplicate could sit in the fixture unnoticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dario Gabriel Lipicar
2026-08-16 09:07:05 -03:00
co-authored by Claude Opus 5
parent 34704d1841
commit 3d7e3e68b3
12 changed files with 249 additions and 40 deletions
+6
View File
@@ -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'
+18
View File
@@ -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 <QRemoteObjectHost>
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);
}
+44
View File
@@ -0,0 +1,44 @@
// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT.
#pragma once
#include <QtPlugin>
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;
};
+41
View File
@@ -0,0 +1,41 @@
// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT.
#pragma once
#include <QObject>
#include <QtPlugin>
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;
};
+64
View File
@@ -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` | `<Rep>ViewPluginBase` | the module plugin |
| `LogosViewReplicaFactory.{h,cpp}.in` | `<Rep>ReplicaFactoryPlugin` | `<name>_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.<system>.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.
+17
View File
@@ -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 = ./.;
+9
View File
@@ -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
+22
View File
@@ -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}"
'';
};
+16 -2
View File
@@ -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")
@@ -1,31 +0,0 @@
// Auto-generated by logos_module(REP_FILE ...) — DO NOT EDIT.
#pragma once
#include <QObject>
#include <QtPlugin>
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;
};
+12 -7
View File
@@ -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 ..