Skip to content

Repository files navigation

Logos Module Builder

A shared Nix flake library that provides reusable functions for building Logos modules with minimal boilerplate.

Overview

Instead of duplicating ~600 lines of build configuration across every module, this library lets you define a module with a single metadata.json file and your source code.

Without Builder With Builder Reduction
~600 lines config ~70 lines config 88%
5 config files 2 config files 60%

Quick Start

1. Create your module directory

my-module/
├── metadata.json        # Single config file (~30 lines)
├── flake.nix            # Minimal flake (~10 lines)
├── CMakeLists.txt       # CMake config (~25 lines)
└── src/                 # Source files (universal authoring model)
    ├── my_module_impl.h
    └── my_module_impl.cpp

In the universal authoring model you write only an impl class deriving LogosModuleContext. Its public methods are the module's API. The Qt plugin glue (my_module_interface.h, my_module_plugin.{h,cpp}, Q_PLUGIN_METADATA, initLogos wiring) is generated from src/my_module_impl.h — you never hand-write it. The classic hand-written interface + plugin path still works for backward compatibility, but the templates and the recommended path are universal.

2. Define your module in metadata.json

{
  "name": "my_module",
  "display_name": "My Module",
  "version": "1.0.0",
  "type": "core",
  "interface": "universal",
  "category": "general",
  "description": "My custom Logos module",
  "main": "my_module_plugin",
  "dependencies": ["waku_module"],

  "nix": {
    "packages": {
      "build": ["protobuf"],
      "runtime": ["zstd"]
    },
    "external_libraries": [],
    "cmake": { "find_packages": [], "extra_sources": [] }
  }
}

3. Create a minimal flake.nix

{
  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;
    };
}

4. Build your module

git init && git add -A   # Nix needs files tracked by git
nix build                    # Build everything
nix build .#lib              # Build just the library
nix build .#generate         # Emit a ready-to-build codebase (all code generators run)
nix build .#lgx              # Build .lgx package
nix build .#lgx-portable     # Build portable .lgx package
nix build .#install          # Build, package, and install (dev)
nix build .#install-portable # Build, package, and install (portable)

nix build .#generate runs every code generator that is part of the build — logos-cpp-generator (--general-only + dependency/interface wrappers) and the interface-specific glue (LIDL, Qt glue, C-ABI dispatch, UI plugin glue) — and leaves the result in result/: the module source plus a fully-populated generated_code/. Inspect the generated glue, or build it directly from the module's nix develop shell (which exports the LOGOS_*_ROOT vars) without re-running any generator. The output is exactly what a normal build compiles. It is produced for every C++ module and for UI modules with a C++ backend (QML-only modules have no generators to run).

UI modules: nix run with logos-standalone-app

For type: "ui_qml" modules, logos-module-builder automatically wires up apps.default so nix run . launches the module in logos-standalone-app. No separate logos-standalone-app input is needed — it is bundled inside logos-module-builder.

With C++ backend (mkLogosQmlModule — validates "type": "ui_qml" + "view" field, compiles backend when "main" is set):

The C++ plugin runs in a separate ui-host process (process-isolated), and the QML view is loaded in the host application. Communication happens via Qt Remote Objects over a private socket. Use logos.module() from QML to access the backend replica.

{
  inputs = {
    logos-module-builder.url = "github:logos-co/logos-module-builder";
    # Add backend dependencies as inputs:
    # 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;
      flakeInputs = inputs;
    };
}

QML-only (mkLogosQmlModule — no C++ compilation, runs in-process):

{
  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;
      flakeInputs = inputs;
    };
}

Then nix run . launches the module in logos-standalone-app. Dependencies listed in metadata.json are automatically bundled from their LGX packages and loaded at runtime.

See templates/ui-qml-backend, templates/ui-qml, and lib/mkLogosQmlModule.nix.

UI modules: the dev loop

nix run . re-evaluates the flake and rebuilds the plugin on every invocation — including for a one-character QML edit, because src covers the whole tree. For iterating on a view, build the launcher once and relaunch it instead:

nix build .#ui-dev                     # once
./result/bin/run-logos-standalone-ui   # relaunch after C++ changes

ui-dev is the same wrapper nix run uses — dependency modules bundled and loaded the same way — exposed as a package so it lands in ./result/bin.

Run it from the repo root and it finds your QML source automatically (looking for the view entry under src/<viewDir>/ then <viewDir>/, matching where the build looks). From then on QML edits need no rebuild at all: save a file and the view re-renders in about 200 ms, with the backend process left running.

It prints what it picked up on startup:

run-logos-standalone-ui: hot-reloading QML from /path/to/my-ui/src/qml
  (export DEV_QML_PATH to override, or LOGOS_QML_HOT_RELOAD=0 to disable)

Set DEV_QML_PATH yourself for a non-standard layout, or run from outside the repo to use the installed QML instead. Reloading rebuilds only the QML: a module's C++ backend runs in a separate ui-host process and keeps its state, while QML-side state (scroll position, text fields) resets. A syntax error is logged with its line number and the next save that compiles restores the view. LOGOS_QML_HOT_RELOAD=0 disables watching.

ui-dev is a development target: it is not part of packages.default and is never bundled into .lgx packages.

UI integration tests

For ui_qml modules, mkLogosQmlModule auto-detects .mjs test files in the tests/ directory and wires up integration testing using logos-qt-mcp's test framework. No extra flake inputs needed.

# Run tests hermetically (builds everything, launches headless, runs tests)
nix build .#integration-test -L

# Build the test framework for interactive use (one-time)
nix build .#test-framework -o result-mcp

# Run tests interactively (app must be running with inspector on :3768)
node tests/ui-tests.mjs

Tests use the QML inspector to interact with the running UI — finding elements, clicking buttons, verifying text. Example test file (tests/ui-tests.mjs):

import { resolve } from "node:path";

// CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcp
const root = process.env.LOGOS_QT_MCP || new URL("../result-mcp", import.meta.url).pathname;
const { test, run } = await import(resolve(root, "test-framework/framework.mjs"));

test("my_module: loads UI", async (app) => {
  await app.waitFor(
    async () => { await app.expectTexts(["Hello"]); },
    { timeout: 15000, interval: 500, description: "UI to load" }
  );
});

run();

See the logos-qt-mcp test framework for available assertions and helpers.

Features

  • ~90% reduction in boilerplate per module
  • Single source of truth via metadata.json — used by Nix build and embedded into Qt plugins at compile time
  • Automatic CMake configuration via LogosModule.cmake
  • External library support (vendor pre-built or flake-input source)
  • Cross-platform (macOS, Linux)
  • Auto-resolved module dependencies from flakeInputs
  • Ready-to-build source outputnix build .#generate runs every code generator and emits the module source + a fully-populated generated_code/ in result/
  • Built-in LGX packagingnix build .#lgx and nix build .#lgx-portable included automatically
  • Built-in install outputsnix build .#install and nix build .#install-portable bundle and install via lgpm in one step
  • Auto-detected UI integration tests — put .mjs test files in tests/ and get nix build .#integration-test for free

Documentation

Document Description
Getting Started Create your first module
Quick Reference Cheat sheet for common tasks
Configuration Reference Complete metadata.json specification
CMake Reference LogosModule.cmake functions
Nix API Reference mkLogosModule and other functions
External Libraries Guide Wrap C/C++ libraries
Migration Guide Migrate existing modules
Troubleshooting Common issues and solutions

Templates

Use nix flake init with our templates:

# Minimal core module (backend/logic, no UI)
nix flake init -t github:logos-co/logos-module-builder

# C++ UI module — view module with C++ backend + QML view (process-isolated)
nix flake init -t github:logos-co/logos-module-builder#ui-qml-backend

# QML-only UI module (no C++ backend, in-process)
nix flake init -t github:logos-co/logos-module-builder#ui-qml

# Module with external library
nix flake init -t github:logos-co/logos-module-builder#with-external-lib

AI Assistant Skills

For AI assistants (Claude, Cursor, etc.), we provide skill files:

Skill Description
create-logos-module Step-by-step guide to create a new core module
create-ui-module Create a ui_qml module with C++ backend + QML view (process-isolated)
create-qml-module Create a ui_qml module (QML-only, in-process)
update-logos-module Guide to update/modify existing modules

Testing

The builder has a pure Nix evaluation test suite (no compilation required). Tests cover metadata parsing, utility functions, external library helpers, and template validity.

# Run tests via nix
nix build '.#checks.x86_64-linux.default'

# Or use nix flake check (runs all checks for the current system)
nix flake check

# From the logos-workspace
ws test logos-module-builder

Tests are in tests/ and are organized into:

File What it tests
test-parse-metadata.nix metadata.json parsing, defaults, required fields, type coercion
test-common.nix Name formats, platform helpers, recursive merge, dependency collection
test-external-lib.nix External library detection, name extraction, vendor build scripts
test-templates.nix All 4 templates parse correctly, expected files exist, field consistency

Executable doc-tests

doctests/ holds step-by-step, runnable tutorials (run in CI by the Doc-Tests workflow via the shared doctest CLI, each building real modules against the commit under test):

  • wrap-external-lib-1…4 — the four ways an external C/C++ library can reach a module build (in-repo source, prebuilt binaries, external source built with make, an external Nix flake).
  • cross-language-composition — the C++ ↔ Rust feature-parity showcase: a contract-first C++ cdylib module, a Rust-first module (trait → .lidl), and a universal C++ consumer, with typed calls and a typed event crossing the language boundary in both directions.
  • cross-language-composition-reverse — the mirror image: contract-first Rust, a pure-C++ universal module in the middle (typed deps + logos_events: emission), and a Rust-first consumer subscribing to the C++ module's typed event. Between the two compositions, every authoring/consumption direction of the parity matrix is exercised.
  • ui-typed-backend — the universal authoring model for UI modules (type: "ui_qml" + interface: "universal"): you write the .rep (the view contract — SLOTs, PROPs, SIGNALs) and a *Backend class deriving <RepClass>SimpleSource + LogosUiPluginContext; the *Plugin/*Interface classes are generated. The backend gets typed dependency calls and typed event subscriptions (armed in onContextReady()), here feeding a .rep PROP that auto-syncs into QML.
  • cdylib-qt-free-outbound — a interface: "cdylib" C++ module calling its dependency through modules().<dep>... with no Qt in its own code: the generated typed wrappers call the logos-protocol lp_* C ABI directly (logos::LpClient), so Qt stays confined to the QRO transport and the plugin glue. A counter + a relay that forwards to it, driven through logoscore.

Run one locally:

nix run github:logos-co/logos-doctest -- run \
  doctests/cross-language-composition.test.yaml \
  --verbose --release-for logos-module-builder=<commit-to-test>

Architecture

logos-module-builder/
├── lib/                    # Nix library functions
│   ├── mkLogosModule.nix   # Builder for core + legacy UI widget modules
│   ├── mkLogosQmlModule.nix # Builder for ui_qml modules (QML view + optional C++ backend)
│   ├── buildCppPlugin.nix  # Shared C++ plugin build pipeline
│   ├── mkStandaloneApp.nix # apps.default for logos-standalone-app
│   ├── mkModuleLib.nix     # Library builder
│   ├── mkModuleInclude.nix # Header generator
│   ├── mkExternalLib.nix   # External library handler
│   └── parseMetadata.nix   # metadata.json parser
├── cmake/
│   └── LogosModule.cmake   # Reusable CMake module
├── templates/              # Module templates
├── docs/                   # Documentation
└── skills/                 # AI assistant skills

License

MIT

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages