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>
1197 lines
63 KiB
Nix
1197 lines
63 KiB
Nix
# Core module builder function
|
|
# This is the main entry point for building Logos modules.
|
|
# Plugin compilation and header generation are delegated to a backend selected
|
|
# by metadata.json "type": core modules use coreBackend, UI modules use uiBackend.
|
|
{ nixpkgs, lib, common, parseMetadata, builderRoot, uiBackend, coreBackend, logos-cpp-sdk, logos-protocol ? null, logos-qt-sdk ? null, logos-plugin-qt ? null, logos-view-module, logos-module, logos-test-framework, logos-rust-sdk ? null, nix-bundle-lgx, nix-bundle-logos-module-install, logos-standalone-app, rust-overlay ? null }:
|
|
|
|
{
|
|
# Required: Path to the module source
|
|
src,
|
|
|
|
# Required: Path to the metadata.json configuration file
|
|
configFile,
|
|
|
|
# Optional: all flake inputs — dependencies in metadata.json are resolved automatically
|
|
flakeInputs ? {},
|
|
|
|
# Optional: Additional flake inputs for external libraries
|
|
externalLibInputs ? {},
|
|
|
|
# Optional: Extra build inputs to add
|
|
extraBuildInputs ? [],
|
|
|
|
# Optional: Extra native build inputs to add
|
|
extraNativeBuildInputs ? [],
|
|
|
|
# Optional: Extra inputs/env for the Rust crate compile (cdylib modules).
|
|
# Programmatic escape hatch complementing metadata `nix.rust` — for arbitrary
|
|
# derivations or store-path env that can't be named by a nixpkgs attr path.
|
|
# Merged on top of the metadata-declared inputs (rustEnv wins on key conflict).
|
|
rustExtraNativeBuildInputs ? [],
|
|
rustExtraBuildInputs ? [],
|
|
rustEnv ? {},
|
|
|
|
# Optional: Override any config values
|
|
configOverrides ? {},
|
|
|
|
# Optional: Custom preConfigure hook
|
|
preConfigure ? "",
|
|
|
|
# Optional: Custom postInstall hook
|
|
postInstall ? "",
|
|
|
|
# Optional: override the logos-standalone-app used for `nix run`.
|
|
# By default, UI modules (type = "ui") automatically get apps.default wired up
|
|
# using the standalone app bundled with logos-module-builder.
|
|
logosStandalone ? null,
|
|
|
|
# Optional: Unit test configuration. When provided, a checks.<system>.unit-tests
|
|
# output is automatically generated using logos-test-framework.
|
|
# tests = {
|
|
# dir = ./tests; # Required: directory containing test sources + CMakeLists.txt
|
|
# mockCLibs = []; # Optional: C libraries to mock at link time
|
|
# preConfigure = ""; # Optional: custom preConfigure hook
|
|
# extraBuildInputs = [];
|
|
# extraCmakeFlags = [];
|
|
# };
|
|
tests ? null,
|
|
}:
|
|
|
|
let
|
|
metadataJson = builtins.readFile configFile;
|
|
|
|
# ── Two configs, and why ──────────────────────────────────────────────────
|
|
#
|
|
# `config` is parsed with NO platform. It is the answer for the handful of
|
|
# fields no `platforms` overlay may vary — name, version, type, interface —
|
|
# and those are exactly the fields read here, above forAllSystems, where no
|
|
# target exists yet: `selectedBackend` below, and the system-agnostic `config`
|
|
# flake output that collectAllModuleDeps reads from a FOREIGN flake.
|
|
#
|
|
# It is NOT a fallback. A field some overlay declares comes back as a throw
|
|
# (resolvePlatforms.poisonField), so reading one of those up here is a loud
|
|
# error rather than the base value — which is the whole point: a superset
|
|
# nobody meant to use on its own is how the .so/.dylib/.dll spelling lists
|
|
# drifted in the first place.
|
|
#
|
|
# `configFor system` is the resolved answer, and every per-system closure
|
|
# below rebinds `config` to it as its first `let` binding. That rebinding is
|
|
# deliberate rather than a rename: it keeps ~40 existing `config.*` reads
|
|
# correct by construction, and there is no reading inside a per-system
|
|
# closure that should see the unresolved tree.
|
|
rawConfig = parseMetadata.parseModuleConfig { json = metadataJson; platform = null; };
|
|
config = common.recursiveMerge [ rawConfig configOverrides ];
|
|
|
|
configFor = system: common.recursiveMerge [
|
|
(parseMetadata.parseModuleConfig {
|
|
json = metadataJson;
|
|
platform = parseMetadata.platformForSystem system;
|
|
})
|
|
configOverrides
|
|
];
|
|
|
|
# Select backend based on module type: core modules are swappable, UI stays Qt
|
|
selectedBackend =
|
|
if config.type == "core" then coreBackend
|
|
else uiBackend;
|
|
|
|
# Import sub-builders (backend-agnostic)
|
|
mkExternalLib = import ./mkExternalLib.nix { inherit lib common; };
|
|
mkStandaloneApp = import ./mkStandaloneApp.nix;
|
|
modulePreConfigure = import ./modulePreConfigure.nix { inherit lib; };
|
|
|
|
# cmake/LogosModule.cmake lives HERE and nowhere else — logos-plugin-qt used
|
|
# to ship a second copy, and this was a `pathExists` probe whose miss handed
|
|
# the build to that copy instead. There is nothing to fall back to now, so a
|
|
# miss throws rather than silently configuring against another file.
|
|
builderCmakeRoot =
|
|
if builtins.pathExists (builderRoot + "/cmake/LogosModule.cmake")
|
|
then "${builderRoot}"
|
|
else throw ("logos-module-builder: cmake/LogosModule.cmake is missing from "
|
|
+ "${toString builderRoot}. It is the only copy; no backend ships one.");
|
|
|
|
# Helper to get a package from nixpkgs by name
|
|
getPkg = pkgs: name:
|
|
let evaluatedName = builtins.seq name name;
|
|
in if builtins.isString evaluatedName
|
|
then lib.getAttrFromPath (lib.splitString "." evaluatedName) pkgs
|
|
else builtins.throw "getPkg expected string but got ${builtins.typeOf evaluatedName}";
|
|
|
|
forAllSystems = f: lib.genAttrs common.systems (system: f system);
|
|
|
|
# Package outputs
|
|
packages = forAllSystems (system:
|
|
let
|
|
pkgs = common.mkPkgs system;
|
|
config = configFor system;
|
|
|
|
# Rust target triple when `system` is a cross pseudo-system; null natively.
|
|
# Every cross branch below keys off this being non-null, so a native build
|
|
# takes exactly the code path it did before.
|
|
rustCrossTarget =
|
|
if system == "x86_64-windows" then "x86_64-pc-windows-gnu" else null;
|
|
|
|
# Rust toolchain for the crate compile. Default = the pinned nixpkgs rustc,
|
|
# so non-Rust modules and Rust modules without a `nix.rust.toolchain` are
|
|
# unchanged. When a module sets `nix.rust.toolchain` (e.g. "1.96.0") and the
|
|
# builder has a rust-overlay input, use a rust-overlay stable toolchain at
|
|
# that version — for crates whose deps need a newer rustc than nixpkgs ships
|
|
# (the railgun engine's alloy 1.8 / ruint need >= 1.91).
|
|
rustPlatform =
|
|
if config.nix_rust.toolchain != null && rust-overlay != null
|
|
then
|
|
let
|
|
# The toolchain must RUN on the builder and merely TARGET `system`.
|
|
# Asking the CROSS set for rust-bin evaluates
|
|
# `targetPackages.threads.package` (nixpkgs all-packages.nix) --
|
|
# an attribute only the MinGW branch touches and that the cross set
|
|
# does not define -- and mkPkgsWith refuses overlays for
|
|
# x86_64-windows for the same "that is not the set you asked for"
|
|
# reason. Taking it from the BUILD system sidesteps both, and is
|
|
# what a cross toolchain should be regardless.
|
|
# buildSystemFor is the identity on every native system, so this is
|
|
# a no-op there.
|
|
bpkgs = common.mkPkgsWith [ (import rust-overlay) ] (common.buildSystemFor system);
|
|
base = bpkgs.rust-bin.stable.${config.nix_rust.toolchain}.default;
|
|
toolchain =
|
|
if rustCrossTarget == null
|
|
then base
|
|
else base.override { targets = [ rustCrossTarget ]; };
|
|
in bpkgs.makeRustPlatform { cargo = toolchain; rustc = toolchain; }
|
|
else pkgs.rustPlatform;
|
|
|
|
# Cross wiring for the crate compile. The derivation runs in the BUILD
|
|
# platform's stdenv (see rustPlatform above), so nothing sets these for us.
|
|
rustCrossEnv =
|
|
if rustCrossTarget == null then { }
|
|
else
|
|
let
|
|
cc = pkgs.stdenv.cc; # `pkgs` is the TARGET set: the mingw wrapper
|
|
u = builtins.replaceStrings [ "-" ] [ "_" ] rustCrossTarget;
|
|
U = lib.toUpper u;
|
|
in {
|
|
CARGO_BUILD_TARGET = rustCrossTarget;
|
|
"CARGO_TARGET_${U}_LINKER" = "${cc}/bin/${cc.targetPrefix}cc";
|
|
# windows-gnu std links `-l:libpthread.a`, but nixpkgs builds
|
|
# mingw-w64 against mcfgthread, which ships no pthreads at all.
|
|
"CARGO_TARGET_${U}_RUSTFLAGS" = "-L native=${pkgs.windows.pthreads}/lib";
|
|
# cc-rs keys its toolchain off CC_<triple>/CXX_/AR_ with dashes
|
|
# replaced by underscores. Without these a build script compiles its
|
|
# bundled C for the BUILDER and the link then fails on undefined
|
|
# symbols -- silently, because the archive is still produced.
|
|
"CC_${u}" = "${cc}/bin/${cc.targetPrefix}cc";
|
|
"CXX_${u}" = "${cc}/bin/${cc.targetPrefix}c++";
|
|
"AR_${u}" = "${cc.bintools}/bin/${cc.targetPrefix}ar";
|
|
# The header half of the same pthreads story as RUSTFLAGS above.
|
|
# mingw-w64 DOES ship <sched.h>, <pthread.h> and <semaphore.h> --
|
|
# but in the winpthreads package, which is not on the default
|
|
# sysroot include path because nixpkgs builds mingw against
|
|
# mcfgthread. A crate's vendored C that reaches for them therefore
|
|
# fails with a bare "fatal error: sched.h: No such file or
|
|
# directory" that reads like the platform is unsupported when it is
|
|
# only unwired. aws-lc-sys hits exactly this, compiling
|
|
# jitterentropy for the Windows target.
|
|
#
|
|
# cc-rs appends CFLAGS_<triple>/CXXFLAGS_<triple> to the compiler
|
|
# invocations it drives, so this reaches build-script C without
|
|
# touching the Rust compile.
|
|
"CFLAGS_${u}" = "-I${pkgs.windows.pthreads}/include";
|
|
"CXXFLAGS_${u}" = "-I${pkgs.windows.pthreads}/include";
|
|
};
|
|
|
|
# ── Concrete dependency classification ─────────────────────────────────
|
|
# A dependency's typed `modules().<dep>` wrapper is generated from its
|
|
# published LIDL contract (`packages.<sys>.lidl`) WITHOUT building the
|
|
# dep's plugin. Deps that don't expose a `lidl` output yet take the
|
|
# TRANSITIONAL header-copy fallback (`legacyHeaderDepNames`), which DOES
|
|
# build them — identical to today's behavior.
|
|
# Returns the dep's published LIDL output, or null if the input isn't a
|
|
# flake exposing packages.<system>.lidl (e.g. a raw-derivation dep, or a
|
|
# module built by a builder that predates this feature) — those fall
|
|
# through to the TRANSITIONAL header-copy path. Guard every level so a
|
|
# non-flake input never throws.
|
|
depLidlOf = name:
|
|
let i = flakeInputs.${name} or null;
|
|
in if i != null && i ? packages && i.packages ? ${system}
|
|
then (i.packages.${system}.lidl or null)
|
|
else null;
|
|
depIsLidl = name: (config.dependency_overrides ? ${name}) || (depLidlOf name != null);
|
|
|
|
# LIDL-based deps → `--dep <name>=<lidl>` for the generator. An override
|
|
# forces a specific definition (.lidl, or .h + impl_class); otherwise we
|
|
# use the dep's published `lidl` output.
|
|
staticDeps = map (name:
|
|
let ov = config.dependency_overrides.${name} or null;
|
|
in if ov != null then {
|
|
inherit name;
|
|
impl_class = ov.impl_class;
|
|
path = if ov.input != null
|
|
then (if flakeInputs ? ${ov.input}
|
|
then "${flakeInputs.${ov.input}}/${ov.file}"
|
|
else throw "dependency_overrides.${name}: flake input '${ov.input}' was not passed to mkLogosModule.")
|
|
else "${src}/${ov.file}";
|
|
} else {
|
|
inherit name;
|
|
impl_class = null;
|
|
path = "${depLidlOf name}/${name}.lidl";
|
|
}
|
|
) (lib.filter depIsLidl config.dependencies);
|
|
|
|
# TRANSITIONAL: header-copy fallback for deps that predate the `lidl`
|
|
# output. These deps ARE built (their headers come from introspecting the
|
|
# compiled plugin). Remove this block — and the `moduleDepIncludes` use in
|
|
# the plugin backends — once every module exposes packages.<sys>.lidl.
|
|
legacyHeaderDepNames = lib.filter (name: !(depIsLidl name)) config.dependencies;
|
|
|
|
# Resolve the fallback deps from inputs. Each entry is exposed
|
|
# as a struct so the plugin builder can pick BOTH the dep's
|
|
# plugin .dylib AND the right header variant for its own
|
|
# --api-style without re-running the codegen at consume time.
|
|
# Shared with buildCppPlugin (view modules) — see common.nix.
|
|
resolvedModuleDeps = common.resolveLegacyHeaderDeps {
|
|
inherit system flakeInputs;
|
|
depNames = legacyHeaderDepNames;
|
|
};
|
|
|
|
# Resolve interface dependencies (method/event contracts) to concrete
|
|
# definition-file paths. A LOCAL interface lives in this repo's `src`;
|
|
# a REMOTE one comes from a flake input named by `input` — mirroring
|
|
# how `dependencies` resolve to flake inputs. We resolve the path here
|
|
# so the generator never touches flake inputs: it just receives
|
|
# `--interface <name>=<path>[=<impl_class>]`. (System-independent, but
|
|
# kept in this scope alongside resolvedModuleDeps for locality.)
|
|
resolvedInterfaceDeps = map (e: {
|
|
inherit (e) name impl_class;
|
|
path = if e.input != null
|
|
then (if flakeInputs ? ${e.input}
|
|
then "${flakeInputs.${e.input}}/${e.file}"
|
|
else throw "interface_dependencies: interface '${e.name}' references flake input '${e.input}', but no such input was passed to mkLogosModule (declare it in flake.nix and pass it via flakeInputs).")
|
|
else "${src}/${e.file}";
|
|
}) config.interface_dependencies;
|
|
|
|
# Resolve a single externalLibInputs entry for a given variant.
|
|
# Supports both simple (bare flake input) and structured ({ input, packages }) formats.
|
|
resolveExtInput = variant: name: value:
|
|
if builtins.isAttrs value && value ? input then
|
|
let
|
|
flakeInput = value.input;
|
|
packages = value.packages or {};
|
|
pkgName = packages.${variant} or packages.default or "default";
|
|
in
|
|
if flakeInput ? packages.${system}.${pkgName}
|
|
then flakeInput.packages.${system}.${pkgName}
|
|
else builtins.throw ''
|
|
External lib "${name}": flake input does not provide packages.${system}.${pkgName}.
|
|
Check the "externalLibInputs" structured entry and ensure the flake input exposes the expected package.
|
|
''
|
|
else
|
|
if value ? packages.${system}.default then value.packages.${system}.default else value;
|
|
|
|
# Whether any external lib input declares per-variant packages
|
|
hasVariants = lib.any (v: builtins.isAttrs v && v ? input && v ? packages)
|
|
(lib.attrValues externalLibInputs);
|
|
|
|
buildPkgs = map (getPkg pkgs) (lib.filter builtins.isString config.nix_packages.build);
|
|
runtimePkgs = map (getPkg pkgs) (lib.filter builtins.isString config.nix_packages.runtime);
|
|
|
|
# Rust crate compile inputs (metadata nix.rust). build -> nativeBuildInputs
|
|
# (host tools), runtime -> buildInputs (link libs). Resolved with the same
|
|
# dotted-path getPkg as buildPkgs/runtimePkgs. Fed only to rustStaticLib,
|
|
# not the C++ plugin link.
|
|
# buildPackages, not pkgs: these are TOOLS that run on the builder
|
|
# (pkg-config, perl, protobuf, cmake). Under cross, resolving them from
|
|
# the target set would try to build each one FOR Windows. Identity on
|
|
# every native system, so no native derivation changes.
|
|
rustNativeBuildPkgs = map (getPkg pkgs.buildPackages) (lib.filter builtins.isString config.nix_rust.packages.build);
|
|
rustBuildPkgs = map (getPkg pkgs) (lib.filter builtins.isString config.nix_rust.packages.runtime);
|
|
|
|
# Pre-resolve default variant external libs (always needed, avoids
|
|
# duplicate evaluation when hasVariants triggers a second buildVariant).
|
|
defaultResolvedExternalLibs = lib.mapAttrs (resolveExtInput "default") externalLibInputs;
|
|
defaultExternalLibs = mkExternalLib.buildExternalLibs {
|
|
inherit pkgs config src;
|
|
externalInputs = defaultResolvedExternalLibs;
|
|
};
|
|
|
|
# metadata `include`: runtime files a module needs BESIDE its plugin but
|
|
# never links against -- in practice, dlopen'd libraries.
|
|
#
|
|
# Nothing else can stage these. The Windows DLL walk
|
|
# (logos-plugin-qt postFixup -> linkDLLsInfolder) is IMPORT-TABLE driven,
|
|
# so a library reached only through dlopen appears in no table and is
|
|
# invisible to it; on Unix there is equally no DT_NEEDED entry to follow.
|
|
# delivery_module hit exactly this with libpq: declared, needed at
|
|
# runtime, and silently absent from the module output.
|
|
#
|
|
# Sources are the module's own runtime nix packages and its resolved
|
|
# external libs; both `lib/` and `bin/` are searched, because a Windows
|
|
# shared library's runtime half lives in bin/ by convention.
|
|
#
|
|
# A name that matches nothing is NORMAL, not an error: the list is a
|
|
# deliberate cross-platform superset (modules name the .so, .dylib and
|
|
# .dll spellings side by side), so at most one spelling can ever match.
|
|
#
|
|
# `config.include` here is the PLATFORM-RESOLVED list (this closure
|
|
# rebinds `config` to `configFor system`), so a module can now name one
|
|
# spelling per target with a `platforms` overlay instead of a superset.
|
|
# The tolerance above stays for the modules that still write the superset
|
|
# — and because the superset is unvalidated, which is how
|
|
# logos-package-downloader-module ended up naming .so and .dylib but not
|
|
# .dll. An overlay whose selector is misspelled throws at parse time
|
|
# instead.
|
|
#
|
|
# Runs BEFORE the module's own postInstall, so author hooks can react to
|
|
# what was staged, and before the Windows postFixup, so linkDLLsInfolder
|
|
# then also walks the staged library's OWN imports (libpq pulls in
|
|
# libssl/libcrypto that way).
|
|
stageIncludedRuntimeFiles =
|
|
let
|
|
sources = runtimePkgs ++ lib.attrValues defaultResolvedExternalLibs;
|
|
in
|
|
lib.optionalString (config.include != [ ] && sources != [ ]) ''
|
|
echo "Staging declared runtime files (metadata 'include')..."
|
|
mkdir -p $out/lib
|
|
for _inc_name in ${lib.escapeShellArgs config.include}; do
|
|
for _inc_root in ${lib.escapeShellArgs (map toString sources)}; do
|
|
for _inc_sub in lib bin; do
|
|
if [ -e "$_inc_root/$_inc_sub/$_inc_name" ]; then
|
|
cp -Lf "$_inc_root/$_inc_sub/$_inc_name" "$out/lib/" 2>/dev/null \
|
|
&& echo " staged $_inc_name" && break 2
|
|
fi
|
|
done
|
|
done
|
|
done
|
|
'';
|
|
|
|
# Resolve SDK deps for this system — injected into the backend
|
|
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
|
# Build-platform half of the SDK. logos-cpp-generator is invoked by BARE
|
|
# NAME from a build phase (logos-plugin-qt/lib/buildPlugin.nix:145), so it
|
|
# must run on the builder. Under cross, packages.x86_64-windows.default
|
|
# carries no runnable generator at all -- logos-cpp-sdk/nix/bin.nix:39
|
|
# silently skips the mingw .exe -- hence "command not found".
|
|
#
|
|
# `logosSdk` deliberately stays TARGET-typed: it is ALSO the header and
|
|
# CMake-package root passed to LOGOS_CPP_SDK_ROOT, and those must keep
|
|
# coming from the Windows set. Splitting the two roles is the whole point;
|
|
# pointing the headers at the build system would produce a build that
|
|
# SUCCEEDS while linking the wrong architecture.
|
|
#
|
|
# buildSystemFor is the identity on every native system, so this is a
|
|
# no-op off the Windows target.
|
|
logosSdkBuild = logos-cpp-sdk.packages.${common.buildSystemFor system}.default;
|
|
logosQtSdk = logos-qt-sdk.packages.${system}.default;
|
|
# The Qt HOST RUNTIME (LogosAPI, LogosAPIProvider, LogosProviderBase, the
|
|
# legacy PluginInterface) a plugin links. It moved out of logos-qt-sdk
|
|
# into logos-plugin-qt and ships as `logos-qt-host`; logos-qt-sdk still
|
|
# forwards it, so this is the repoint, not a new dependency. TARGET-typed
|
|
# like logosQtSdk — it is a library that gets linked into the plugin.
|
|
# logos-qt-sdk stays for what the host runtime never carried: the
|
|
# Qt-typed logos_qt_lp_bridge.h / logos_qt_wire.h / logos_ui_plugin_context.h
|
|
# and the logos-qt-generator that emits #includes of them.
|
|
logosQtHost = logos-plugin-qt.packages.${system}.logos-qt-host;
|
|
# The Qt glue generator (universal/cdylib/ui backends) — Qt code is
|
|
# the Qt layer's product; logos-cpp-generator keeps Qt-free outputs.
|
|
logosQtGenerator = logos-qt-sdk.packages.${common.buildSystemFor system}.logos-qt-generator;
|
|
# The cdylib Qt-plugin glue generator lives in logos-plugin-qt (the Qt
|
|
# plugin BACKEND owns the glue; the SDK does not). logos-qt-sdk still
|
|
# ships an older copy of the SAME emitter, and calling that one is not a
|
|
# compile error — it silently emits STALE glue. That is how a
|
|
# host-services grant went undelivered while every build stayed green.
|
|
logosQtHostGenerator =
|
|
logos-plugin-qt.packages.${common.buildSystemFor system}.logos-qt-host-generator;
|
|
# The four LogosView*.in templates logos_module(REP_FILE ...) instantiates.
|
|
# They live in logos-view-module (the ui_qml authoring flavour), NOT in
|
|
# the plugin backend any more, and cmake/LogosModule.cmake here refuses to
|
|
# guess — it hard-errors unless handed LOGOS_VIEW_TEMPLATE_DIR.
|
|
#
|
|
# buildSystemFor, not plain ${system}: these are text files with no
|
|
# platform dimension, and logos-view-module publishes only the four
|
|
# NATIVE systems, so `packages.x86_64-windows` would EVAL-fail on the
|
|
# Windows leg — a failure that is invisible until someone crosses.
|
|
viewTemplates =
|
|
logos-view-module.packages.${common.buildSystemFor system}.logos-view-templates;
|
|
# The VIEW plugin glue generator (`--backend ui`). It lives in
|
|
# logos-view-module, beside the LogosView*.in templates the glue it emits
|
|
# is compiled against and beside logos_ui_plugin_context.h, which that
|
|
# glue calls into -- the three are one authoring surface and used to be
|
|
# split across two repos. logos-qt-sdk shipped the same emitter and
|
|
# rotted: it gained the teardown hook, the copy here did not, and nothing
|
|
# detected it because a missing hook is silent at every layer.
|
|
#
|
|
# buildSystemFor: a code generator RUNS on the build machine, and
|
|
# logos-view-module publishes only the four NATIVE systems, so plain
|
|
# ${system} would EVAL-fail on the Windows leg.
|
|
logosViewGenerator =
|
|
logos-view-module.packages.${common.buildSystemFor system}.logos-view-generator;
|
|
# logos_ui_plugin_context.h -- the context a view's *Backend derives, and
|
|
# the header the emitted glue calls maybeUiPluginAboutToUnload() in.
|
|
#
|
|
# It comes from logos-view-module, the SAME pin as the generator above,
|
|
# and that is the whole point. The emitter and this header are one
|
|
# MATCHED PAIR: the emitter writes a call, the header declares what it
|
|
# calls. While both lived in logos-qt-sdk they moved together under one
|
|
# pin and could not disagree. Sourcing the generator from one repo and
|
|
# this header from another would make every ui_qml build depend on two
|
|
# pins agreeing, with nothing enforcing it -- and the failure is a
|
|
# compile error deep inside GENERATED code, far from the pin that caused
|
|
# it. One pin, one pair.
|
|
logosViewInclude =
|
|
logos-view-module.packages.${common.buildSystemFor system}.include;
|
|
logosProtocolPkg = logos-protocol.packages.${system}.default;
|
|
logosModule = logos-module.packages.${system}.default;
|
|
|
|
# The logos-protocol semver — parsed from the protocol header the
|
|
# whole stack links. Stamped into every module's embedded metadata
|
|
# (see modulePreConfigure.stampProtocolVersion). null (no stamp) only
|
|
# if the input is somehow absent — modules then load as "legacy".
|
|
protocolVersion =
|
|
if logos-protocol == null then null
|
|
else
|
|
let
|
|
header = builtins.readFile "${logos-protocol}/cpp/logos_protocol.h";
|
|
parts = builtins.split "LOGOS_PROTOCOL_VERSION_STRING \"([^\"]*)\"" header;
|
|
in if builtins.length parts < 2 then null
|
|
else builtins.head (builtins.elemAt parts 1);
|
|
|
|
# ── Rust cdylib authoring (codegen.rust) ───────────────────────────────
|
|
# A Rust module's module-impl C ABI scaffold (logos_module_* exports +
|
|
# typed trait + RustModuleContext + dep clients) is generated from the SAME
|
|
# .lidl contract that drives the Qt glue, and the crate is compiled to a
|
|
# staticlib — both done HERE by the builder, exactly as it runs the C++
|
|
# generator. The author writes no build.rs and the module's flake stays
|
|
# trivial (no buildRustPackage / preConfigure staging).
|
|
#
|
|
# logos-lidl-gen AND the SDK source the crate links both come from this
|
|
# builder's own logos-rust-sdk input — so a Rust module's flake.nix is
|
|
# identical to a C++ one (just logos-module-builder), and the generator and
|
|
# the runtime SDK are the SAME pinned rev (no skew). logos-rust-sdk depends
|
|
# back on this builder for its tests, so its module-builder input is cut
|
|
# with `follows` in flake.nix to break the cycle (see there).
|
|
isRustModule = (config.codegen or {}) ? rust;
|
|
rustCfg = (config.codegen or {}).rust or {};
|
|
rustCrateDir =
|
|
"${src}/${rustCfg.crate or (throw "codegen.rust must set 'crate' (the crate directory, e.g. \"rust-lib\") in ${config.name}")}";
|
|
# The staticlib basename (produces lib<name>.a) — read from the crate's
|
|
# Cargo.toml ([lib].name, else [package].name with - -> _) so the author
|
|
# needn't repeat it. codegen.rust.staticlib still overrides if set.
|
|
rustCargoToml =
|
|
if !isRustModule then {}
|
|
else builtins.fromTOML (builtins.readFile "${rustCrateDir}/Cargo.toml");
|
|
rustStaticName =
|
|
rustCfg.staticlib
|
|
or (rustCargoToml.lib.name
|
|
or (lib.replaceStrings ["-"] ["_"] rustCargoToml.package.name));
|
|
# Rust-FIRST authoring: when codegen.rust names the contract `trait`, that
|
|
# trait is declared in the crate and the .lidl is DERIVED from it at build
|
|
# time (logos-lidl-gen --from-rust over the crate source) — exactly as a
|
|
# universal C++ module derives its .lidl from the impl header. The .rs file
|
|
# is the single source of truth: no committed .lidl, no manual derive step.
|
|
# The scaffold is then generated with --no-trait (the trait is the
|
|
# author's). Without `trait`, the module is contract-first: codegen.lidl is
|
|
# a committed file and the trait is generated.
|
|
rustTrait = rustCfg.trait or null;
|
|
rustDeriveMode = rustTrait != null;
|
|
# The .rs file holding the trait (+ optional <Trait>Events companion),
|
|
# relative to the crate dir.
|
|
rustSource = rustCfg.source or "src/lib.rs";
|
|
rustSdk =
|
|
if !isRustModule then null
|
|
else if logos-rust-sdk == null
|
|
then throw "codegen.rust module '${config.name}' requires logos-module-builder to be built with a logos-rust-sdk input (it provides the lidl-gen generator + the SDK source). Update the builder."
|
|
else logos-rust-sdk;
|
|
# lidl-gen is a build-time TOOL: it runs on the builder to emit the Rust
|
|
# scaffold. Resolving it from the TARGET set asks logos-rust-sdk for an
|
|
# x86_64-windows attribute it does not publish -- and which would be an
|
|
# unrunnable PE if it did. buildSystemFor is the identity natively.
|
|
rustGen = if !isRustModule then null
|
|
else rustSdk.packages.${common.buildSystemFor system}.lidl-gen;
|
|
|
|
# The dep contracts that feed the Rust generator: the same resolved
|
|
# concrete + interface deps the C++ generator gets. Concrete deps →
|
|
# `modules().<dep>`; interface deps → a bound client (`<Iface>Client::bind`).
|
|
# Both arrive as `--dep name=<lidl>` (the Rust CLI has no separate
|
|
# interface flag — every generated client carries new() AND bind()).
|
|
rustDepFlags = lib.concatStringsSep " " (
|
|
(map (d: "--dep ${d.name}=${d.path}") staticDeps)
|
|
++ (map (e: "--dep ${e.name}=${e.path}") resolvedInterfaceDeps)
|
|
);
|
|
|
|
# The contract .lidl, derived from the crate's trait in rust-first mode.
|
|
# Reused by the scaffold gen, the Qt glue (staged into the build below), and
|
|
# the published `packages.<sys>.lidl`.
|
|
derivedLidl =
|
|
if !rustDeriveMode then null
|
|
else pkgs.runCommand "logos-${config.name}-derived-lidl" {
|
|
nativeBuildInputs = [ rustGen ];
|
|
} ''
|
|
mkdir -p $out
|
|
logos-lidl-gen --from-rust "${rustCrateDir}/${rustSource}" \
|
|
--trait ${rustTrait} --module-name ${config.name} --module-version ${config.version} \
|
|
-o "$out/${config.name}.lidl"
|
|
'';
|
|
|
|
# The .lidl the generators consume: the derived one (rust-first) or the
|
|
# committed codegen.lidl (contract-first).
|
|
rustLidlPath =
|
|
if rustDeriveMode then "${derivedLidl}/${config.name}.lidl"
|
|
else "${src}/${config.codegen.lidl}";
|
|
|
|
# A Rust module's EXPORT SET is decided by this string: lidl-gen gates
|
|
# logos_module_grant_host_services on >= 0.3 and the teardown pair on
|
|
# >= 0.5. So it cannot be optional here the way it is for metadata
|
|
# stamping below, where null legitimately means "pre-protocol, load as
|
|
# legacy".
|
|
#
|
|
# It used to be passed as
|
|
# ${lib.optionalString (protocolVersion != null) "--protocol-version ..."}
|
|
# which does not make a bad version WRONG — it makes the flag VANISH.
|
|
# lidl-gen then falls back to "0.1.0", emits the seven founding exports,
|
|
# and exits 0. Every Rust module in the workspace would quietly regenerate
|
|
# incomplete, link cleanly, and fail at dlopen() on Linux with an
|
|
# undefined symbol — invisible on macOS, and three repos away from here.
|
|
#
|
|
# checks.module-impl-abi-nm DETECTS that by reading the built plugin's
|
|
# symbol table. This is the other half: refuse at the point of the
|
|
# mistake, so it never reaches a build. The two causes need different
|
|
# messages because they are different bugs.
|
|
rustProtocolVersion =
|
|
if protocolVersion != null then protocolVersion
|
|
else if logos-protocol == null then
|
|
throw ("logos-module-builder: module '" + config.name + "' is a Rust "
|
|
+ "cdylib (codegen.rust), but this builder has no logos-protocol "
|
|
+ "input, so the module-impl C ABI version it must generate against "
|
|
+ "is unknown. Generating anyway would emit the pre-0.3 export set "
|
|
+ "and produce a module that fails to dlopen.")
|
|
else
|
|
throw ("logos-module-builder: could not read "
|
|
+ "LOGOS_PROTOCOL_VERSION_STRING from ${logos-protocol}/cpp/"
|
|
+ "logos_protocol.h, needed to generate the Rust cdylib scaffold "
|
|
+ "for '" + config.name + "'. The header moved or changed shape — "
|
|
+ "fix the parse above; do NOT let it fall back, because the "
|
|
+ "fallback silently emits an incomplete module-impl C ABI.");
|
|
|
|
rustScaffold =
|
|
if !isRustModule then null
|
|
else pkgs.runCommand "logos-${config.name}-rust-scaffold" {
|
|
nativeBuildInputs = [ rustGen ];
|
|
} ''
|
|
mkdir -p $out
|
|
logos-lidl-gen "${rustLidlPath}" --provider ${lib.optionalString rustDeriveMode "--no-trait"} \
|
|
${lib.optionalString ((config.concurrency or "single") == "multi") "--concurrency multi"} ${rustDepFlags} \
|
|
--protocol-version ${rustProtocolVersion} \
|
|
-o "$out/provider_gen.rs"
|
|
'';
|
|
|
|
# Rust-first only: stage the derived .lidl into generated_code/ BEFORE the
|
|
# Qt-glue codegen runs — that's where cdylibCodegen reads it for a rust-first
|
|
# module (so no codegen.lidl is needed in metadata; the builder owns the
|
|
# path). Empty for contract-first, where the .lidl is committed.
|
|
lidlStaging = lib.optionalString rustDeriveMode ''
|
|
mkdir -p generated_code
|
|
cp ${derivedLidl}/${config.name}.lidl generated_code/${config.name}.lidl
|
|
'';
|
|
|
|
# The crate source laid out for the build: the crate under rust-lib/ (with
|
|
# the generated scaffold injected at generated/provider_gen.rs) and the
|
|
# builder's logos-rust-sdk source alongside it, so the crate's
|
|
# `logos-rust-sdk = { path = "../logos-rust-sdk-src" }` dep resolves against
|
|
# the SAME rev the generator came from. The author crate carries only the
|
|
# trait impl + hook — no build.rs, no OUT_DIR.
|
|
rustCrateSrc =
|
|
if !isRustModule then null
|
|
else pkgs.runCommand "logos-${config.name}-rust-src" {} ''
|
|
mkdir -p $out
|
|
cp -r ${rustCrateDir} $out/rust-lib
|
|
chmod -R u+w $out/rust-lib
|
|
mkdir -p $out/rust-lib/generated
|
|
cp ${rustScaffold}/provider_gen.rs $out/rust-lib/generated/provider_gen.rs
|
|
cp -r ${rustSdk} $out/logos-rust-sdk-src
|
|
'';
|
|
|
|
rustStaticLib =
|
|
if !isRustModule then null
|
|
else rustPlatform.buildRustPackage ({
|
|
pname = rustStaticName;
|
|
version = config.version;
|
|
src = rustCrateSrc;
|
|
sourceRoot = "logos-${config.name}-rust-src/rust-lib";
|
|
cargoLock = {
|
|
lockFile = "${rustCrateDir}/Cargo.lock";
|
|
allowBuiltinFetchGit = true;
|
|
};
|
|
# External system build deps for the crate compile — from metadata
|
|
# `nix.rust` plus the programmatic escape-hatch args. Empty by default,
|
|
# so modules with no native deps build exactly as before.
|
|
nativeBuildInputs = rustNativeBuildPkgs ++ rustExtraNativeBuildInputs
|
|
# The cc-rs / linker wiring above names the cross compiler by store
|
|
# path, but build scripts also expect it on PATH.
|
|
++ lib.optional (rustCrossTarget != null) pkgs.stdenv.cc;
|
|
buildInputs = rustBuildPkgs ++ rustExtraBuildInputs;
|
|
env = config.nix_rust.env // rustEnv // rustCrossEnv;
|
|
doCheck = false;
|
|
}
|
|
# nixpkgs' cargoBuildHook derives `--target` from the stdenv's HOST
|
|
# platform, and this derivation deliberately runs in the BUILD
|
|
# platform's stdenv (see rustPlatform above) so that the toolchain is
|
|
# runnable. Left alone it therefore builds for the BUILDER -- silently,
|
|
# producing a perfectly good Linux archive that then fails to link into
|
|
# a PE. Drive cargo directly for the cross case instead.
|
|
// lib.optionalAttrs (rustCrossTarget != null) {
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
export CARGO_HOME=$TMPDIR/cargo
|
|
cargo build --release --offline --target ${rustCrossTarget}
|
|
runHook postBuild
|
|
'';
|
|
installPhase = ''
|
|
runHook preInstall
|
|
mkdir -p $out/lib
|
|
cp target/${rustCrossTarget}/release/lib${rustStaticName}.a $out/lib/
|
|
runHook postInstall
|
|
'';
|
|
});
|
|
|
|
# Stage the compiled staticlib where LogosModule.cmake's
|
|
# LOGOS_MODULE_RUST_STATIC_LIBS block finds it (the plugin build's lib/).
|
|
rustStaging = lib.optionalString isRustModule ''
|
|
mkdir -p lib
|
|
cp ${rustStaticLib}/lib/lib${rustStaticName}.a lib/
|
|
'';
|
|
|
|
|
|
# Backend arguments for a given external-lib variant ("default" or
|
|
# "portable"). Shared by buildVariant (compiles the plugin) and the
|
|
# generate output (snapshots the post-codegen source tree) so both use the
|
|
# identical preConfigure / deps / env.
|
|
mkPluginArgs = variant:
|
|
let
|
|
externalLibs =
|
|
if variant == "default" then defaultExternalLibs
|
|
else mkExternalLib.buildExternalLibs {
|
|
inherit pkgs config src;
|
|
externalInputs = lib.mapAttrs (resolveExtInput variant) externalLibInputs;
|
|
};
|
|
|
|
# rustStaging (empty for non-Rust modules) drops the compiled Rust
|
|
# staticlib into lib/ before cmake, where the LOGOS_MODULE_RUST_STATIC_LIBS
|
|
# block links it — the builder-driven replacement for the per-flake
|
|
# buildRustPackage + cp the author used to write by hand.
|
|
userPreConfigure =
|
|
rustStaging + (
|
|
if builtins.isFunction preConfigure
|
|
then preConfigure { inherit externalLibs; }
|
|
else preConfigure);
|
|
|
|
preConfigureStr = modulePreConfigure.compose {
|
|
inherit config externalLibs protocolVersion;
|
|
userPre = userPreConfigure;
|
|
# Stage the rust-first derived .lidl before the glue codegen reads it.
|
|
preCodegen = lidlStaging;
|
|
fixDarwin = false;
|
|
# logos-plugin-qt buildPlugin already stages external libs into lib/
|
|
copyExternals = false;
|
|
};
|
|
|
|
goCmakeFlags = lib.optionals (config.go_static_lib_names != []) [
|
|
"-DLOGOS_MODULE_GO_STATIC_LIBS=${lib.concatStringsSep ";" config.go_static_lib_names}"
|
|
];
|
|
|
|
# LOGOS_API_STYLE forwards through to logos-cpp-generator and
|
|
# picks which type surface the generated <Module> client wrappers
|
|
# (and the umbrella LogosModules struct) expose. Mirrors the
|
|
# backend's apiStyle (logos-plugin-qt buildPlugin.nix): core
|
|
# universal modules are header-first cdylibs → Qt-free lp_* wrappers.
|
|
# UI universal backends (type: ui_qml) are NOT modules — they derive a
|
|
# Qt SimpleSource whose .rep slots are Qt-typed, so their
|
|
# LogosUiPluginContext.modules() dep wrappers are Qt-typed too (the
|
|
# generator default — no flag). Every other interface keeps qt.
|
|
# (Only consulted in the source layout; nix builds get apiStyle from
|
|
# the backend's --general-only call.)
|
|
#
|
|
# `config.consumer_api_style` (parseMetadata.nix — the resolved
|
|
# `codegen.consumer_api_style`) is what makes this an override rather
|
|
# than a pure derivation. It only ever REMOVES the flag: a
|
|
# cdylib-packaged module that asks for the Qt consumer surface must
|
|
# not have `lp` forced on it here. It is deliberately NOT allowed to
|
|
# ADD one — the trigger condition below is character-for-character
|
|
# today's, so no module that passes no flag today starts passing one
|
|
# (a `cdylib` module never got this flag even though the nix backend
|
|
# types it `lp`; unifying that would change every cdylib module's
|
|
# derivation for a flag only the legacy source layout reads).
|
|
#
|
|
# There is no `--binding` counterpart here on purpose: this branch of
|
|
# LogosModule.cmake never invokes logos-qt-generator at all, so it
|
|
# cannot emit the origin-bound wrapper SET that the origin-bound
|
|
# umbrella needs. The Qt consumer surface for a cdylib module is a
|
|
# nix-build capability; the source layout keeps the one shape it can
|
|
# actually produce.
|
|
apiStyleCmakeFlags =
|
|
if config.interface == "universal" && (config.type or "core") != "ui_qml"
|
|
&& config.consumer_api_style == "lp"
|
|
then [ "-DLOGOS_API_STYLE=lp" ]
|
|
else [];
|
|
# The backend only knows about Qt + logosModule (interface.h).
|
|
# SDK (generator, lib, headers) is injected via extra* args.
|
|
in ({
|
|
inherit pkgs src config logosModule;
|
|
postInstall = stageIncludedRuntimeFiles + postInstall;
|
|
preConfigure = preConfigureStr;
|
|
moduleDeps = resolvedModuleDeps;
|
|
inherit externalLibs;
|
|
# pkgs.jq is target-typed too and jq runs in preConfigure
|
|
# (modulePreConfigure.nix:203). buildPackages == pkgs natively.
|
|
extraNativeBuildInputs = extraNativeBuildInputs ++ buildPkgs ++ [ logosSdkBuild logosQtGenerator logosQtHostGenerator logosViewGenerator pkgs.buildPackages.jq ];
|
|
extraBuildInputs = extraBuildInputs ++ runtimePkgs ++ [ logosQtSdk logosQtHost logosProtocolPkg ]
|
|
# A Rust staticlib's vendored C may want winpthreads: with <sched.h>
|
|
# reachable, aws-lc-sys compiles aws-lc's thread_pthread.c and the
|
|
# plugin link then needs pthread_rwlock_*, pthread_once, sched_yield.
|
|
# aws-lc assumes the standard mingw environment, where winpthreads is
|
|
# simply present; nixpkgs builds mingw against mcfgthread, so it is a
|
|
# separate package on no default path. As a buildInput its lib/ lands
|
|
# on NIX_LDFLAGS, which is what lets the `pthread` named by
|
|
# LogosModule.cmake's WIN32 branch resolve.
|
|
#
|
|
# Cross Rust modules only, and free for the ones that do not need it:
|
|
# ld pulls archive members on demand, so a module referencing no
|
|
# pthread symbol links exactly as before.
|
|
++ lib.optional (isRustModule && rustCrossTarget != null) pkgs.windows.pthreads;
|
|
# Qt splits each module's TOOLS (repc, moc, qmltyperegistrar) into a
|
|
# SEPARATE package that must run on the BUILD machine. Without these
|
|
# flags find_package(Qt6 COMPONENTS RemoteObjects) fails on a
|
|
# thoroughly misleading message -- it names Qt6RemoteObjects, but the
|
|
# TARGET config is found fine; it is Qt6RemoteObjectsTools that is
|
|
# missing. logos-nix's Windows overlay exposes the flags; the
|
|
# attribute is absent (and so `or []`) on a native build, which is why
|
|
# this needs no isWindows guard.
|
|
extraCmakeFlags = (pkgs.logosQtCrossCmakeFlags or [ ]) ++ [
|
|
"-DLOGOS_CPP_SDK_ROOT=${logosSdk}"
|
|
"-DLOGOS_QT_SDK_ROOT=${logosQtSdk}"
|
|
"-DLOGOS_QT_HOST_ROOT=${logosQtHost}"
|
|
"-DLOGOS_PROTOCOL_ROOT=${logosProtocolPkg}"
|
|
"-DLOGOS_VIEW_TEMPLATE_DIR=${viewTemplates}"
|
|
"-DLOGOS_VIEW_INCLUDE_DIR=${logosViewInclude}"
|
|
] ++ goCmakeFlags ++ apiStyleCmakeFlags
|
|
++ lib.optionals isRustModule [ "-DLOGOS_MODULE_RUST_STATIC_LIBS=${rustStaticName}" ];
|
|
extraEnv = {
|
|
LOGOS_CPP_SDK_ROOT = "${logosSdk}";
|
|
LOGOS_QT_SDK_ROOT = "${logosQtSdk}";
|
|
LOGOS_QT_HOST_ROOT = "${logosQtHost}";
|
|
LOGOS_PROTOCOL_ROOT = "${logosProtocolPkg}";
|
|
LOGOS_MODULE_BUILDER_ROOT = builderCmakeRoot;
|
|
# Both channels on purpose, not belt-and-braces: LogosModule.cmake
|
|
# prefers the cache variable above and falls back to this env var,
|
|
# and the two reach different consumers. The flag is what a nix
|
|
# buildPlugin's cmakeConfigurePhase sees; the env var is what a
|
|
# hand-run `cmake` in a dev shell sees, where no cmakeFlags exist.
|
|
LOGOS_VIEW_TEMPLATE_DIR = "${viewTemplates}";
|
|
LOGOS_VIEW_INCLUDE_DIR = "${logosViewInclude}";
|
|
};
|
|
}
|
|
# Only pass interfaceDeps when the module declares any — keeps existing
|
|
# dependency-only modules buildable against a backend that predates the
|
|
# interface-dependencies feature (graceful degradation). A Rust module's
|
|
# deps ALSO feed the Rust generator (rustDepFlags) for the typed
|
|
# modules()/bind() it actually calls; they still go to the C++ backend
|
|
# too so the generated umbrella (logos_sdk.h, emitted from
|
|
# metadata.dependencies) finds each dep's api header and compiles.
|
|
// lib.optionalAttrs (config.interface_dependencies != []) {
|
|
interfaceDeps = resolvedInterfaceDeps;
|
|
}
|
|
# LIDL-based concrete deps → `--dep` flags (generate from the dep's
|
|
# published LIDL, no dep plugin build). Gated so a backend that predates
|
|
# this feature still builds (such deps then fall through unresolved).
|
|
// lib.optionalAttrs (staticDeps != []) {
|
|
inherit staticDeps;
|
|
});
|
|
|
|
# Compile the plugin for a variant (delegated to the backend).
|
|
buildVariant = variant: selectedBackend.buildPlugin (mkPluginArgs variant);
|
|
|
|
moduleLib = buildVariant "default";
|
|
moduleLibPortable = if hasVariants then buildVariant "portable" else null;
|
|
|
|
# Ready-to-build source tree: the backend runs every generator the build
|
|
# runs, then snapshots the result (module source + generated_code/) instead
|
|
# of compiling. Same args as the default plugin build, so the emitted tree
|
|
# is exactly what a real build generates. Built from the module's
|
|
# `nix develop` shell (which exports LOGOS_*_ROOT) without re-running codegen.
|
|
moduleGenerate = selectedBackend.generate (mkPluginArgs "default");
|
|
|
|
# Two header variants per module — Qt-typed and lp (Qt-free,
|
|
# logos-protocol C ABI). Each is its own Nix derivation, so a
|
|
# downstream module only realises the one its `--api-style` actually
|
|
# consumes. The lp variant lets a core universal (header-first cdylib)
|
|
# module copy a Qt-free typed wrapper for a LEGACY dependency that
|
|
# publishes no `.lidl` (the wrapper is generated by introspecting the
|
|
# dep's built plugin, so it works regardless of how the dep was
|
|
# authored). Default output (`include`) stays the Qt variant for
|
|
# backward compatibility with consumers that read `${dep}/include`.
|
|
# (A third `std` variant — std-typed signatures but still marshalling
|
|
# through QVariant, so never actually Qt-free — used to be built here.
|
|
# `buildPlugin.nix` only ever selects "qt" or "lp", so it had no
|
|
# consumer; it was retired rather than rebuilt for every module.)
|
|
# The contract buildHeaders falls back to when it cannot introspect the
|
|
# built plugin (cross-compilation — a Linux builder cannot load a PE).
|
|
# Preference order:
|
|
# 1. this module's published `lidl` output (universal + cdylib), then
|
|
# 2. a contract committed at src/<name>.lidl.
|
|
# (2) is the escape hatch for handcrafted Qt / `interface: "legacy"`
|
|
# modules, which derive no contract from their sources. It is deliberately
|
|
# NOT folded into `moduleLidl` below: publishing a `lidl` output flips
|
|
# every downstream consumer of this module from the transitional
|
|
# header-copy path onto `--dep` (see depIsLidl above), which would change
|
|
# native builds across the tree. This binding is consumed by buildHeaders
|
|
# ALONE, and buildHeaders only reads it when cross-compiling.
|
|
committedLidl = src + "/src/${config.name}.lidl";
|
|
headerContractLidl =
|
|
if moduleLidl != null then "${moduleLidl}/${config.name}.lidl"
|
|
else if builtins.pathExists committedLidl then "${committedLidl}"
|
|
else null;
|
|
|
|
# `qtGenerator` is what lets the QT variant come from the module's
|
|
# CONTRACT (logos-qt-generator --backend consumer) instead of from
|
|
# introspecting the compiled plugin. Both tools are passed for a pure
|
|
# tool role -- the backend picks the one its selected emitter needs and
|
|
# puts only that one on PATH. Omitting qtGenerator does not break the
|
|
# build; it silently demotes every contract-bearing module back to the
|
|
# legacy Qt emitter, which is why buildHeaders shouts about that case
|
|
# rather than just falling back.
|
|
moduleIncludeQt = selectedBackend.buildHeaders {
|
|
inherit pkgs src config;
|
|
# buildHeaders uses these ONLY to put a generator on PATH -- a pure
|
|
# tool role, hence the BUILD-platform variants under cross.
|
|
logosSdk = logosSdkBuild;
|
|
qtGenerator = logosQtGenerator;
|
|
pluginLib = moduleLib;
|
|
apiStyle = "qt";
|
|
contractLidl = headerContractLidl;
|
|
};
|
|
moduleIncludeLp = selectedBackend.buildHeaders {
|
|
inherit pkgs src config;
|
|
# No qtGenerator: logos-qt-generator has no lp backend, so the lp
|
|
# wrapper still comes from logos-cpp-generator's (non-legacy-Qt) lp
|
|
# emitter, byte-for-byte as before.
|
|
logosSdk = logosSdkBuild;
|
|
pluginLib = moduleLib;
|
|
apiStyle = "lp";
|
|
contractLidl = headerContractLidl;
|
|
};
|
|
|
|
# Publish this module's interface as LIDL — the language-neutral contract
|
|
# a consumer turns into typed `modules().<name>` bindings WITHOUT building
|
|
# this module's plugin (source → LIDL → C++). Cheap: runs only the C++
|
|
# frontend (`--header-to-lidl`) over the impl header; no Qt/plugin compile.
|
|
# Produced for universal modules; the impl header + class come from the
|
|
# same convention `universalCodegen` uses (`codegen.impl_*` or defaults).
|
|
lidlImplClass = config.codegen.impl_class or (modulePreConfigure.defaultImplClassFromName config.name);
|
|
lidlIhRaw = config.codegen.impl_header or "${config.name}_impl.h";
|
|
lidlImplHeaderRel = if lib.hasInfix "/" lidlIhRaw then lidlIhRaw else "src/${lidlIhRaw}";
|
|
moduleLidl =
|
|
if config.interface == "universal"
|
|
then pkgs.runCommand "logos-${config.name}-lidl" {
|
|
nativeBuildInputs = [ logosSdkBuild ];
|
|
} ''
|
|
mkdir -p $out
|
|
logos-cpp-generator --header-to-lidl "${src}/${lidlImplHeaderRel}" \
|
|
--impl-class "${lidlImplClass}" \
|
|
--metadata "${configFile}" \
|
|
-o "$out/${config.name}.lidl"
|
|
''
|
|
# Cdylib modules publish their .lidl as the interface (whether the impl
|
|
# is Rust or C++), so consumers generate typed bindings from it like for
|
|
# any other dep. Contract-first modules copy the committed file; a
|
|
# rust-first module publishes the .lidl DERIVED from its trait.
|
|
else if rustDeriveMode
|
|
then pkgs.runCommand "logos-${config.name}-lidl" {} ''
|
|
mkdir -p $out
|
|
cp "${derivedLidl}/${config.name}.lidl" "$out/${config.name}.lidl"
|
|
''
|
|
else if config.interface == "cdylib" && config.codegen ? lidl
|
|
then pkgs.runCommand "logos-${config.name}-lidl" {} ''
|
|
mkdir -p $out
|
|
cp "${src}/${config.codegen.lidl}" "$out/${config.name}.lidl"
|
|
''
|
|
else null;
|
|
|
|
# Combined package — copies the Qt-typed headers (backward
|
|
# compat). The `//` merge exposes src + version on the derivation
|
|
# so downstream bundlers (nix-bundle-lgx) can locate metadata.json.
|
|
combined = (pkgs.runCommand "logos-${config.name}-module" {} ''
|
|
mkdir -p $out/lib $out/include
|
|
|
|
# Copy library files (not symlinks)
|
|
if [ -d "${moduleLib}/lib" ]; then
|
|
cp -rL ${moduleLib}/lib/* $out/lib/
|
|
fi
|
|
|
|
# Copy include files (not symlinks) — use find to avoid nullglob issues
|
|
if [ -d "${moduleIncludeQt}/include" ] && [ -n "$(find ${moduleIncludeQt}/include -maxdepth 1 -not -name '.*' -not -path ${moduleIncludeQt}/include -print -quit)" ]; then
|
|
cp -rL ${moduleIncludeQt}/include/* $out/include/
|
|
fi
|
|
'') // { inherit src; version = config.version; };
|
|
|
|
in {
|
|
# Individual outputs (e.g., nix build .#chat-lib)
|
|
"${config.name}-lib" = moduleLib;
|
|
"${config.name}-include" = moduleIncludeQt;
|
|
"${config.name}-headers-qt" = moduleIncludeQt;
|
|
"${config.name}-headers-lp" = moduleIncludeLp;
|
|
|
|
# Short aliases (e.g., nix build .#lib)
|
|
lib = moduleLib;
|
|
include = moduleIncludeQt;
|
|
headers-qt = moduleIncludeQt;
|
|
headers-lp = moduleIncludeLp;
|
|
|
|
# Default package - combined lib + include (nix build)
|
|
default = combined;
|
|
|
|
# Ready-to-build codebase: all code generators run, emitted as a source
|
|
# tree (nix build .#generate). Build it from `nix develop` — no generator
|
|
# re-runs (LogosModule.cmake consumes the pre-populated generated_code/).
|
|
generate = moduleGenerate;
|
|
"${config.name}-generate" = moduleGenerate;
|
|
} // lib.optionalAttrs (moduleLibPortable != null) {
|
|
"${config.name}-lib-portable" = moduleLibPortable;
|
|
lib-portable = moduleLibPortable;
|
|
} // lib.optionalAttrs (moduleLidl != null) {
|
|
# Published LIDL contract — consumers generate bindings from this without
|
|
# building the plugin. Cheap (frontend only). Absent for non-universal
|
|
# modules, so consumers fall back to the header-copy path for those.
|
|
"${config.name}-lidl" = moduleLidl;
|
|
lidl = moduleLidl;
|
|
}
|
|
);
|
|
|
|
# Development shell (delegates to backend for deps)
|
|
devShells = forAllSystems (system:
|
|
let
|
|
pkgs = common.mkPkgs system;
|
|
config = configFor system;
|
|
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
|
# Build-platform half of the SDK. logos-cpp-generator is invoked by BARE
|
|
# NAME from a build phase (logos-plugin-qt/lib/buildPlugin.nix:145), so it
|
|
# must run on the builder. Under cross, packages.x86_64-windows.default
|
|
# carries no runnable generator at all -- logos-cpp-sdk/nix/bin.nix:39
|
|
# silently skips the mingw .exe -- hence "command not found".
|
|
#
|
|
# `logosSdk` deliberately stays TARGET-typed: it is ALSO the header and
|
|
# CMake-package root passed to LOGOS_CPP_SDK_ROOT, and those must keep
|
|
# coming from the Windows set. Splitting the two roles is the whole point;
|
|
# pointing the headers at the build system would produce a build that
|
|
# SUCCEEDS while linking the wrong architecture.
|
|
#
|
|
# buildSystemFor is the identity on every native system, so this is a
|
|
# no-op off the Windows target.
|
|
logosSdkBuild = logos-cpp-sdk.packages.${common.buildSystemFor system}.default;
|
|
logosQtSdk = logos-qt-sdk.packages.${system}.default;
|
|
# Same repoint in the dev shell: LOGOS_QT_HOST_ROOT below.
|
|
logosQtHost = logos-plugin-qt.packages.${system}.logos-qt-host;
|
|
# The Qt glue generator (universal/cdylib/ui backends) — Qt code is
|
|
# the Qt layer's product; logos-cpp-generator keeps Qt-free outputs.
|
|
logosQtGenerator = logos-qt-sdk.packages.${common.buildSystemFor system}.logos-qt-generator;
|
|
# The cdylib Qt-plugin glue generator lives in logos-plugin-qt (the Qt
|
|
# plugin BACKEND owns the glue; the SDK does not). logos-qt-sdk still
|
|
# ships an older copy of the SAME emitter, and calling that one is not a
|
|
# compile error — it silently emits STALE glue. That is how a
|
|
# host-services grant went undelivered while every build stayed green.
|
|
logosQtHostGenerator =
|
|
logos-plugin-qt.packages.${common.buildSystemFor system}.logos-qt-host-generator;
|
|
# The four LogosView*.in templates logos_module(REP_FILE ...) instantiates.
|
|
# They live in logos-view-module (the ui_qml authoring flavour), NOT in
|
|
# the plugin backend any more, and cmake/LogosModule.cmake here refuses to
|
|
# guess — it hard-errors unless handed LOGOS_VIEW_TEMPLATE_DIR.
|
|
#
|
|
# buildSystemFor, not plain ${system}: these are text files with no
|
|
# platform dimension, and logos-view-module publishes only the four
|
|
# NATIVE systems, so `packages.x86_64-windows` would EVAL-fail on the
|
|
# Windows leg — a failure that is invisible until someone crosses.
|
|
viewTemplates =
|
|
logos-view-module.packages.${common.buildSystemFor system}.logos-view-templates;
|
|
# The VIEW plugin glue generator (`--backend ui`). It lives in
|
|
# logos-view-module, beside the LogosView*.in templates the glue it emits
|
|
# is compiled against and beside logos_ui_plugin_context.h, which that
|
|
# glue calls into -- the three are one authoring surface and used to be
|
|
# split across two repos. logos-qt-sdk shipped the same emitter and
|
|
# rotted: it gained the teardown hook, the copy here did not, and nothing
|
|
# detected it because a missing hook is silent at every layer.
|
|
#
|
|
# buildSystemFor: a code generator RUNS on the build machine, and
|
|
# logos-view-module publishes only the four NATIVE systems, so plain
|
|
# ${system} would EVAL-fail on the Windows leg.
|
|
logosViewGenerator =
|
|
logos-view-module.packages.${common.buildSystemFor system}.logos-view-generator;
|
|
# logos_ui_plugin_context.h -- the context a view's *Backend derives, and
|
|
# the header the emitted glue calls maybeUiPluginAboutToUnload() in.
|
|
#
|
|
# It comes from logos-view-module, the SAME pin as the generator above,
|
|
# and that is the whole point. The emitter and this header are one
|
|
# MATCHED PAIR: the emitter writes a call, the header declares what it
|
|
# calls. While both lived in logos-qt-sdk they moved together under one
|
|
# pin and could not disagree. Sourcing the generator from one repo and
|
|
# this header from another would make every ui_qml build depend on two
|
|
# pins agreeing, with nothing enforcing it -- and the failure is a
|
|
# compile error deep inside GENERATED code, far from the pin that caused
|
|
# it. One pin, one pair.
|
|
logosViewInclude =
|
|
logos-view-module.packages.${common.buildSystemFor system}.include;
|
|
logosProtocolPkg = logos-protocol.packages.${system}.default;
|
|
logosModule = logos-module.packages.${system}.default;
|
|
|
|
backendShell = selectedBackend.devShellInputs pkgs { inherit logosModule; };
|
|
buildPkgs = map (getPkg pkgs) config.nix_packages.build;
|
|
runtimePkgs = map (getPkg pkgs) config.nix_packages.runtime;
|
|
|
|
# Resolve external lib inputs for this system so we can point cmake directly
|
|
# at their Nix store paths via LOGOS_EXT_ROOT_<NAME>, skipping the ./lib/ staging copy.
|
|
resolveExtInputDev = name: value:
|
|
if builtins.isAttrs value && value ? input then
|
|
let pkgName = (value.packages or {}).default or "default";
|
|
in value.input.packages.${system}.${pkgName} or null
|
|
else
|
|
value.packages.${system}.default or value;
|
|
devExternalLibs = lib.filterAttrs (_: v: v != null && lib.isDerivation v)
|
|
(lib.mapAttrs resolveExtInputDev externalLibInputs);
|
|
in {
|
|
default = pkgs.mkShell {
|
|
nativeBuildInputs = backendShell.nativeBuildInputs ++ buildPkgs ++ [ logosSdkBuild logosViewGenerator ];
|
|
buildInputs = backendShell.buildInputs ++ runtimePkgs ++ lib.attrValues devExternalLibs;
|
|
shellHook = ''
|
|
${backendShell.shellHook}
|
|
export LOGOS_CPP_SDK_ROOT="${logosSdk}"
|
|
export LOGOS_QT_SDK_ROOT="${logos-qt-sdk.packages.${system}.default}"
|
|
export LOGOS_QT_HOST_ROOT="${logosQtHost}"
|
|
export LOGOS_PROTOCOL_ROOT="${logos-protocol.packages.${system}.default}"
|
|
export LOGOS_MODULE_BUILDER_ROOT="${builderCmakeRoot}"
|
|
# The plugin backend used to export this from its own devShellInputs
|
|
# shellHook (spliced in above). It stopped when the templates left it,
|
|
# and nothing in that repo can catch the regression — a missing value
|
|
# here surfaces only when someone hand-runs cmake on a REP_FILE module.
|
|
export LOGOS_VIEW_TEMPLATE_DIR="${viewTemplates}"
|
|
export LOGOS_VIEW_INCLUDE_DIR="${logosViewInclude}"
|
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: drv: ''
|
|
export LOGOS_EXT_ROOT_${lib.toUpper name}="${drv}"
|
|
'') devExternalLibs)}
|
|
echo "Logos ${config.name} module development environment"
|
|
echo "LOGOS_CPP_SDK_ROOT: $LOGOS_CPP_SDK_ROOT"
|
|
echo "LOGOS_MODULE_ROOT: $LOGOS_MODULE_ROOT"
|
|
echo "LOGOS_MODULE_BUILDER_ROOT: $LOGOS_MODULE_BUILDER_ROOT"
|
|
'';
|
|
};
|
|
}
|
|
);
|
|
|
|
# LGX package outputs (nix-bundle-lgx provided by the builder)
|
|
nixBundleLgx = nix-bundle-lgx;
|
|
|
|
optionalLgx =
|
|
{
|
|
packages = forAllSystems (system:
|
|
let
|
|
bundleLgx = nixBundleLgx.bundlers.${system}.default;
|
|
bundleLgxPortable = nixBundleLgx.bundlers.${system}.portable;
|
|
installDev = nix-bundle-logos-module-install.bundlers.${system}.dev;
|
|
installPortable = nix-bundle-logos-module-install.bundlers.${system}.portable;
|
|
moduleLib = packages.${system}.lib;
|
|
# Use the portable-linked plugin for lgx-portable when available
|
|
moduleLibForPortable =
|
|
packages.${system}.lib-portable or moduleLib;
|
|
in {
|
|
lgx = bundleLgx moduleLib;
|
|
install = installDev moduleLib;
|
|
lgx-portable = bundleLgxPortable moduleLibForPortable;
|
|
install-portable = installPortable moduleLibForPortable;
|
|
}
|
|
);
|
|
};
|
|
|
|
# Resolve the standalone app: explicit override > built-in from module-builder
|
|
resolvedStandalone =
|
|
if logosStandalone != null then logosStandalone
|
|
else if config.type == "ui" then logos-standalone-app
|
|
else null;
|
|
|
|
optionalApps =
|
|
if resolvedStandalone == null then {}
|
|
else {
|
|
apps = forAllSystems (system:
|
|
let
|
|
pkgs = common.mkPkgs system;
|
|
config = configFor system;
|
|
# Collect all module dependencies (direct + transitive) for bundling
|
|
allDeps = common.collectAllModuleDeps system flakeInputs config.dependencies;
|
|
in {
|
|
default = mkStandaloneApp {
|
|
inherit pkgs;
|
|
standalone = resolvedStandalone.packages.${system}.default;
|
|
plugin = packages.${system}.default;
|
|
metadataFile = configFile;
|
|
dirName = "logos-${config.name}-plugin-dir";
|
|
format = "qt-plugin";
|
|
moduleDeps = allDeps;
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
# Merge LGX outputs into packages
|
|
mergedPackages = lib.mapAttrs (system: sysPkgs:
|
|
sysPkgs // (optionalLgx.packages.${system} or {})
|
|
) packages;
|
|
|
|
# Build unit tests — explicit config wins, otherwise auto-detect tests/CMakeLists.txt
|
|
mkTests = import ./mkLogosModuleTests.nix {
|
|
inherit nixpkgs lib common parseMetadata;
|
|
inherit logos-cpp-sdk logos-protocol logos-qt-sdk logos-plugin-qt;
|
|
# Source of logos-view-generator: a ui_qml module's unit tests run the same
|
|
# autoCodegen the plugin build does.
|
|
inherit logos-view-module;
|
|
logos-test-framework = logos-test-framework;
|
|
};
|
|
|
|
resolvedTests =
|
|
if tests != null then tests
|
|
else if builtins.pathExists (src + "/tests/CMakeLists.txt") then {
|
|
dir = src + "/tests";
|
|
}
|
|
else null;
|
|
|
|
testChecks =
|
|
if resolvedTests == null then {}
|
|
else mkTests {
|
|
inherit src flakeInputs externalLibInputs;
|
|
configFile = configFile;
|
|
testDir = resolvedTests.dir;
|
|
mockCLibs = resolvedTests.mockCLibs or [];
|
|
preConfigure = resolvedTests.preConfigure or preConfigure;
|
|
extraBuildInputs = resolvedTests.extraBuildInputs or [];
|
|
extraCmakeFlags = resolvedTests.extraCmakeFlags or [];
|
|
};
|
|
|
|
optionalTests =
|
|
if testChecks == {} then {}
|
|
else { checks = testChecks; };
|
|
|
|
# Also expose unit-tests as a package so `nix build .#unit-tests` works
|
|
testPackages =
|
|
if testChecks == {} then {}
|
|
else lib.mapAttrs (_system: sysChecks:
|
|
{ unit-tests = sysChecks.unit-tests; }
|
|
) testChecks;
|
|
|
|
finalPackages = lib.mapAttrs (system: sysPkgs:
|
|
sysPkgs // (testPackages.${system} or {})
|
|
) mergedPackages;
|
|
|
|
in {
|
|
packages = finalPackages;
|
|
inherit devShells config;
|
|
# The RESOLVED config, per target. `config` above cannot answer for a
|
|
# platform-keyed field and says so when asked; a consumer that needs
|
|
# `dependencies` / `include` / `main` for a specific system reads this.
|
|
# collectAllModuleDeps already prefers it when a dependency publishes one.
|
|
configFor = forAllSystems configFor;
|
|
inherit metadataJson;
|
|
} // optionalApps // optionalTests
|