mirror of
https://github.com/logos-co/logos-module-builder.git
synced 2026-08-30 19:51:07 +00:00
* fix(cmake): one LogosModule.cmake, the union of the two that had diverged
There were two copies — here and in logos-plugin-qt — and the received wisdom
that "the builder's shadows the backend's" is wrong. Selection is BY MODULE
CLASS, and each copy was missing what the other had:
mkLogosModule.nix:79 tests the BUILDER root, always true, so CORE
modules take this copy;
buildCppPlugin.nix:191 tests the MODULE'S OWN src, almost never true, so
every ui_qml module falls through to the backend's.
This copy had no REP_FILE, no logos_replica_factory, no LogosViewPluginBase — a
core module passing REP_FILE had it silently swallowed into
MODULE_UNPARSED_ARGUMENTS. The backend's had no generated_code/*.cpp glob, no
metadata.json configure_file, no LOGOS_API_STYLE and no Rust static-lib block.
Merged as a strict union, 721 -> 888 lines. logos_find_qt stays a macro(): the
function() form is the older one, and the macro IS the mingw fix. The four
LogosView*.in templates come along because the replica-factory function
resolves them as siblings through CMAKE_CURRENT_FUNCTION_LIST_DIR.
Prerequisite for relocating the view-plugin templates, not a cleanup — that
move cannot be reasoned about while two files disagree about what a view module
even is.
Verified: a core module (test_basic_module_cpp) and a ui_qml module
(test_fullapi_ui) both build through this copy, and all five of the builder's
own checks pass, including rust-native-dep, which is what exercises the
LOGOS_MODULE_RUST_STATIC_LIBS block the merge carried.
NOTE this de-duplicates nothing yet: buildCppPlugin.nix still routes ui_qml at
the backend's copy, so every in-tree REP_FILE consumer still compiles against
logos-plugin-qt's. Flipping that routing, and having the backend re-export this
file, is the follow-up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(qml): resolve interface_dependencies in the QML pipeline
buildCppPlugin.nix never resolved interface_dependencies, so a ui_qml module's
interfaces reached the generator only through cpp-generator's own re-read of
metadata.json — invisible to the plugin backend, and silently skipping any entry
with an `input:` (a cross-repo interface), which can only be resolved to a path
by the builder.
Now resolved and passed as interfaceDeps, exactly as mkLogosModule.nix does for
core modules. Needed because the qt consumer wrappers are now emitted per
dependency and per interface by the backend, which has to be told what they are.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: retire interface "provider"; throw instead of generating nothing
Drops providerCodegen and the PROVIDER_HEADER plumbing in LogosModule.cmake.
autoCodegen ends in a catch-all `else ""`, so simply deleting the branch would
make `interface: "provider"` mean "generate no glue at all": the module builds
green and is then un-callable from every consumer, with nothing in the log to
say why. It throws instead, naming the replacement.
tests/test-module-pre-configure.nix covers exactly that: a mutation control
confirms the suite fails with `expected expression to throw, but it succeeded
with ""` when the throw is removed. 258 -> 265 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: parse and validate metadata.json#host_services (C2)
A closed set — token_registry, token_delivery, dynamic_calls — parsed and
VALIDATED, unlike the older `capabilities` field, which parseMetadata.nix never
reads. A security decision must not ride on a key nothing looks at, which is why
this is a new field rather than a new meaning for that one.
Two tiers, and the distinction is the point:
* token_registry / token_delivery are TRUST-ROOT — a module holding them can
enumerate the token store or hand authority to an arbitrary target — so they
are additionally restricted to a hardcoded allowlist of module names
(capability_module). Hardcoded rather than configurable: an allowlist a
module could extend from its own metadata would not be an allowlist.
* dynamic_calls is elevated but NOT name-restricted, because a third-party
module that genuinely forwards untyped calls (a webview shell) has to be
able to ask for it.
An unknown service name is refused OUTRIGHT rather than filtered out: it means
the module believes it holds a privilege that does not exist, and silently
dropping the entry hides that from whoever wrote it. Same reasoning as
lp_grant_host_services, which rejects an unknown name wholesale and leaves the
existing grant untouched.
The declaration is advisory. Authority is the host's grant, pushed into the
module's own image over the module-impl C ABI.
265 -> 273 tests. The allowlist case carries a mutation control: with the
trust-root check removed the suite fails with `expected expression to throw, but
it succeeded with ... "name":"sneaky_module","host_services":["token_registry"]`,
so the test genuinely guards the boundary rather than restating it. Includes the
bypass attempt of mixing a permitted service in with a restricted one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: emit cdylib glue with logos-qt-host-generator, not qt-sdk's stale copy
B1 relocated the cdylib Qt-plugin glue generator to logos-plugin-qt (the Qt
plugin BACKEND owns the glue; the SDK does not), but universalCodegen and
cdylibCodegen still called `logos-qt-generator` from logos-qt-sdk, which ships
an OLDER copy of the same emitter. Both copies compile and both emit working
glue, so nothing failed — the builds simply used stale glue.
That is not theoretical. It is why the Phase C host-services grant never
reached a module: the new glue reads the `hostServices` property and forwards
it across the C ABI, the old one does not, and every build stayed green while
capability_module refused every requestModule for want of a grant that was
delivered to its process and then dropped on the floor.
Measured: `hostServices` appears 4 times in logos-plugin-qt's copy and 0 times
in logos-qt-sdk's. After the switch the built capability_module plugin
references logos_module_grant_host_services twice (the export plus the glue's
call) where it previously referenced it once.
Threads the plugin-qt FLAKE (not its lib — the generator is a package of it)
from flake.nix through lib/default.nix into buildCppPlugin, mkLogosModule and
mkLogosModuleTests, and puts logos-qt-host-generator on PATH everywhere
logos-qt-generator already was. `--backend ui` still uses qt-sdk's generator:
the view backend has not moved.
logos-test-modules ipc-tests: FAIL -> PASS, which is the end-to-end proof that
a universal capability_module now mints tokens under a host-granted privilege.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: thread logos-plugin-qt through the ui_qml build path too
mkLogosQmlModule imports buildCppPlugin at its OWN call site, separate from
lib/default.nix's, and that one was left unwired — so every ui_qml module with a
C++ backend hit `logos-plugin-qt = null` and failed to evaluate.
Caught by the regression batch: logos-accounts-ui went from building to FAIL
while all five test-modules checks passed, which is the shape of a defect in one
module CLASS rather than in the generator switch itself.
logos-accounts-ui builds again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: pin the cdylib glue to the maintained generator
The existing assertion ("universal still emits the cdylib Qt glue") checked for
the string `logos-qt-generator` and so failed the moment the builder switched to
`logos-qt-host-generator` — the test doing its job. Retargeted at the invariant
that actually matters (`--backend cdylib` is still emitted) and extended into a
drift guard:
* both codegen paths MUST name logos-qt-host-generator
* neither may fall back to `logos-qt-generator ` (trailing space, so it cannot
match the host generator; `--backend ui` still legitimately uses qt-sdk's)
Worth the four assertions because the failure they catch is invisible: both
copies of the emitter compile and both produce loadable glue, so reverting to
qt-sdk's would emit stale glue and stay green — which is precisely how the
host-services grant went undelivered for a whole phase.
265 -> 277 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(fixtures): declare universal authoring where the fixture already claimed it
The `core-universal-module` fixture is named "universal", is described as a
"Universal interface module fixture (mirrors logos-accounts-module pattern)",
and sits under a test section headed "Universal interface core module" -- but
it carried no `interface` key at all, so parseMetadata resolved it to
"legacy". The fixture was not testing the thing its name claims. The real
logos-accounts-module is `interface: universal`.
Declare `interface: universal` on the three `type: core` fixtures whose
legacy-ness was incidental, and pin the value so it cannot silently regress:
core-universal-module mirrors logos-accounts-module (universal)
core-module mirrors the minimal template (universal)
extlib-module mirrors the external-lib template and the real
vendor-extlib core modules (storage/wallet/libp2p/
blockchain) -- all universal
No `type: core` module in the workspace is still legacy-authored.
The other four legacy fixtures are LEFT legacy on purpose, and are now pinned
with an assertion + the evidence, so the coverage is deliberate instead of
accidental:
ui-qml-backend-module mirrors logos-package-manager-ui, which is STILL
legacy; the universal ui_qml shape is already covered
on disk by the ui-qml-backend TEMPLATE
qml-module QML-only: no C++ backend to derive a contract from;
module-with-deps mkLogosQmlModule never reads `interface`, and
universal+ui_qml routes to uiCodegen which demands a
.rep neither fixture has
ui-module `type: ui` has no template and one real instance
(logos-basecamp), which is legacy
These are metadata-parse fixtures only -- none of the three edited fixtures
contains any C++, so this is a metadata-shape correction, not a code port.
No fixture in this repo held a hand-written Qt plugin to migrate.
Verified: all 5 checks run BY NAME (default, qml-integration, rust-native-dep,
static-extlib, test-framework-integration) pass before and after. `default`
goes 277 -> 284 assertions, exactly the 7 added. qml-integration,
static-extlib and test-framework-integration produce byte-identical store
paths to baseline. rust-native-dep's path changes only because mkLogosModule
embeds the builder's own flake source (its fixture src is byte-identical:
xa6m5ci38fc5adcbi0hfyqvgzrmyr3j9) -- attributed by rebuilding with the change
stashed. Positive control: reverting core-universal-module to legacy makes
`default` FAIL on the new assertion, so it bites.
NOT verified: no plugin is built from any edited fixture (they are metadata
only), so there is no `lm methods` surface to diff -- the method-surface check
does not apply here. Nothing downstream of this repo was rebuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: hand buildHeaders the qt generator, so a contract can drive the wrapper
`contractLidl` already reached buildHeaders on every call — the qt backend just
declined to read it outside cross-compilation. What was missing was the tool:
logos-qt-generator went into buildPlugin's extraNativeBuildInputs and nowhere
near buildHeaders, so the contract-driven branch could not have run even if it
had been selected.
Passing it is the whole change on this side. `logosQtGenerator` is already the
BUILD-platform binary (buildSystemFor), which is what a cross build needs from
a pure tool role, and the backend puts only the generator its selected emitter
actually uses on PATH — so an lp variant and a contract-less module keep the
derivation they had.
Deliberately NOT passed to moduleIncludeLp: logos-qt-generator has no lp
backend. The lp wrapper still comes from logos-cpp-generator's lp emitter,
which is not the legacy Qt one and is not what this migration is about.
buildCppPlugin — the ui_qml pipeline — is left on the legacy path and now says
why. It computes no `lidl` output for the module it builds, so it has no
contract to hand over; and it costs nothing today, because mkLogosQmlModule
reads `moduleLib` and never `moduleInclude`, so that derivation is never
realised. A ui_qml module is a leaf and nothing consumes its client wrapper.
Fixing it therefore starts with giving that pipeline a contract, not with
adding a flag here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: link the Qt host runtime from logos-qt-host, not logos-qt-sdk
The Qt host runtime -- LogosAPI, LogosAPIProvider, LogosProviderBase and the
legacy PluginInterface -- moved out of logos-qt-sdk into logos-plugin-qt, where
it ships as the `logos-qt-host` CMake package. LogosModule.cmake now takes it
from LOGOS_QT_HOST_ROOT and links logos-qt-host::logos_qt_host; mkLogosModule,
buildCppPlugin, mkLogosModuleTests and the module dev shell all pass that root.
logos-qt-sdk is NOT dropped. It stays required for the Qt-typed headers that
were never part of the host runtime -- logos_qt_lp_bridge.h and logos_qt_wire.h,
which the generated Qt consumer wrappers #include by name, and
logos_ui_plugin_context.h -- and for the logos-qt-generator that emits them. Its
include dirs are kept, but AFTER the host runtime's, so the host headers win the
five names the two roots share.
mkLogosModuleTests now also passes -DLOGOS_QT_HOST_ROOT to logos-test-framework,
whose LogosTest.cmake already prefers it and falls back to LOGOS_QT_SDK_ROOT.
logos-plugin-qt's logos-protocol input gains a `follows` (as logos-qt-sdk's
already had). Building logos-qt-host makes that input load-bearing for the first
time, and a second logos-protocol on the link line would mean a second
TokenManager singleton.
Nothing here fails open. A LOGOS_QT_HOST_ROOT with no runtime under it is a
FATAL_ERROR rather than a fall-through to logos-qt-sdk; a host package that
resolves without defining its target is a FATAL_ERROR rather than a plugin
linked without a runtime; and the legacy logos-qt-sdk fallback, which stays
available while other consumers migrate, announces itself in the configure log.
The new `qt-host-repoint` check pins all four selection paths plus both guards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(b2b): probe logos-qt-sdk by a header it owns, and drop the qt-sdk fallback
Two changes, both forced by logos-qt-sdk no longer forwarding the host runtime's
headers.
The presence probe tested ${LOGOS_QT_SDK_ROOT}/cpp/logos_api.h or
include/cpp/logos_api.h. Neither exists any more: the host split moved
logos_api.h out of qt-sdk's cpp/, and the forwarder that kept it in include/cpp/
is gone. A correct root would have been reported as "logos-qt-sdk not found"
while CMake was looking straight at it. It now probes logos_qt_wire.h, which
this SDK does own -- along with logos_qt_lp_bridge.h and
logos_ui_plugin_context.h, the only reason the root is still required at all.
(The source branch was already dead on arrival: qt-sdk's cpp/ has held no
logos_api.h since B1, so LOGOS_QT_SDK_IS_SOURCE could never be TRUE.)
The legacy fallback -- no LOGOS_QT_HOST_ROOT means take the host runtime from
logos-qt-sdk's forwarding package -- is deleted. Its premise is false now, and
what it would do instead is worse than failing: find_package(logos-qt-sdk)
still succeeds and its INTERFACE target still chains logos-qt-host, so the
build would work by accident through one more hop while the include path it set
up (${LOGOS_QT_SDK_ROOT}/include{,/cpp,/core}) contained none of the headers it
was chosen for. LOGOS_QT_HOST_ROOT is now the one source, and its absence is a
FATAL_ERROR that says so.
test-qt-host-repoint.nix follows: its test 3 asserted the fallback happened and
was announced; it now asserts the fallback is gone -- a build with no
LOGOS_QT_HOST_ROOT aborts even with a good logos-qt-sdk root present. Test 3b is
new and pins the probe fix directly: that same run must NOT say "logos-qt-sdk
not found", and a qt-sdk root scaffolded the way the real prefix now looks
(logos_qt_wire.h, no logos_api.h) is still found.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: LogosModule.cmake is this repo's, and every module type now gets it
logos-plugin-qt shipped a second copy of cmake/LogosModule.cmake, and which
one a module configured with was decided by its TYPE:
buildCppPlugin.nix set LOGOS_MODULE_BUILDER_ROOT only when the MODULE's own
repo carried a cmake/LogosModule.cmake. No module does, so every ui_qml
plugin fell through to the value logos-plugin-qt's lib/default.nix sets --
its own root -- while mkLogosModule's core path used this repo's copy.
Both compiled, so the two drifted with nothing to say so: this copy carried
A1's union (the generated_code/*.cpp glob, LOGOS_API_STYLE, the metadata
configure_file, Go/Rust static-lib linking, the external-library FATAL_ERRORs,
logos_find_qt as a Windows-safe macro) while plugin-qt's had B2b's addition of
logos_qt_arg_decode.{cpp,h} to the host-runtime source list -- which
qt_provider_object.cpp's dispatch needs, and whose absence is a link error, not
a configure error. That file is ported here.
Routing: both nix entry points now pass LOGOS_MODULE_BUILDER_ROOT
unconditionally, so a ui_qml plugin and a core module configure with the same
file. The old module-local branch is gone with it -- an unconditional value is
what makes "there is one copy" a property of the code rather than of what
happens to be on disk -- and a missing file now throws instead of falling back
to something that quietly configures differently.
logos_module() prints the file it came from, so a future fork shows up in any
configure log rather than after a phase of debugging.
Verified: counter, counter_qml, logos-storage-ui, logos-package-manager-ui,
test_fullapi_ui, logos-accounts-ui, logos-blockchain-ui, logos-wallet-ui and
logos-evm-wallet-ui (ui_qml) plus test_basic_module_cpp and capability_module
(core) all build, and their configure logs name this file. Checks by name:
default, qml-integration, qt-host-repoint, static-extlib OK; rust-native-dep
and test-framework-integration fail identically at the pre-change baseline
(this repo's own lock pins a logos-protocol without TokenManager::forIdentity,
so logos-qt-host will not compile from it -- green in the workspace closure).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: read the view templates from their owner, and check the ABI they declare
Two changes, one theme: nothing in this build should be able to pick a second
copy of a file without saying so.
1. cmake/LogosView*.in are deleted. They now live once, in logos-plugin-qt's
cmake/, and _logos_module_add_replica_factory takes the directory from
LOGOS_VIEW_TEMPLATE_DIR (cache variable, then environment) instead of
probing for a sibling of this .cmake file.
The sibling probe is what made the duplication possible: it meant the
templates had to sit next to LogosModule.cmake, but logos-plugin-qt's
rep-file-plugin fixture instantiates them too and cannot depend on this
repo, so it kept a byte-identical copy. logos-plugin-qt is the only place
both consumers can read one copy from; see its cmake/README.md.
There is no fallback. An unset variable, or a directory missing any of the
four templates, is a FATAL_ERROR naming what it wanted.
2. A new `view-interface-abi` check, in CI.
LogosViewPlugin and LogosViewReplicaFactory are declared TWICE and always
will be: module side in logos-plugin-qt's templates, host side in
logos-view-module-runtime's headers. They cannot share a header — a module
plugin has to compile against Qt alone, and logos-view-module-runtime
depends on logos-plugin-qt, so the include could only point the wrong way.
They bind at runtime through the IID string, and a mismatch there is silent
the whole way: both sides compile, the plugin loads, qobject_cast returns
nullptr, the view is blank.
Until now the only thing holding those two in agreement was a comment
asking a human to keep them in sync — which had already rotted (it pointed
at src/, the headers are in include/) and prevented none of the five
duplicate-source defects in this refactor.
This repo depends on logos-plugin-qt AND on logos-view-module-runtime, and
is the only one that does, so the comparison lives here. tests/
view-interface-abi.py extracts the IID and the ordered pure-virtual list
from both sides and fails on any difference — including a class rename,
which would otherwise turn the comparison into a vacuous pass.
Proven by mutation: adding an argument to enableRemoting on the module side,
bumping the IID on the host side, and renaming the host-side class each turn
the check red with a specific message; reverting each turns it green.
Note: like the already-red rust-native-dep and test-framework-integration
checks, view-interface-abi does not evaluate until this repo's logos-plugin-qt
pin advances past the commit that adds packages.<sys>.logos-view-templates. It
says exactly that, and names logos-plugin-qt as the input to move.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(view-abi): check the class that actually binds at runtime, and let it run
The guard had two independent faults; either one alone made it decorative.
1. IT DID NOT CATCH WHAT IT NAMED. The script read `#define <Name>_iid` and
the `virtual ...;` list inside `class <Name>`. Both of those are the
abstract shape. The runtime binding is declared on the CONCRETE class the
template also contains — @LOGOS_FACTORY_CLASS@, with Q_OBJECT,
Q_PLUGIN_METADATA and Q_INTERFACES — and that class was never inspected.
Two mutations that both reach production were run against it and it stayed
green on both:
Q_PLUGIN_METADATA(IID LogosViewReplicaFactory_iid)
-> Q_PLUGIN_METADATA(IID "logos.view.replica_factory/2.0")
Q_INTERFACES(LogosViewReplicaFactory) deleted
It now parses every class in the file, finds the ones deriving from the
interface, and requires: at least one exists (so a dropped base class or a
rename cannot make the comparison vacuous); a QObject-derived one has
Q_OBJECT; a QObject-derived one names the interface in Q_INTERFACES; and
any Q_PLUGIN_METADATA IID resolves to the IID the HOST casts on.
Two more windows were open and are now closed: the argument of
Q_DECLARE_INTERFACE was only checked for EXISTENCE, though it is the string
qobject_cast compares and need not be the #define — it is now resolved
through the file's #defines and compared both across sides and against that
side's own #define; and the interface's own base list, which is its vtable
layout, was not compared at all.
Proven by mutation, 12 cases, each reverted to green afterwards: the 2
above, plus a changed Q_DECLARE_INTERFACE IID, a dropped interface base, a
renamed interface, a dropped Q_OBJECT, a base added to the interface, a
*ViewPluginBase that stops implementing LogosViewPlugin, the 2 the old
guard already caught (regression), and 2 null controls — a comment edit and
a reflowed declaration — which must stay GREEN and do.
The nix wrapper also sets `set -o pipefail` explicitly and folds stderr
into $out. `... | tee $out` takes tee's exit status; that it worked at all
depended on stdenv happening to set pipefail, which is not a thing a guard
should rest on.
2. IT COULD NOT EXECUTE. With no overrides — the CI condition — the check
EVAL-THREW: this flake pinned logos-plugin-qt at b8b9b414, which predates
packages.<sys>.logos-view-templates. So the step was unconditionally red
from the day it landed, camouflaged by test-framework-integration being red
beside it for the same stale pin.
Both logos-plugin-qt and logos-plugin-core (the same repo, selected per
module type — a split pin gives core and ui modules two different
LogosModule.cmake and two Qt host runtimes) now name rev fcf5a29 in the
URL, not just in the lock, so `nix flake update` cannot walk them back onto
a master that still lacks those outputs.
That bump fixes the EVAL failure for all three affected checks.
view-interface-abi is now GREEN with no overrides. rust-native-dep and
test-framework-integration now evaluate and build, and fail later on an
unrelated pre-existing problem: logos-qt-host does not compile against the
pinned logos-protocol, whose TokenManager has no forIdentity /
isolateIdentity. That is the same failure logos-plugin-qt's own #qt-host
check has at fcf5a29 with its own lock and no overrides, so it is not
introduced here and cannot be fixed here.
CI: qt-host-repoint is added — it is hermetic and it existed without ever
being listed. rust-native-dep is deliberately still not listed, with the
reason recorded next to it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(view-abi): follow bases transitively, or the guard watches an empty window
Implementor detection matched only the immediate base clause, so one level of
indirection skipped every concrete-class check — and the "no implementors"
backstop did not fire either, because the helper base still counted as an
implementor in its own right.
That is not a hypothetical shape. lidl_gen_ui.cpp emits every real ui_qml
plugin deriving from LogosViewPlugin INDIRECTLY, via <Rep>ViewPluginBase, so
for that half of the pair the window this guard exists to watch was empty in
production.
Resolve each class's transitive base set through the other classes in the file
before testing membership, and make the QObject check transitive with it. The
two mutations that escaped — an indirect base with the IID bumped to /2.0, and
an indirect base with Q_INTERFACES deleted — now fail, each naming its own
cause; indirection alone stays green and reports both implementors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): bump the B3/B4 stack, and rev-pin the inputs update cannot reach
Moves every input this builder shares with the SDK codegen stack onto the
revisions that stack has actually pushed:
logos-protocol 03842db -> c8bab12 feat/per-client-token-store
logos-cpp-sdk e3744fb -> a04b278 feat/sdk-codegen-b3-d11
logos-qt-sdk c6be61d -> 8a06b87 feat/sdk-codegen-b3-d11
logos-plugin-qt fcf5a29 -> cc24fa1 feat/b4-qt-host-windows-target
logos-plugin-core fcf5a29 -> cc24fa1 same rev, per the type split
logos-view-module-runtime 471dd56 -> 5510acd feat/sdk-codegen-b4-qt-host
logos-standalone-app 288fec2 -> 39f4f2b feat/sdk-codegen-b4-qt-host
logos-test-framework eb1600c -> c382ab1 feat/sdk-codegen-b4-test-framework
Every one of those is a BRANCH TIP, not master. Six of them were plain
`github:logos-co/<repo>` urls, so `nix flake update` would have relocked them
onto master and the bump would silently not have happened -- the update
succeeds, the lock changes, and the rev is still wrong. They are rev-pinned
here for the same reason logos-plugin-qt already was. Each is a fast-forward
from its own master (03842db, e3744fb, c6be61d, 8846fc5, 471dd56, 288fec2 and
eb1600c are ancestors of their targets), so pinning gives up nothing; drop the
revs as the branches land.
logos-plugin-qt/-core go to cc24fa1, the tip of feat/b4-qt-host-windows-target,
NOT to the sibling feat/b4-qt-host-windows-target-8ccb1fc. The sibling
re-baselines onto 8ccb1fc and drops the LogosModule.cmake repoint and the
view-templates commit, so its flake exposes no packages.<sys>.logos-view-templates
and `view-interface-abi` would hit its own throw instead of running. cc24fa1
carries the old fcf5a29 pin's content under rebased shas (4c581a6/e4ea357/fcf5a29
are fe780a6/34704d1/3d7e3e6 there).
logos-test-framework's pin is the least obvious and the one worth keeping: this
branch teaches mkLogosModuleTests to pass -DLOGOS_QT_HOST_ROOT, and it is
LogosTest.cmake on c382ab1 that prefers it -- master's copy knows only
LOGOS_QT_SDK_ROOT, so a master lock links the unit tests against the wrong
runtime root without failing.
The follows keep one logos-protocol on the link line: root, logos-plugin-qt,
logos-plugin-core, logos-qt-sdk and logos-cpp-sdk all resolve to c8bab12.
All seven checks build on aarch64-darwin: default, qml-integration,
qt-host-repoint, rust-native-dep, static-extlib, test-framework-integration,
view-interface-abi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(metadata): codegen.consumer_api_style, gated on how the image gets tokens
Lets a module declare its CONSUMER type surface independently of its provider
packaging. A cdylib-packaged module can now hold Qt-typed dependency wrappers —
the combination that was previously inexpressible, and the only reason
`interface: "provider"` and `--provider-header` are still alive.
The enabling codegen landed first (logos-cpp-sdk 620f2e1, logos-qt-sdk 6b88630):
a default-constructible Qt umbrella and a consumer binding that takes an
explicit origin instead of a LogosAPI. An earlier attempt at this key FAILED
because it was added without that codegen, so its only reachable outcome was a
compile error inside generated code. It is meaningful now.
── The predicate ───────────────────────────────────────────────────────────
packagedAsCdylib = interface == "cdylib"
|| (interface == "universal" && type != "ui_qml")
character-for-character what modulePreConfigure.autoCodegen branches on when it
decides to emit the module-impl C ABI, so the two cannot drift.
The distinction is NOT "this image has no LogosAPI" — a cdylib module's plugin
does contain one, in the Qt glue that receives tokens. What separates the two
worlds is where the image's own TokenManager is FILLED:
cdylib-packaged : logos_module_accept_token -> lp_token_save, same image.
An origin-bound wrapper's null sync hook costs nothing.
Qt plugin : only LpBridge::syncTokens, installed exclusively by
forTarget(api, …). An origin-bound wrapper there would be
silently unauthenticated — the exact defect syncTokens
exists for.
Measured with `nm -gU`, not argued: `logos_module_accept_token` is defined in
exactly the shapes the predicate calls true, across six real plugins.
`universal` + `ui_qml` is the trap — it looks cdylib-shaped but uiCodegen emits
only view glue, so it is a Qt object holding a LogosAPI. Excluded.
`--binding origin` is not selectable from metadata at all: it is derived as
`isQt && packagedAsCdylib` in the backend, an AND no key can reach from the
wrong side. What a key CAN express wrongly is the mirror move — `lp` on a Qt
plugin — unsafe for the identical reason, and refused at eval naming the key.
Verified additive on seven modules spanning legacy/core, universal/core,
universal/ui_qml and QML-only: `diff -r` clean, narHash identical where
comparable, and `test_qml_only` byte-identical by store path. Note compiled
modules cannot share a store path across any builder edit, since mkLogosModule
bakes LOGOS_MODULE_BUILDER_ROOT — narHash is the honest comparison.
Three mutation controls, each failing loudly: removing the eval throw, nulling
the backend's own assertion, and dropping the packagedAsCdylib conjunct from
`originBound` — the last being the one that pins the token-mirror hazard rather
than just the lp refusal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): take the backend that honours consumer_api_style
08ae7ac added the metadata key but this repo still pinned logos-plugin-qt at
cc24fa1c, which predates 2d25069's `originBound` derivation. Both backend
inputs resolved there, so the key parsed and validated and then reached a
backend that ignored it: a module asking for Qt-typed consumers still got
lp-typed wrappers, and failed to compile with a type mismatch 20 errors deep
rather than anything naming the key.
Both `logos-plugin-qt` and `logos-plugin-core` move — they are two inputs onto
the same repo, selected by module type, and a split pin would give core and ui
modules different backends.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): take the SDKs that carry the origin-bound consumer codegen
The backend pin (723244b) was necessary but not sufficient. consumer_api_style
= "qt" on a cdylib emits `new LogosModules()` against the Qt umbrella, which is
default-constructible only from logos-cpp-sdk 620f2e1; and the wrapper it
constructs binds through LpBridge::forOrigin, which arrives with logos-qt-sdk
aca2951. Pinned at a04b278 / 8a06b87 the key resolved, reached a backend that
honoured it, and then failed at
no matching constructor for initialization of 'LogosModules'
in generated code — three pin levels away from anything naming the feature.
Recording the chain, because it took four bumps to find: a module's consumer
surface is decided by test-modules -> module-builder -> {plugin-qt, cpp-sdk,
qt-sdk}, and every level has to move together. Each intermediate state built
something; none of them built the right thing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: refuse a core module that ships a plugin with no interface
`interface` defaults to "legacy", and legacy generates no glue. For a CONSUMER
that is correct: a ui_qml view plugin is never 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 already throws for — the module builds green and is
un-callable from every consumer, with the first symptom arriving at runtime in a
different process.
`main` is the discriminator, and measurement is why. `type` alone cannot do it:
of the modules that reach autoCodegen with no interface, the ten ui_qml ones are
consumers and test_framework_module is core but ships no plugin at all — it has
no plugin source, only calculator.{h,cpp}, and exists to drive mkLogosModuleTests.
Gating on `type == "core"` would have thrown for it. A provider ships a plugin,
so it names one in `main`; that fixture's `main` was vestigial and is dropped
here, which is also the honest description of what it always was.
Both directions checked, not just the quiet one: with the vestigial `main` put
back, test-framework-integration fails to evaluate with this message; with it
dropped, the same attribute evaluates and the check builds. The workspace still
evaluates on aarch64-darwin (624 packages) and x86_64-windows (386).
Depends on logos-test-modules retiring test_ipc_module, which was the last
module this would have thrown for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(metadata): drop `dynamic_calls`, which broke the grants it sat beside
`dynamic_calls` was listed as a host service and is not one. It gated nothing
and could not: the by-name path is ungated at every layer, so any module could
always make dynamic calls without asking.
What the declaration actually did was break the asking module. `hostServiceBit`
(logos_protocol.cpp:109-111) recognises exactly `token_registry` and
`token_delivery`; `lp_grant_host_services` returns LP_ERR_INVALID_ARG on the
first entry it does not recognise (:695-702), which fails the WHOLE grant. So a
module asking for `dynamic_calls` alongside a real service silently lost the
real one — the opposite of the "ADVISORY … an ungranted module gets
LP_ERR_UNSUPPORTED" the comment here promised.
Removed from `known` rather than made inert, so asking is now a build error
instead of a silent downgrade. No module in the tree declares it.
The supported surface for by-name calls is LogosModules::dynamic(target) plus
LpClient::getMethods(), added in logos-cpp-sdk alongside this. Neither needs a
grant, which is the whole point.
Two tests move with it. The "accepted for any module" case becomes an
assertThrows. The "allowlist is not bypassed by mixing in a permitted service"
case loses its premise — both remaining services are trust-root, so there is no
"permitted for anyone" name left to mix, and keeping the old form would have
passed for the wrong reason (unknown name, not the allowlist). It now pins that
asking for BOTH trust-root services does not slip past.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci: use logos-co/setup-nix-cache-action for Nix setup and caching
Replaces the per-repo installer + cachix pair with the shared action, which
installs Nix with the Logos Attic cache (cache.nix.logos.co) preconfigured and
publishes what the job builds — master to the public cache, every other ref to
ci.
Each converted job also gains
environment: ${{ github.ref == 'refs/heads/master' && 'public-cache' || '' }}
because ATTIC_TOKEN_PUBLIC only exists inside that environment. Without it the
secret resolves empty on master and publishing is silently skipped — the job
still passes, so the omission would not show up as a failure.
The action installs Nix itself on every runner, macOS included. That is a
deliberate reversal of the workaround these files carried: the comments here
said cachix/install-nix-action collides with the runner's pre-existing _nixbld
users (eDSRecordAlreadyExists), so DeterminateSystems' installer was used
instead. It no longer reproduces — logos-delivery-module has already been
converted the plain way and its `build-and-test (macos-latest)` leg passes.
Keeping the workaround would have meant a second installer plus a duplicated
substituter/key block in ten files, guarding against something two green runs
say does not happen. If it ever recurs it fails loudly at install, which is
recoverable; the silent-skip above is the failure mode worth engineering
against.
One property is deliberately NOT carried over: the old cachix step ran with
`continue-on-error: true` so a failed cache push could not fail a job whose
tests passed. The action exposes no equivalent, and adding one here would also
swallow genuine setup failures now that the same step installs Nix rather than
only publishing at the end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: align guides and skills with the universal codegen pipeline
The docs/ set, the authoring skills and the CMakeLists template still told
authors to write `interface: "provider"` classes with LOGOS_METHOD, or claimed
the builder runs `logos-cpp-generator --from-header` to emit the Qt glue. The
universal path is three steps — `--header-to-lidl` to derive the contract, then
`logos-qt-host-generator --backend cdylib` for the plugin glue, then
`logos-cpp-generator --lidl --backend cdylib` for the C-ABI exports — and
`--from-header` drives the separate `interface: "cdylib"` authoring path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: take the view templates from logos-view-module
The LogosView*.in templates moved out of logos-plugin-qt, which is now limited
to making a cdylib loadable by logos-module-loader-qt. Add logos-view-module as
an input and read them from there.
- view-interface-abi now reads logos-view-module's logos-view-templates and
diffs it against logos-view-module-runtime's host headers. The throw fallback
named logos-plugin-qt and told the reader to bump that pin; it names
logos-view-module now.
- This repo supplies LOGOS_VIEW_TEMPLATE_DIR itself, since the backend no
longer does, via the extraCmakeFlags/extraEnv seams already carrying
LOGOS_CPP_SDK_ROOT and the other four roots. Both channels are load-bearing:
the cmake flag drives the nix build, the env var is what a dev shell resolves
when someone runs cmake by hand.
- Indexed through common.buildSystemFor, NOT the raw system: logos-view-module
publishes only the four native systems, while this repo's systems list adds
x86_64-windows, so a raw index would EVAL-fail the Windows leg.
- LogosModule.cmake's hard error named logos-plugin-qt as the caller that
passes the variable; it names this repo now.
The input is left on master rather than rev-pinned: the output does not exist
on any pushed branch yet, so view-interface-abi hits the throw until the move
lands and the pin can be set. Verified locally with
--override-input logos-view-module path:...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): pin logos-view-module at the merged template move
logos-view-module#2 merged as 1f95a75, so packages.<sys>.logos-view-templates
now exists on its master and the input no longer has to sit unpinned.
Rev-pinned for the same reason logos-plugin-qt is: `nix flake update` must not
be able to walk this back to a commit without that output, which
view-interface-abi and every ui_qml plugin build need.
view-interface-abi now passes with NO --override-input, which is what the
unpinned state could not demonstrate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): pin logos-standalone-app at the branch that absorbed master
The last pin on this branch that walked master backward. master's b960f44 had
bumped logos-standalone-app to e582e6c; this branch pinned 39f4f2b, which is on
feat/sdk-codegen-b4-qt-host only and does not contain it — so merging would
have reverted the hot-reload fix (#36) and the capability-bundling removal.
That branch has now absorbed master (logos-standalone-app b67eddd, `nix build
.#default` green) and this pins the result, exactly as logos-view-module-runtime
was handled in 8484fbb.
Every root input on this branch now CONTAINS master's rev: cpp-sdk, plugin-qt,
plugin-core, protocol, qt-sdk, test-framework, standalone-app and
view-module-runtime all check out as ancestors. Nothing walks master backward
any more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): track master for every input whose PR has merged
logos-protocol (#59), logos-cpp-sdk (#138), logos-plugin-qt (#19) and
logos-qt-sdk (#33) have all landed, so the four rev pins bridging to them are
retired and each stale rationale is rewritten to name the PR that closed it.
logos-plugin-core moves in lockstep with logos-plugin-qt — it is the same repo
under a second input name, and a split pin would put two logos-qt-hosts in one
closure.
All four were SQUASH-merged, so `merge-base --is-ancestor <pin> master` is
correctly false while the content is in master. Every retirement was confirmed
against master's FILES via gh api, not ancestry.
The qt-sdk pin is worth a note: nothing makes logos-qt-sdk `follows` anywhere, so
one revision across consumers was upheld by hand-pinning the same rev — and it
had already drifted (this repo pinned aca2951 while logos-test-framework and
logos-basecamp pinned 8a06b870). Tracking master makes it structural.
Kept: logos-test-framework @ c382ab1 (master's LogosTest.cmake still knows only
LOGOS_QT_SDK_ROOT, not LOGOS_QT_HOST_ROOT — verified, 0 hits) and logos-rust-sdk
@ 0b4b8ed (no confirmed upstream merge).
All 7 checks build on aarch64-darwin and the flake evaluates on all 4 systems.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): retire four more pins, now that their PRs have merged
Takes this flake from five rev pins to one.
* logos-test-framework c382ab1 -> master. The pin existed because master's
cmake/LogosTest.cmake knew only LOGOS_QT_SDK_ROOT, so an unpinned input made
`test-framework-integration` link the unit tests against the wrong runtime
root. logos-test-framework#6 merged and master now has LOGOS_QT_HOST_ROOT
(6 references) — the exact gap the comment named.
* logos-standalone-app b67eddd -> master (13b81c9, #37). The host shell for
ui_qml `nix run` / integration tests carries the qt-host repoint, the
hot-reload fix (#36) and the capability-bundling removal on master now.
* logos-view-module-runtime 3ef779c -> master (b9a6778f, #25). Master no longer
rev-pins logos-plugin-qt itself, so both sides of the view ABI check are back
in step.
* logos-view-module 1f95a75 -> master. 1f95a75 IS that repo's master tip (the #2
merge), so this pin was already a no-op.
Every rationale is rewritten to name the PR that closed it rather than deleted.
All four upstreams were SQUASH-merged, so the old revs are not ancestors of the
new masters even though their content is in them; the retirements were confirmed
against master's FILES.
Relocked with an explicit --update-input per retired input, NOT a bare
`nix flake lock`: that does not re-resolve an input which is already locked, even
once its url stops carrying a rev, and it silently walked pins backward twice
earlier today. All eight now read master — view-module-runtime b9a6778f,
view-module 1f95a75f, standalone-app 13b81c9e, test-framework 5f75c941,
cpp-sdk 95d7b3a9, protocol f4407ff4, plugin-qt 9b2c64e5, qt-sdk 19c844f2.
logos-rust-sdk @0b4b8ed stays: no merged upstream confirmed for it.
All 7 checks build, including test-framework-integration — the one that would
actually catch a mislocked test-framework rather than compile around it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): retire the last rev pin, logos-rust-sdk
This flake now pins nothing by rev.
0b4b8ed was kept on the grounds that no merged upstream could be confirmed for
it. That was wrong: it is an ANCESTOR of logos-rust-sdk's master (7c65d31), so it
was never waiting on anything — just behind. Unlike every other pin here it also
carried no stated reason; the comment beside it explains the `follows` that cuts
the rust-sdk -> module-builder test cycle, not the rev.
Master adds exactly two commits over it, neither touching what this builder
consumes (logos-lidl-gen and the SDK source the crate links):
671bcc6 chore(lidl-gen): delete the dead gen_provider example (#39)
7c65d31 ci: use logos-co/setup-nix-cache-action for Nix setup and caching (#40)
checks.rust-native-dep — the one that compiles a Rust module through lidl-gen —
rebuilds from source against rust-sdk master and passes, as do `default` and
view-interface-abi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): take the rust-sdk that emits the 11th module-impl export
logos-rust-sdk#41 merged (52f0c6f), so its provider scaffold now emits
logos_module_grant_host_services — the one module-impl C ABI export it was
missing.
That is what broke this branch's cross-language doctests. Since ed50731 the
cdylib Qt glue comes from logos-plugin-qt's logos-qt-host-generator, and the
protocol bump turned on that glue's LOGOS_PROTOCOL_VERSION_MINOR >= 3 call to
the export. logos-protocol only declares it; each language backend owes the
definition, and logos-cpp-sdk had one where logos-rust-sdk did not. Every Rust
plugin therefore linked a call with no definition — not a link error on ELF, but
nixpkgs' -Wl,-z,now forces eager binding, so the module's host process aborted at
dlopen. It died AFTER the token exchange, so the runtime reported a successful
load and only the post-load registry snapshot showed the module gone, which is
why it looked like the dependency resolver dropping the Rust module.
doctests/cross-language-composition.test.yaml: 19 passed, 0 failed — including
the assertion that failed in CI (load-module cpp_frontdesk_module listing
rust_orchestrator_module) and every typed call across the boundary in both
directions. checks.rust-native-dep also builds.
Caveat: this was verified on macOS, which links plugins -undefined
dynamic_lookup and so never binds the symbol at all — that is exactly why the
bug was invisible there and fatal on Linux. What macOS proves is that nothing
regressed and the qt-sdk/cpp-sdk skew is gone; the ubuntu doc-tests are what
confirm the Rust modules now load.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): take the rust-sdk that calls grant_host_services publicly
logos-rust-sdk#42 merged (a3d0d71). #41 had emitted
`logos_rust_sdk::ffi::lp_grant_host_services(...)`, but `mod ffi` is private, so
every Rust module generated against protocol >= 0.3 failed to COMPILE with
error[E0603] — which is why this branch's doctests went from "the Rust module
does not load" to "Build it FAILED". #42 routes the call through a public
wrapper, matching how the crate already exposes save_token.
checks.rust-native-dep rebuilds a real Rust module through the generator against
a3d0d71 and passes, and doctests/cross-language-composition.test.yaml is 19
passed / 0 failed — including the load-module assertion that started this and
every typed call across the C++/Rust boundary in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1071 lines
56 KiB
Nix
1071 lines
56 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
|
|
# Parse the module configuration
|
|
rawConfig = parseMetadata.parseModuleConfig (builtins.readFile configFile);
|
|
config = common.recursiveMerge [ rawConfig 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;
|
|
|
|
# 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.
|
|
#
|
|
# 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;
|
|
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}";
|
|
|
|
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} \
|
|
${lib.optionalString (protocolVersion != null) "--protocol-version ${protocolVersion}"} \
|
|
-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 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}"
|
|
] ++ 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}";
|
|
};
|
|
}
|
|
# 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;
|
|
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;
|
|
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);
|
|
|
|
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 ];
|
|
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}"
|
|
${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;
|
|
# 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;
|
|
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;
|
|
metadataJson = builtins.readFile configFile;
|
|
} // optionalApps // optionalTests
|