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>
15 KiB
Nix API Reference
Complete reference for logos-module-builder Nix functions.
Overview
The logos-module-builder exposes its API via lib attribute:
logos-module-builder.lib.mkLogosModule { ... } # core + legacy UI widgets
logos-module-builder.lib.mkLogosQmlModule { ... } # ui_qml (QML view + optional C++ backend)
mkLogosModule
Builder for core C++ modules and legacy UI widget modules. For ui_qml modules (QML view with optional C++ backend), use mkLogosQmlModule instead.
Syntax
mkLogosModule {
src = ./.;
configFile = ./metadata.json;
# Optional
flakeInputs = inputs; # Pass all flake inputs — deps auto-resolved
externalLibInputs = { }; # For external C libs fetched as flake inputs
extraBuildInputs = [ ];
extraNativeBuildInputs = [ ];
configOverrides = { };
preConfigure = ""; # String or function: { externalLibs }: "..."
postInstall = "";
logosStandalone = null; # Override logos-standalone-app for `nix run`
}
Parameters
src (required)
Path to the module source directory.
src = ./.;
configFile (required)
Path to the metadata.json configuration file.
configFile = ./metadata.json;
flakeInputs (optional)
All flake inputs. The builder automatically filters this by dependencies in metadata.json to resolve module dependencies — you don't need to pass them individually.
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs; # dependencies[] in metadata.json are resolved automatically from input names
};
externalLibInputs (optional)
Flake inputs for external C/C++ libraries. Keys must match library names in metadata.json's nix.external_libraries. For pre-built vendor libraries, use vendor_path in metadata.json instead — no externalLibInputs needed.
The builder auto-detects whether the resolved input is a Nix derivation (via lib.isDerivation). If it is, the derivation is used directly. If it's raw source, it's built with make / custom command. No flags needed in metadata.json.
Simple format — bare flake input, resolves to packages.${system}.default:
externalLibInputs = {
gowalletsdk = inputs.go-wallet-sdk;
};
Structured format — per-variant package mappings. When any entry uses this format, the builder produces both lib and lib-portable outputs, each linked against the corresponding external lib variant. The lgx output bundles lib and lgx-portable bundles lib-portable.
externalLibInputs = {
logos_pm = {
input = inputs.logos-package-manager;
packages = {
default = "lib"; # → input.packages.${system}.lib
portable = "lib-portable"; # → input.packages.${system}.lib-portable
};
};
};
extraBuildInputs (optional)
Additional Nix packages to add to buildInputs.
extraBuildInputs = with pkgs; [
openssl
libsodium
];
extraNativeBuildInputs (optional)
Additional Nix packages to add to nativeBuildInputs (build-time only).
extraNativeBuildInputs = with pkgs; [
rustc
cargo
];
configOverrides (optional)
Override values from metadata.json. Merged recursively.
configOverrides = {
version = "2.0.0";
nix_packages = {
build = [ "extra-package" ];
};
};
metadata.json: interface, codegen, and Go static libs (automatic)
The builder prepends steps before your preConfigure:
-
"interface": "universal"— derives everything fromsrc/<name>_impl.h(impl class derived from the module name, e.g.accounts_module→AccountsModuleImpl) in three steps:logos-cpp-generator --header-to-lidl→generated_code/<name>.lidl, the contract (also the events sidecar dependents' typed-event codegen reads)logos-qt-host-generator --lidl … --backend cdylib→<name>_cdylib_glue.{h,cpp}, the Qt pluginlogos_hostloadslogos-cpp-generator --lidl … --backend cdylib→<name>_module_impl.cpp,<name>_types.h(and<name>_events_cdylib.cppwhen the header declareslogos_events:), the Qt-free C-ABI wrapper around the impl class
(A single
logos-cpp-generator --from-header --backend qtcall used to do all of this;--backend qtno longer exists.)Optional overrides:
"interface": "universal", "codegen": { "impl_header": "src/custom_impl.h", "impl_class": "CustomImpl" }Method docs: a comment directly above a method's declaration becomes that method's
description, carried ingetMethods()and shown bylm methods,logoscore module-info, and Basecamp's Methods list:/// Processes the input and returns a result. QString doSomething(const QString& input); -
"interface": "universal"+"type": "ui_qml"(handled bymkLogosQmlModule) — for a C++ UI backend,"codegen": { "rep": "src/<name>.rep" }names the Qt Remote Objects view contract;repcruns on it and the*Backendclass derives the generated<RepClass>SimpleSource. The*Plugin/*Interfaceglue is still generated. -
External libraries —
logos-plugin-qtalready copies flake-built externals intolib/before your hook; you usually do not need tocpthem inpreConfigure. -
go_build: trueon annix.external_librariesentry — passes-DLOGOS_MODULE_GO_STATIC_LIBS=…to CMake soLogosModule.cmakelinks the static archive with whole-archive /-force_loadas needed.
LOGOS_MODULE_BUILDER_ROOT always points at this flake’s source — both entry points (mkLogosModule and buildCppPlugin) set it unconditionally, and evaluation throws if cmake/LogosModule.cmake is missing from it. That file is the only copy: logos-plugin-qt used to ship a second one, and because the override used to be conditional the two were selected by module type (every ui_qml plugin configured with the backend’s copy, every core module with this one). logos_module() echoes the file it was read from at configure time so a future fork is visible in the log.
preConfigure (optional)
Extra shell commands (or a function) appended after the automatic codegen / setup above.
String form — plain shell commands:
preConfigure = ''
echo "Running custom preConfigure"
./scripts/generate-something.sh
'';
Function form — receives { externalLibs } with resolved store paths keyed by library name:
preConfigure = { externalLibs }: ''
# Only when you need something beyond the defaults
echo "extra step using ${externalLibs.mylib}"
'';
postInstall (optional)
Shell commands to run after installation.
postInstall = ''
# Custom post-install
mkdir -p $out/share
cp extra-files/* $out/share/
'';
logosStandalone (optional)
Override the logos-standalone-app used for nix run. By default, logos-module-builder bundles logos-standalone-app internally and automatically wires up apps.default for UI modules ("type": "ui" or QML modules). You only need this parameter if you want to use a custom build of logos-standalone-app.
logosStandalone = my-custom-standalone-app;
Return Value
Returns an attribute set with:
{
packages = {
<system> = {
default = <combined package>;
<name>-lib = <library package>;
<name>-include = <headers package>;
lib = <library package>;
include = <headers package>;
lgx = <lgx package>; # always included
lgx-portable = <portable lgx>; # always included
install = <dev install package>; # always included
install-portable = <portable install package>; # always included
# Only when externalLibInputs uses structured format with variants:
<name>-lib-portable = <portable library package>;
lib-portable = <portable library package>;
};
};
devShells = {
<system> = {
default = <dev shell>;
};
};
apps = { ... }; # only for type="ui" (legacy widget modules)
config = <parsed config>;
metadataJson = <metadata.json content>;
}
Example
{
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;
preConfigure = ''
echo "Building my module..."
'';
};
}
mkLogosQmlModule
Builder for ui_qml modules — QML view with an optional C++ backend. Validates that metadata.json has "type": "ui_qml" and a non-null "view" field. When "main" is declared, compiles the C++ backend via buildCppPlugin and bundles it alongside the QML view. When "main" is absent, produces a QML-only output (no compilation). Always wires apps.default.
Syntax
mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
# Optional — same parameters as mkLogosModule
flakeInputs = inputs;
externalLibInputs = { };
extraBuildInputs = [ ];
extraNativeBuildInputs = [ ];
configOverrides = { };
preConfigure = "";
postInstall = "";
logosStandalone = null;
}
Return Value
{
packages = {
<system> = {
default = <combined plugin (if backend) + QML view>;
<name>-lib = <library package>; # only when backend present
lib = <library package>; # only when backend present
lgx = <lgx package>;
lgx-portable = <portable lgx>;
install = <dev install package>;
install-portable = <portable install package>;
};
};
devShells = {
<system> = {
default = <dev shell>;
};
};
apps = {
<system> = {
default = <logos-standalone-app runner>; # always present
};
};
config = <parsed config>;
metadataJson = <metadata.json content>;
}
Example (with backend)
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json; # type: ui_qml, main: "calc_ui_cpp_plugin", view: "qml/Main.qml"
flakeInputs = inputs;
};
}
Example (QML-only, no backend)
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json; # type: ui_qml, view: "Main.qml" (no "main")
flakeInputs = inputs;
};
}
parseMetadata
Parse a metadata.json file.
parseModuleConfig
Parse JSON content and apply defaults, resolving any platforms overlays for
the given target.
let
parseMetadata = logos-module-builder.lib.parseMetadata;
config = parseMetadata.parseModuleConfig {
json = builtins.readFile ./metadata.json;
platform = parseMetadata.platformForSystem system; # inside forAllSystems
};
in {
inherit (config) name version type category description;
inherit (config) dependencies nix_packages external_libraries cmake;
}
platform is required. It may be null, which means "no target known" —
the parse then succeeds, but any field a platforms overlay declares throws
when read instead of quietly returning the base value. That is the shape the
builders use above forAllSystems, where only platform-invariant fields
(name, version, type, interface) are needed.
platformForSystem
Turn a nix system string into the { os, architecture, abi } triple that
when selectors are matched against.
logos-module-builder.lib.parseMetadata.platformForSystem "x86_64-windows"
# { os = "windows"; architecture = "x86_64"; abi = "gnu"; }
Note the abi: the Windows target is mingw (x86_64-w64-mingw32). Do not
derive the triple with lib.systems.elaborate — it reads the pseudo-system
string alone and answers msvc.
platformOf
The same triple, from a package set that is already in scope.
logos-module-builder.lib.parseMetadata.platformOf pkgs.stdenv.hostPlatform
common
Utility functions.
systems
List of supported systems.
logos-module-builder.lib.common.systems
# [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]
# plus "x86_64-windows" when the logos-nix input is threaded into the builder
getLibExtension
Get library extension for platform.
logos-module-builder.lib.common.getLibExtension pkgs
# "dylib" on macOS, "so" on Linux
getPluginFilename
Get plugin filename for module.
logos-module-builder.lib.common.getPluginFilename pkgs "my_module"
# "my_module_plugin.dylib" or "my_module_plugin.so"
collectAllModuleDeps
Recursively resolve all module dependencies (direct + transitive) from flake inputs. Returns a flat attrset mapping module names to their LGX derivations. Used internally by mkStandaloneApp to bundle dependencies.
logos-module-builder.lib.common.collectAllModuleDeps system flakeInputs depNames
# { waku_module = <lgx derivation>; chat = <lgx derivation>; ... }
nameFormats
Convert module name to various formats.
logos-module-builder.lib.common.nameFormats "my_module"
# { snake = "my_module"; pascal = "MyModule"; camel = "myModule"; upper = "MY_MODULE"; }
Lower-level Builders
For advanced use cases, you can use some lower-level builders directly. Plugin compilation has been delegated to backends — mkModuleLib and mkModuleInclude no longer exist.
Plugin Backends
Plugin compilation is delegated to a backend (e.g. logos-plugin-qt). The active backends are exposed as uiBackend and coreBackend:
logos-module-builder.lib.uiBackend.buildPlugin { ... }
logos-module-builder.lib.uiBackend.buildHeaders { ... }
logos-module-builder.lib.coreBackend.buildPlugin { ... }
These are internal implementation details — most modules don't need to call them directly.
mkExternalLib
Build/resolve external libraries from flake inputs. Returns an attrset mapping library names to derivations. If a resolved input is already a Nix derivation (lib.isDerivation), it is used directly; otherwise the source is built with make / custom command.
logos-module-builder.lib.mkExternalLib.buildExternalLibs {
pkgs = ...;
config = ...;
externalInputs = { };
}
mkStandaloneApp
Build the apps.default entry for nix run.
logos-module-builder.lib.mkStandaloneApp {
pkgs = ...;
standalone = logos-standalone-app.packages.${system}.default;
plugin = self.packages.${system}.default;
metadataFile = ./metadata.json;
dirName = "logos-my-module-plugin-dir"; # optional
format = "qt-plugin"; # or "qml"
moduleDeps = { }; # resolved module deps (LGX packages)
}
version
Library version string.
logos-module-builder.lib.version
# "0.2.0"