# 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(). 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/.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 # 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; }