Files
logos-module-builder/lib/modulePreConfigure.nix
T
Dario LipicarandClaude Opus 4.8 0c7ec19040 Per-module concurrent dispatch: concurrency metadata flag (#133)
* feat: thread --concurrency to the multi-aware generators

parseMetadata exposes top-level `concurrency` (default "single"). mkLogosModule
passes `--concurrency multi` to the Rust generator (thread-safe scaffold) and
modulePreConfigure to the Qt generator (deferring glue). The C++ cdylib needs
nothing — its logos_module_dispatch is already concurrency-safe — so
`--concurrency` is intentionally NOT passed to logos-cpp-generator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: compile metadata nix.cmake.extra_sources into the plugin

extra_sources was parsed (parseMetadata.nix) but never consumed — the author's
own .cpp files were silently dropped from the build. logos_module() compiles
only the generated glue + SDK sources, so a module that splits a
declarations-only header (the form the cpp-generator scans, to keep inline std
calls out of the header) from its impl body had the body's symbols left
UNDEFINED. The plugin still links (-undefined dynamic_lookup) and loads, then
SIGSEGVs the first time the dispatch calls one of those methods — the call goes
through a symbol stub that resolves to null (bl -> 0x0). This is what crashed
the universal-cdylib concurrent-dispatch driver on its first modules() call
(FanoutDriverModuleImpl::fanOut / ::peak were `U` in the dylib).

Thread extra_sources through like go_static_lib_names: the Nix build emits
-DLOGOS_MODULE_EXTRA_SOURCES=a;b;c (buildCppPlugin.nix) and logos_module()
appends them to PLUGIN_SOURCES (LogosModule.cmake), resolving relative paths
against the module's CMakeLists dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump plugin backend to the extra_sources consumer (plugin-qt bea4ff8)

Pairs with buildCppPlugin.nix emitting -DLOGOS_MODULE_EXTRA_SOURCES: the
backend's LogosModule.cmake (logos-plugin-qt, shared by core+ui) now appends
those sources to PLUGIN_SOURCES, so metadata.json nix.cmake.extra_sources is
actually compiled — the split declarations-header + impl-body pattern works
from metadata alone, no hand-written logos_module(SOURCES ...).

(Temporary rev pin to the chore/extra-sources-autowire branch for validation;
re-point to master once it lands.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Revert "chore: bump plugin backend to the extra_sources consumer (plugin-qt bea4ff8)"

This reverts commit f3a01999f6.

* Revert "fix: compile metadata nix.cmake.extra_sources into the plugin"

This reverts commit 6469bc044e.

* chore: bump protocol/cpp-sdk/qt-sdk/rust-sdk to merged masters

  logos-protocol  9de4165 → 4ea32a3  (protocol#5)
  logos-cpp-sdk   f032cf2 → aea29d3  (cpp-sdk#93)
  logos-qt-sdk    8123692 → 63795e8  (qt-sdk#5)
  logos-rust-sdk  340f42d → eff1ad5  (rust-sdk#20)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:57:36 -03:00

223 lines
9.9 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
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-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
'';
providerCodegen = config:
let
cg = config.codegen or {};
headerPath =
if cg ? provider_header then
cg.provider_header
else if cg ? impl_header && lib.hasInfix "/" cg.impl_header then
cg.impl_header
else
"src/${config.name}_impl.h";
in
''
echo "logos-module-builder: generating provider dispatch (${config.name})..."
logos-cpp-generator --provider-header "$(pwd)/${headerPath}" \
--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-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 qt generator emits
# only the *Interface.h and the *Plugin glue that wires the (Qt-typed)
# LogosModules aggregate into the backend on initLogos.
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-qt-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 == "provider" then providerCodegen config
else if config.interface == "cdylib" then cdylibCodegen config
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 providerCodegen uiCodegen autoCodegen compose stampProtocolVersion;
}