mirror of
https://github.com/logos-co/logos-module-builder.git
synced 2026-08-27 18:21:16 +00:00
modulePreConfigure.nix switches the `--backend ui` invocation to
logos-view-generator (logos-view-module), which now owns that emitter and
the logos_ui_plugin_context.h it pairs with.
THE REASON THIS IS NOT COSMETIC. This builder pins logos-qt-sdk at 4a1104c,
which predates b7b82e5 — the commit that added the module teardown hook to
the ui emitter. At 4a1104c BOTH halves lack it, so every build has been
green and self-consistent, and the hook has NEVER REACHED A SINGLE SHIPPED
ui_qml MODULE. Measured on the same module, before and after, by loading the
built plugin with QPluginLoader and dumping its QMetaObject:
BASELINE MIGRATED
initLogos(LogosAPI*) unloadFinished() [signal]
initLogos(LogosAPI*)
int aboutToUnload() [invokable]
So this delivers the hook for the first time rather than preserving it.
LOGOS_VIEW_INCLUDE_DIR is added BEFORE the qt-sdk root in
LogosModule.cmake, so the emitter and its header resolve from ONE pin. That
ordering is load-bearing until logos-qt-sdk stops installing its copy of
logos_ui_plugin_context.h — anything going through logos_module() is safe;
a hand-run cmake putting LOGOS_QT_SDK_ROOT first would silently get the
wrong header. Called out in the code rather than left implicit.
Blast radius is 7 modules, not one: package_manager_ui, chat_ui, wallet_ui,
test_uiqml_probe, test_fullapi_ui, calc_ui_cpp and templates/ui-qml-backend
all match ui_qml + interface:universal. None override aboutToUnload(), and
the base default is Synchronous, so every one answers 0 and no host waits —
the change is additive. Only package_manager_ui was built end to end.
Three assertions in tests/test-module-pre-configure.nix pin the binary
choice; repointing at logos-qt-generator fails them. Also corrects a comment
there that claimed "the ui backend still legitimately uses qt-sdk's".
7/7 checks green, 399 unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
267 lines
13 KiB
Nix
267 lines
13 KiB
Nix
# Shell snippets prepended to mkLogosModule / mkLogosModuleTests preConfigure.
|
|
{ lib }:
|
|
|
|
let
|
|
# accounts_module -> AccountsModuleImpl
|
|
defaultImplClassFromName = moduleName:
|
|
let
|
|
parts = lib.filter (s: s != "") (lib.splitString "_" moduleName);
|
|
cap = s:
|
|
if s == "" then ""
|
|
else lib.toUpper (lib.substring 0 1 s) + lib.substring 1 (builtins.stringLength s - 1) s;
|
|
in
|
|
lib.concatStrings (map cap parts) + "Impl";
|
|
|
|
# Copy resolved external library outputs into ./lib for CMake EXTERNAL_LIBS / includes
|
|
copyExternalLibsToLib = externalLibs:
|
|
let
|
|
names = builtins.attrNames externalLibs;
|
|
one = name:
|
|
let
|
|
v = externalLibs.${name};
|
|
in
|
|
if v == null then ""
|
|
else ''
|
|
if [ -d "${v}/lib" ]; then
|
|
cp -f "${v}"/lib/* lib/ 2>/dev/null || true
|
|
fi
|
|
# Windows ships a shared library's runtime half in bin/ (CMake's
|
|
# RUNTIME destination). Mirror of the staging in
|
|
# logos-plugin-qt/lib/buildPlugin.nix -- see the longer note there
|
|
# for why only this library's own files are taken, not all of bin/.
|
|
if [ -d "${v}/bin" ]; then
|
|
for f in "${v}"/bin/lib${name}.dll "${v}"/bin/${name}.dll; do
|
|
[ -f "$f" ] && cp -fL "$f" lib/ 2>/dev/null || true
|
|
done
|
|
fi
|
|
if [ -d "${v}/include" ]; then
|
|
cp -f "${v}"/include/*.h lib/ 2>/dev/null || true
|
|
fi
|
|
'';
|
|
in
|
|
if names == [] then ""
|
|
else ''
|
|
mkdir -p lib
|
|
${lib.concatMapStringsSep "\n" one names}
|
|
'';
|
|
|
|
# macOS: fix install_name on copied dylibs so tests/runtime resolve via RPATH
|
|
fixupDarwinDylibs = ''
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
for f in lib/*.dylib; do
|
|
[ -f "$f" ] || continue
|
|
bn=$(basename "$f")
|
|
install_name_tool -id "@rpath/$bn" "$f" 2>/dev/null || true
|
|
done
|
|
fi
|
|
'';
|
|
|
|
universalCodegen = config:
|
|
let
|
|
cg = config.codegen or {};
|
|
implClass = cg.impl_class or (defaultImplClassFromName config.name);
|
|
ihRaw = cg.impl_header or "${config.name}_impl.h";
|
|
fromPath =
|
|
if lib.hasInfix "/" ihRaw then ihRaw else "src/${ihRaw}";
|
|
# Include string embedded in generated glue (basename when path is qualified)
|
|
implHeaderInclude =
|
|
if lib.hasInfix "/" ihRaw then builtins.baseNameOf ihRaw else ihRaw;
|
|
in
|
|
''
|
|
echo "logos-module-builder: generating universal module glue (${config.name})..."
|
|
# Universal modules are header-first cdylibs: same Qt-free mechanism as
|
|
# the `cdylib` interface, but the LIDL contract is DERIVED from the impl
|
|
# header instead of hand-committed. The author still writes only the
|
|
# impl class (deriving LogosModuleContext); the module's own TUs stay
|
|
# Qt-free and its outbound modules().<dep> calls go through the lp_* C
|
|
# ABI (apiStyle=lp). Qt is confined to the generated uniform glue.
|
|
#
|
|
# 1. Derive the LIDL contract from the impl header. Doubles as the
|
|
# published events sidecar consumed by dependents' typed-event codegen.
|
|
logos-cpp-generator --header-to-lidl "${fromPath}" \
|
|
--impl-class ${implClass} \
|
|
--metadata metadata.json \
|
|
-o ./generated_code/${config.name}.lidl
|
|
# 2. The uniform Qt-plugin glue over the common module-impl C ABI
|
|
# (logos_host loads it unchanged — load ABI preserved).
|
|
logos-qt-host-generator --lidl ./generated_code/${config.name}.lidl \
|
|
--backend cdylib \
|
|
${lib.optionalString ((config.concurrency or "single") == "multi") "--concurrency multi"} \
|
|
--output-dir ./generated_code
|
|
# 3. The Qt-FREE C-ABI export wrapper (+ typed event emitters) around
|
|
# the hand-written impl class.
|
|
# No --concurrency here: the C++ cdylib's logos_module_dispatch is
|
|
# already safe to call concurrently (no lock across the handler), so the
|
|
# multi worker pool lives entirely in the Qt glue above. The author owns
|
|
# thread-safety of the impl's methods under concurrency:"multi".
|
|
logos-cpp-generator --lidl ./generated_code/${config.name}.lidl \
|
|
--backend cdylib \
|
|
--impl-class ${implClass} \
|
|
--impl-header ${implHeaderInclude} \
|
|
--output-dir ./generated_code
|
|
'';
|
|
|
|
# Cdylib authoring: the module is (or wraps) a cdylib exporting the common
|
|
# module-impl C ABI (logos_module_impl.h in logos-protocol). The generator
|
|
# emits only the uniform Qt-plugin glue from the LIDL contract; the C
|
|
# exports come from the module's own language backend — the C++ SDK's
|
|
# `--from-header --backend cdylib` wrapper or the Rust SDK's
|
|
# `lidl-gen --provider`. The glue is identical either way.
|
|
cdylibCodegen = config:
|
|
let
|
|
cg = config.codegen or {};
|
|
# A rust-first module (codegen.rust.trait) derives its .lidl from the trait;
|
|
# the builder stages it at generated_code/<name>.lidl (mkLogosModule's
|
|
# lidlStaging), so no codegen.lidl is needed. Otherwise it's the committed
|
|
# contract named by codegen.lidl.
|
|
lidlFile =
|
|
if ((cg.rust or {}).trait or null) != null
|
|
then "generated_code/${config.name}.lidl"
|
|
else (cg.lidl or (throw "cdylib interface requires codegen.lidl in metadata.json"));
|
|
# Contract-first C++ flavor: when codegen names an impl_class, the
|
|
# generator ALSO emits the C-ABI export wrapper (+ typed events) around
|
|
# that hand-written Qt-free class. Without it (e.g. Rust modules whose
|
|
# exports come from lidl-gen --provider) only the uniform glue is
|
|
# generated.
|
|
implClass = cg.impl_class or null;
|
|
implHeaderRaw = cg.impl_header or "${config.name}_impl.h";
|
|
implHeader =
|
|
if lib.hasInfix "/" implHeaderRaw
|
|
then builtins.baseNameOf implHeaderRaw
|
|
else implHeaderRaw;
|
|
implFlags =
|
|
if implClass == null then ""
|
|
else "--impl-class ${implClass} --impl-header ${implHeader}";
|
|
in
|
|
''
|
|
echo "logos-module-builder: generating cdylib Qt glue (${config.name})..."
|
|
logos-qt-host-generator --lidl "${lidlFile}" \
|
|
--backend cdylib \
|
|
${lib.optionalString ((config.concurrency or "single") == "multi") "--concurrency multi"} \
|
|
--output-dir ./generated_code
|
|
${lib.optionalString (implClass != null) ''
|
|
# Contract-first C++ flavor: the Qt-FREE C-ABI export wrapper
|
|
# (+ typed event emitters) around the hand-written impl class.
|
|
# No --concurrency: the C++ cdylib dispatch is already concurrency-safe;
|
|
# the multi worker pool lives in the Qt glue (logos-qt-generator above).
|
|
logos-cpp-generator --lidl "${lidlFile}" \
|
|
--backend cdylib \
|
|
${implFlags} \
|
|
--output-dir ./generated_code
|
|
''}
|
|
'';
|
|
|
|
# UI plugin backends (type=ui_qml + interface=universal): the USER
|
|
# writes the .rep (the view contract) and the *Backend class (deriving
|
|
# <RepClass>SimpleSource + LogosUiPluginContext); the view generator emits
|
|
# only the *Interface.h and the *Plugin glue that wires the (Qt-typed)
|
|
# LogosModules aggregate into the backend on initLogos.
|
|
#
|
|
# The binary is logos-VIEW-generator (logos-view-module), not
|
|
# logos-qt-generator (logos-qt-sdk). Both shipped the same emitter for a
|
|
# while and they rotted apart: logos-qt-sdk#38 added the module teardown
|
|
# hook to the copy this line used to call, and the other copy never got it.
|
|
#
|
|
# That divergence was invisible, and would have become permanent the moment
|
|
# this line was repointed without reconciling first: ui-host reaches
|
|
# aboutToUnload() BY NAME through the meta-object, so a generated plugin
|
|
# class that does not declare it simply has no such meta-method --
|
|
# QMetaObject::invokeMethod returns false and the host moves on, which is
|
|
# indistinguishable from a view answering "Synchronous, nothing to wait for".
|
|
# No build, load or call fails; every view just silently loses its chance to
|
|
# finish. logos-view-module's `ui-plugin-metaobject` check now compiles the
|
|
# emitted plugin and drives that handshake through QPluginLoader, so the
|
|
# regression cannot recur silently in the new home.
|
|
#
|
|
# The generator lives with the LogosView*.in templates its output is compiled
|
|
# against and with logos_ui_plugin_context.h, which its output calls into --
|
|
# one authoring surface, one repo, matching how logos-plugin-qt owns the
|
|
# cdylib Qt-plugin glue.
|
|
#
|
|
# `--backend ui` is spelled explicitly even though it is that binary's
|
|
# default: it keeps the call site self-describing, and logos-view-generator
|
|
# REFUSES an unrecognised --backend rather than silently defaulting.
|
|
uiCodegen = config:
|
|
let
|
|
cg = config.codegen or {};
|
|
repFile = cg.rep or "src/${config.name}.rep";
|
|
backendFlags =
|
|
lib.optionalString (cg ? backend_class) " --backend-class ${cg.backend_class}"
|
|
+ lib.optionalString (cg ? backend_header) " --backend-header ${cg.backend_header}";
|
|
in
|
|
''
|
|
echo "logos-module-builder: generating ui plugin glue (${config.name})..."
|
|
logos-view-generator --backend ui \
|
|
--metadata metadata.json \
|
|
--rep "${repFile}"${backendFlags} \
|
|
--output-dir ./generated_code
|
|
'';
|
|
|
|
autoCodegen = config:
|
|
if config.interface == "universal" && (config.type or "core") == "ui_qml"
|
|
then uiCodegen config
|
|
else if config.interface == "universal" then universalCodegen config
|
|
else if config.interface == "cdylib" then cdylibCodegen config
|
|
# `interface: "provider"` (LOGOS_METHOD dispatch via
|
|
# `logos-cpp-generator --provider-header`) was removed. Throw rather than
|
|
# falling through to the `else ""` no-op below: an unrecognised interface
|
|
# silently generates NO glue, so the module would build green and then be
|
|
# un-callable from every consumer.
|
|
else if config.interface == "provider" then
|
|
throw ("logos-module-builder: module '${config.name}' declares the removed "
|
|
+ "interface \"provider\". Use interface \"universal\": write a plain "
|
|
+ "src/${config.name}_impl.h and the contract is derived from it.")
|
|
# `legacy` — the default when metadata.json omits `interface` — generates no
|
|
# glue at all. For a CONSUMER that is correct and normal: a ui_qml view
|
|
# plugin is not loaded by liblogos, and a fixture that only builds tests has
|
|
# nothing to expose. For a module that ships a plugin liblogos loads and
|
|
# other modules call, it is the silent form of exactly what the `provider`
|
|
# branch above throws for — the module builds green and is un-callable from
|
|
# every consumer.
|
|
#
|
|
# `main` is what separates the two, and it is the only field that does:
|
|
# `type` alone cannot, because the core fixtures that legitimately generate
|
|
# nothing are core too. A provider ships a plugin, so it names one.
|
|
else if (config.type or "core") == "core" && (config.main or null) != null then
|
|
throw ("logos-module-builder: module '${config.name}' is a core module "
|
|
+ "shipping a plugin (main: ${config.main}) but declares no "
|
|
+ "`interface`, so NO glue would be generated and every call into "
|
|
+ "it would fail at runtime rather than at build time. Use "
|
|
+ "interface \"universal\" (write a plain src/${config.name}_impl.h "
|
|
+ "and the contract is derived from it) or \"cdylib\" (bring your "
|
|
+ "own C ABI plus codegen.lidl).")
|
|
else "";
|
|
|
|
# Order: optional ext copy -> optional darwin fixup -> codegen -> user hook
|
|
# Note: mkLogosModule main builds already copy externals in logos-plugin-qt buildPlugin
|
|
# (externalLibCopies). Use copyExternals=true only for contexts without that (e.g. unit tests).
|
|
# Stamp the logos-protocol semver the module is being built against into
|
|
# the metadata.json the plugin embeds (Q_PLUGIN_METADATA). One number
|
|
# governs Logos load/call compatibility (same MAJOR <=> compatible);
|
|
# liblogos reads it pre-load. Runs before cmake/moc so the embedded copy
|
|
# carries the field; modules built by older builders simply lack it and
|
|
# load permissively ("legacy").
|
|
stampProtocolVersion = protocolVersion:
|
|
if protocolVersion == null then "" else ''
|
|
if [ -f ./metadata.json ]; then
|
|
jq '. + {logos_protocol_version: "${protocolVersion}"}' ./metadata.json > ./metadata.json.lp-stamp && mv ./metadata.json.lp-stamp ./metadata.json
|
|
echo "Stamped logos_protocol_version=${protocolVersion} into metadata.json"
|
|
fi
|
|
'';
|
|
|
|
# preCodegen runs AFTER the protocol-version stamp / external copy / darwin fix
|
|
# but BEFORE codegen — used to stage a builder-derived .lidl (rust-first) into
|
|
# the tree where cdylibCodegen will read codegen.lidl.
|
|
compose = { config, externalLibs, userPre, fixDarwin ? false, copyExternals ? false, protocolVersion ? null, preCodegen ? "" }:
|
|
let
|
|
stamp = stampProtocolVersion protocolVersion;
|
|
copy = if copyExternals then copyExternalLibsToLib externalLibs else "";
|
|
codegen = autoCodegen config;
|
|
fix = if fixDarwin then fixupDarwinDylibs else "";
|
|
in
|
|
stamp + copy + fix + preCodegen + codegen + userPre;
|
|
|
|
in {
|
|
inherit defaultImplClassFromName copyExternalLibsToLib fixupDarwinDylibs universalCodegen uiCodegen autoCodegen compose stampProtocolVersion;
|
|
}
|