Nothing in metadata.json was platform-keyed, so every platform decision
lived in nix or CMake and modules declared cross-platform supersets by
hand: `include` lists the .so, .dylib AND .dll spellings side by side, and
logos-package-downloader-module already forgot its .dll.
A module may now declare an ORDERED list of selector-keyed overlays:
"nix": {
"packages": { "runtime": ["nlohmann_json"] },
"platforms": [
{ "when": { "os": "linux" }, "packages": { "runtime": ["krb5"] } },
{ "when": { "os": "windows", "architecture": "x86_64", "abi": "gnu" },
"cmake": { "extra_link_libraries": ["ws2_32","bcrypt","ntdll"] } }
]
}
Each of os/architecture/abi is independently optional, so one selector
spells a full variant, a whole OS, or an architecture everywhere. Every
matching entry applies in declaration order; lists concatenate and
attrsets recurse. Recursion rather than overwrite is deliberate and is
argued at resolvePlatforms.nix:20 — every overlay-able key under `nix` is
an object, so literal overwrite would drop packages.build in the krb5 case
above and make "lists concatenate" unreachable for the whole block.
Resolution happens over the raw JSON tree BEFORE the existing parse, so
the 300-line body of parseMetadata.nix runs unchanged over the resolved
answer and config.* keeps its exact flat shape. No consumer below the
parse changed.
The design rule throughout is that a selector must never fail SILENTLY,
because a selector that never fires looks exactly like a platform that
needs nothing:
* `parseModuleConfig` now takes `{ json, platform }`, both required, and
validates the platform on EVERY path — including the early return for
a module with no overlays at all. Guarding only the overlay path would
make the guarantee conditional on file content: authored in one repo,
discovered months later in another.
* An unrecognised os/architecture/abi throws with an alias hint, and so
does a component-wise-valid combination that exists on no target
({darwin, aarch64, gnu} — darwin's abi is "unknown").
* A `platforms` key anywhere it is not read throws naming the path, and
the sweep covers near-misses like `platform` (singular), which is one
character from the mistake it exists to prevent.
* Only `include` is overlay-able at the top level for now. `main` and
`dependencies` resolve for the BUILD but the shipped manifest is the
verbatim source file and the LogosModules umbrella is generated from
the raw dependencies array, so a core module keying them would build
one plugin and ship a manifest naming another. The precondition for
re-admitting each is recorded as data, not prose, so the reason
travels with the restriction.
396 assertions (was 369 reported, of which one asserted nothing — an
assertBool missing its third argument made the element a lambda, and
deepSeq does not force lambdas; tests/default.nix now refuses any element
that is not a bool). All seven checks pass.
Proven inert for the existing tree: field-by-field against master's parser
across 24 config keys x 102 modules x 5 targets, 0 differing; and through
the real mkLogosModule, a non-matching overlay yields a byte-identical
derivation to no overlay at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.9 KiB
Quick Reference
Cheat sheet for common logos-module-builder tasks.
Create a New Module
# 1. Create directory
mkdir logos-my-module && cd logos-my-module
# 2. Create metadata.json
cat > metadata.json << 'EOF'
{
"name": "my_module",
"display_name": "My Module",
"version": "1.0.0",
"type": "core",
"interface": "universal",
"category": "general",
"description": "My module",
"main": "my_module_plugin",
"dependencies": [],
"nix": {
"packages": { "build": [], "runtime": [] },
"external_libraries": [],
"cmake": { "find_packages": [], "extra_sources": [] }
}
}
EOF
# 3. Create flake.nix
cat > flake.nix << 'EOF'
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
EOF
# 4. Create CMakeLists.txt
cat > CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.14)
project(MyModulePlugin LANGUAGES CXX)
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
logos_module(NAME my_module SOURCES src/my_module_impl.h src/my_module_impl.cpp)
EOF
# 5. Create source files in src/ directory (universal model: impl class only)
mkdir -p src
# (see templates for source file content)
# 6. Track files and build
git init && git add -A
nix build
metadata.json Quick Reference
{
"name": "module_name",
"version": "1.0.0",
"type": "core",
"interface": "universal",
"category": "general",
"description": "A Logos module",
"main": "module_name_plugin",
"dependencies": ["waku_module", "other_module"],
"nix": {
"packages": {
"build": ["protobuf", "abseil-cpp"],
"runtime": ["zstd"]
},
"external_libraries": [
{ "name": "mylib", "vendor_path": "lib" }
],
"cmake": {
"find_packages": ["Protobuf", "Threads"],
"extra_sources": ["src/helper.cpp"],
"extra_include_dirs": ["include"],
"extra_link_libraries": ["pthread"]
}
}
}
Platform-specific values
include and everything under nix may be overridden per target by an ordered
list of overlays. Every matching entry applies, in declaration order; lists
concatenate, scalars overwrite, and objects recurse (merged key by
key, base-only keys survive).
"include": [],
"platforms": [
{ "when": { "os": "linux" }, "include": ["libcore.so"] },
{ "when": { "os": "darwin" }, "include": ["libcore.dylib"] },
{ "when": { "os": "windows" }, "include": ["libcore.dll"] }
],
"nix": {
"packages": { "runtime": ["nlohmann_json"] },
"platforms": [
{ "when": { "os": "linux" }, "packages": { "runtime": ["krb5"] } }
]
}
The empty base for include is the pattern to copy, not a stylistic choice.
Lists concatenate, so leaving libcore.so in the base would resolve darwin to
["libcore.so","libcore.dylib"] — a cross-platform superset, which is exactly
what the overlays exist to eliminate. The .so is the Linux value; it goes in
the Linux overlay. nix.packages.runtime above shows the other case:
nlohmann_json is genuinely correct everywhere, so it stays in the base.
main and dependencies may not be platform-keyed. Not on principle — the
shipped metadata.json is the source file copied verbatim, so a per-target
value would be resolved for the build and not for the artifact. A platforms
key anywhere but the top level or nix (e.g. nix.packages.platforms) is a
hard error, not a silently skipped overlay.
os ∈ linux | darwin | windows, architecture ∈ x86_64 | aarch64,
abi ∈ gnu | unknown — each independently optional, an empty when is an
error, and an unrecognised value is an error rather than a non-match. Note the
Windows target is mingw, so its abi is gnu (and {"abi":"gnu"} on its own
therefore also matches Linux). See docs/configuration.md for the full rules.
CMakeLists.txt Quick Reference
cmake_minimum_required(VERSION 3.14)
project(MyModulePlugin LANGUAGES CXX)
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
logos_module(
NAME my_module
SOURCES
src/my_module_impl.h
src/my_module_impl.cpp
src/helper.cpp
EXTERNAL_LIBS
mylib
FIND_PACKAGES
Protobuf
LINK_LIBRARIES
pthread
)
PROTO_FILESused to be accepted here and is not parsed any more — compile.protofiles yourself and add the results vianix.cmake.extra_sources.
flake.nix Quick Reference
Basic Module
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
With Module Dependencies
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
waku_module.url = "github:logos-co/logos-waku-module"; # input name must match dependency name
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs; # waku_module resolved automatically from dependencies[]
};
}
With External Library (flake input, built from source)
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
mylib = { url = "github:org/mylib"; flake = false; };
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
externalLibInputs = {
mylib = inputs.mylib;
};
};
}
ui_qml Module (QML view + optional C++ backend)
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
# Add backend module dependencies as inputs if needed
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
Source File Templates (universal model)
You write only the impl class. Everything else is generated from
src/my_module_impl.h into generated_code/: my_module.lidl (the contract),
my_module_cdylib_glue.{h,cpp} (the Qt plugin, Q_PLUGIN_METADATA + onInit)
and my_module_module_impl.cpp / my_module_types.h (the Qt-free C ABI).
The old *_interface.h / *_plugin.{h,cpp} names are no longer emitted.
Module code is Qt-free — use std::string.
Impl Header (src/my_module_impl.h)
#pragma once
#include <string>
#include "logos_module_context.h"
class MyModuleImpl : public LogosModuleContext
{
public:
/// Returns a processed string. Public methods are the module API.
std::string myMethod(const std::string& input);
logos_events:
/// Typed event; subscribers use `modules().my_module.onProcessed(...)`.
void processed(const std::string& result);
};
Impl Implementation (src/my_module_impl.cpp)
#include "my_module_impl.h"
std::string MyModuleImpl::myMethod(const std::string& input) {
std::string result = "Result: " + input;
processed(result); // generated event body fans out to subscribers
return result;
}
LogosModuleContext gives you modules().<dep>.method(...) for typed calls into
dependencies, typed event subscriptions, and an onContextReady() hook (override
it to arm subscriptions once the module is wired).
UI C++ backend (universal ui_qml)
For a C++ UI module ("type": "ui_qml" + "interface": "universal") you write a
.rep view contract plus a *Backend class deriving the generated
<RepClass>SimpleSource and LogosUiPluginContext. Point codegen.rep at the
.rep and use REP_FILE in CMakeLists.txt:
// src/my_ui.rep — the QtRO view contract
class MyUi {
SLOT(int add(int a, int b))
PROP(QString status="Ready" READONLY)
}
// src/my_ui_backend.h
#pragma once
#include "rep_my_ui_source.h"
#include "logos_ui_plugin_context.h"
class MyUiBackend : public MyUiSimpleSource, public LogosUiPluginContext {
public:
int add(int a, int b) override; // feed PROPs via setStatus(...)
};
logos_module(
NAME my_ui
REP_FILE src/my_ui.rep
SOURCES
src/my_ui_backend.h
src/my_ui_backend.cpp
INCLUDE_DIRS
src
)
Common Commands
# Build module (combined lib + include)
nix build
# Build just the library
nix build .#lib
# Build just the generated headers
nix build .#include
# Build .lgx packages
nix build .#lgx
nix build .#lgx-portable
# Run UI module in logos-standalone-app
nix run .
# Enter dev shell
nix develop
# Build specific output (alternative syntax)
nix build .#my_module-lib
nix build .#my_module-include
# Check flake
nix flake check
# Update flake inputs
nix flake update