mirror of
https://github.com/logos-co/logos-tutorial.git
synced 2026-08-30 12:21:12 +00:00
1828 lines
88 KiB
Markdown
1828 lines
88 KiB
Markdown
# Logos Module Developer Guide
|
||
|
||
A comprehensive guide to creating, building, testing, packaging, and distributing modules for the Logos platform.
|
||
|
||
## Table of Contents
|
||
|
||
- [Overview](#overview)
|
||
- [Architecture](#architecture)
|
||
- [Prerequisites](#prerequisites)
|
||
- [Part 1: Creating a Module](#part-1-creating-a-module)
|
||
- [1.1 Scaffold with logos-module-builder](#11-scaffold-with-logos-module-builder)
|
||
- [1.2 Project Structure](#12-project-structure)
|
||
- [1.3 The metadata.json Configuration](#13-the-metadatajson-configuration)
|
||
- [1.4 Writing Module Code](#14-writing-module-code)
|
||
- [1.5 Building Your Module](#15-building-your-module)
|
||
- [Part 2: Inspecting Your Module](#part-2-inspecting-your-module)
|
||
- [2.1 The lm CLI Tool](#21-the-lm-cli-tool)
|
||
- [2.2 The logos-module-viewer](#22-the-logos-module-viewer)
|
||
- [Part 3: Testing UI Modules](#part-3-testing-ui-modules)
|
||
- [3.1 How It Works](#31-how-it-works)
|
||
- [3.2 Writing Tests](#32-writing-tests)
|
||
- [3.3 Running Tests](#33-running-tests)
|
||
- [Part 4: Packaging Your Module](#part-4-packaging-your-module)
|
||
- [4.1 The LGX Package Format](#41-the-lgx-package-format)
|
||
- [4.2 Building LGX Packages](#42-building-lgx-packages)
|
||
- [Built-in Nix Derivation (Preferred)](#built-in-nix-derivation-preferred)
|
||
- [Using nix bundle (Alternative)](#using-nix-bundle-alternative)
|
||
- [Part 5: Installing and Managing Modules](#part-5-installing-and-managing-modules)
|
||
- [5.1 The lgpm CLI](#51-the-lgpm-cli)
|
||
- [5.2 Installing from Local Files](#52-installing-from-local-files)
|
||
- [5.3 Installing from a Registry](#53-installing-from-a-registry)
|
||
- [Part 6: Running Your Module](#part-6-running-your-module)
|
||
- [6.1 Running with logoscore](#61-running-with-logoscore)
|
||
- [Part 7: Running in logos-basecamp](#part-7-running-in-logos-basecamp)
|
||
- [7.1 Building logos-basecamp](#71-building-logos-basecamp)
|
||
- [7.2 Module Types in logos-basecamp](#72-module-types-in-logos-basecamp)
|
||
- [Part 8: Inter-Module Communication](#part-8-inter-module-communication)
|
||
- [8.1 The LogosAPI](#81-the-logosapi)
|
||
- [8.2 The C++ SDK Code Generator](#82-the-c-sdk-code-generator)
|
||
- [8.3 LogosResult](#83-logosresult)
|
||
- [8.4 Communication Modes](#84-communication-modes)
|
||
- [Part 9: Advanced Topics](#part-9-advanced-topics)
|
||
- [9.1 Tutorials](#91-tutorials)
|
||
- [9.2 Module Dependencies](#92-module-dependencies)
|
||
- [Reference: Repository Map](#reference-repository-map)
|
||
- [Reference: CLI Tools Summary](#reference-cli-tools-summary)
|
||
- [Troubleshooting](#troubleshooting)
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
The **Logos platform** is a modular application framework built in C++ on top of Qt 6. Applications are composed of dynamically loaded **modules** (plugins) that communicate via an IPC layer. The platform provides:
|
||
|
||
- **Process isolation** -- each module runs in its own host process (on desktop), communicating via Qt Remote Objects
|
||
- **Cross-platform support** -- macOS (arm64, x86_64) and Linux (arm64, x86_64)
|
||
- **A package format** (`.lgx`) for distributing modules with platform-specific variants
|
||
- **A desktop application shell** (`logos-basecamp`) with a sidebar, tabbed workspace, and plugin management UI
|
||
- **A CLI runtime** (`logoscore`) for running modules headlessly
|
||
|
||
## Architecture
|
||
|
||
```
|
||
+---------------------------------------------------------------+
|
||
| Application Layer |
|
||
| logos-basecamp (Desktop GUI) or logoscore (CLI Runtime) |
|
||
+---------------------------------------------------------------+
|
||
| | |
|
||
v v v
|
||
+---------------+ +------------------+ +------------------+
|
||
| Module A | | Module B | | Package Manager |
|
||
| (logos_host) | | (logos_host) | | Module |
|
||
+-------+-------+ +--------+---------+ +--------+---------+
|
||
| | |
|
||
| Qt Remote Objects (IPC) |
|
||
+--------------------------------------------+
|
||
|
|
||
+--------v---------+
|
||
| liblogos | (Core Runtime)
|
||
| logos-liblogos |
|
||
+--------+---------+
|
||
|
|
||
+--------v---------+
|
||
| logos-cpp-sdk | (SDK: LogosAPI,
|
||
| | Code Generator,
|
||
| | Types, IPC)
|
||
+------------------+
|
||
```
|
||
|
||
**Key components:**
|
||
|
||
| Component | Repository | Role |
|
||
| ---------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||
| **logos-module-builder** | [logos-co/logos-module-builder](https://github.com/logos-co/logos-module-builder) | Scaffolding and build system for new modules |
|
||
| **logos-module** | [logos-co/logos-module](https://github.com/logos-co/logos-module) | Plugin loading/introspection library + `lm` CLI |
|
||
| **logos-cpp-sdk** | [logos-co/logos-cpp-sdk](https://github.com/logos-co/logos-cpp-sdk) | C++ SDK, types, IPC layer, code generator |
|
||
| **logos-liblogos** | [logos-co/logos-liblogos](https://github.com/logos-co/logos-liblogos) | Core library (`logos_host`, `liblogos_core`) |
|
||
| **logos-logoscore-cli** | [logos-co/logos-logoscore-cli](https://github.com/logos-co/logos-logoscore-cli) | Headless CLI runtime (`logoscore`) |
|
||
| **logos-package** | [logos-co/logos-package](https://github.com/logos-co/logos-package) | LGX package format library + `lgx` CLI |
|
||
| **logos-package-manager** | [logos-co/logos-package-manager](https://github.com/logos-co/logos-package-manager) | Local package manager library + `lgpm` CLI |
|
||
| **logos-package-downloader** | [logos-co/logos-package-downloader](https://github.com/logos-co/logos-package-downloader) | Online catalog browser + `lgpd` CLI |
|
||
| **logos-standalone-app** | [logos-co/logos-standalone-app](https://github.com/logos-co/logos-standalone-app) | Minimal shell for running/testing UI modules in isolation |
|
||
| **logos-basecamp** | [logos-co/logos-basecamp](https://github.com/logos-co/logos-basecamp) | Desktop application shell |
|
||
|
||
## Prerequisites
|
||
|
||
### Required
|
||
|
||
- **Nix** with flakes enabled. This is the primary build tool for the entire ecosystem. Install Nix from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||
|
||
```bash
|
||
# If you need experimental features enabled per-command:
|
||
nix --extra-experimental-features "nix-command flakes" <command>
|
||
|
||
# Or enable globally in ~/.config/nix/nix.conf:
|
||
experimental-features = nix-command flakes
|
||
```
|
||
|
||
### Recommended Knowledge
|
||
|
||
- C++ (C++17)
|
||
- Qt 6 basics (`QObject`, `Q_INVOKABLE`, `Q_PLUGIN_METADATA`, signals/slots)
|
||
- Basic CMake
|
||
- Basic Nix concepts (flakes, derivations)
|
||
|
||
---
|
||
|
||
## Part 1: Creating a Module
|
||
|
||
### 1.1 Scaffold with logos-module-builder
|
||
|
||
The fastest way to create a new module is using the **logos-module-builder** template:
|
||
|
||
```bash
|
||
# Create a new directory for your module
|
||
mkdir logos-my-module && cd logos-my-module
|
||
|
||
# Scaffold a minimal core module (no external dependencies)
|
||
nix flake init -t github:logos-co/logos-module-builder
|
||
|
||
# Or scaffold a module that wraps an external C/C++ library
|
||
nix flake init -t github:logos-co/logos-module-builder#with-external-lib
|
||
|
||
# For ui_qml modules with C++ backend (process-isolated)
|
||
nix flake init -t github:logos-co/logos-module-builder#ui-qml-backend
|
||
|
||
# For ui_qml modules (QML-only, no C++)
|
||
nix flake init -t github:logos-co/logos-module-builder#ui-qml
|
||
```
|
||
|
||
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. For reproducible builds, pin it to a specific commit — see the `flake.nix` examples in [Section 3.2](#32-building-lgx-packages) and the [tutorials](tutorial-wrapping-c-library.md#23-flakenix--nix-build-config).
|
||
|
||
**Available templates:**
|
||
|
||
| Template | Use Case |
|
||
| ------------------- | ----------------------------------------------------- |
|
||
| `default` | Minimal core module (C++ backend, no UI) |
|
||
| `with-external-lib` | Core module wrapping an external C/C++ library |
|
||
| `ui-qml-backend` | ui_qml with C++ backend + QML view (process-isolated) |
|
||
| `ui-qml` | ui_qml QML-only (in-process, no C++) |
|
||
|
||
The `ui-qml-backend` and `ui-qml` templates automatically enable `nix run` to launch and test your UI plugin in isolation without the full logos-basecamp shell. The standalone app runner is bundled with `logos-module-builder` — no extra flake input is needed. All module dependencies declared in `metadata.json` are auto-bundled from their LGX packages.
|
||
|
||
This generates a ready-to-build project with all the boilerplate handled for you.
|
||
|
||
### 1.2 Project Structure
|
||
|
||
> We will use the recommended **pure-C++ pattern** (`"interface": "universal"`) for a core module. The scaffolding templates currently emit the older Qt-plugin layout; you replace their `src/` files with the two `*_impl` files shown here (see [Section 1.4](#14-understanding-the-module-code)).
|
||
|
||
A pure-C++ core module looks like this:
|
||
|
||
```
|
||
logos-my-module/
|
||
├── flake.nix # Nix flake (build config, ~15 lines)
|
||
├── metadata.json # Single source of truth: module metadata + build config (~30 lines)
|
||
├── CMakeLists.txt # CMake build file (~25 lines)
|
||
└── src/
|
||
├── my_module_impl.h # Plain C++ class — no Qt
|
||
└── my_module_impl.cpp # Implementation
|
||
```
|
||
|
||
The key insight: **logos-module-builder** reduces ~600 lines of configuration across 5+ files down to ~70 lines across 2-3 files, and the `universal` pattern collapses the three hand-written Qt source files into one plain C++ class. `metadata.json` serves as the single source of truth — it contains both the runtime metadata (embedded into the generated plugin binary) and the build configuration (read by the builder via the `nix` section).
|
||
|
||
The `CMakeLists.txt` is minimal -- it includes `LogosModule.cmake` (provided by the builder) and calls the `logos_module()` macro, which sets up the plugin target, runs `logos-cpp-generator` for `universal` modules, links the SDK, configures include paths, and compiles the generated glue. You just list your `*_impl` source files. See the [C-library tutorial](tutorial-wrapping-c-library.md#step-3-configure-the-logos-module) for a complete `CMakeLists.txt`.
|
||
|
||
### 1.3 The metadata.json Configuration
|
||
|
||
The `metadata.json` file is the single source of truth for your module. It is embedded into the generated plugin binary (for runtime metadata, read by `lm`), read by `logos-module-builder` to configure the Nix build, used by CMake to resolve external dependencies and link libraries (via the `nix` section), and used by `nix-bundle-lgx` to generate the LGX manifest. See the scaffolded [`metadata.json`](https://github.com/logos-co/logos-module-builder/blob/master/templates/minimal-module/metadata.json) in the template.
|
||
|
||
The full set of available fields:
|
||
|
||
```json
|
||
{
|
||
"name": "my_module",
|
||
"display_name": "My Module",
|
||
"version": "1.0.0",
|
||
"type": "core",
|
||
"category": "general",
|
||
"description": "My first Logos module",
|
||
"icon": "src/icons/my_module.png",
|
||
"main": "my_module_plugin",
|
||
"interface": "universal",
|
||
"dependencies": [],
|
||
"provides": [],
|
||
"uses": [],
|
||
"include": [],
|
||
|
||
"nix": {
|
||
"packages": {
|
||
"build": [],
|
||
"runtime": []
|
||
},
|
||
"external_libraries": [],
|
||
"cmake": {
|
||
"find_packages": [],
|
||
"extra_sources": [],
|
||
"extra_include_dirs": [],
|
||
"extra_link_libraries": []
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Field reference:**
|
||
|
||
| Field | Required | Default | Description |
|
||
| -------------------------------- | -------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `name` | Yes | -- | Module name (used for filenames and identifiers) |
|
||
| `display_name` | No | `name` | Human-readable label shown in UIs (Package Manager, App Manager, `lm metadata`, `lgx manifest`). Consumers fall back to `name` when unset, so older packages keep working. |
|
||
| `version` | No | `1.0.0` | Semantic version |
|
||
| `type` | No | `core` | Module type (`core`, `ui`, `ui_qml`) |
|
||
| `category` | No | `general` | Category (general, network, chat, wallet, integration) |
|
||
| `description` | No | `"A Logos module"` | Human-readable description |
|
||
| `icon` | No | `null` | Relative path to the module icon. **PNG, exactly 256x256.** Required for `ui_qml` modules (manifest 0.4.0+), optional for `core`. Bundled once at `assets/icon.png` inside the `.lgx` so hosts can show it before install; also copied into the standalone app plugin directory. Convention: `src/icons/<module_name>.png`. |
|
||
| `main` | Yes (`core`/`ui`), optional (`ui_qml`) | -- | Plugin entry point. For `core`/`ui` modules: plugin name without extension (the generated `<name>_plugin`). For `ui_qml`: optional backend plugin name (omit if QML-only). |
|
||
| `interface` | No | -- | Set to `"universal"` for the pure-C++ pattern: you write a plain `src/<name>_impl.h`/`.cpp` and the builder runs `logos-cpp-generator --from-header` to synthesize the Qt plugin. Omit for the older hand-written Qt-plugin pattern. |
|
||
| `concurrency` | No | `"single"` | Dispatch mode. `"single"` (default): calls to this module are dispatched one at a time (event-loop semantics) — you need no thread-safety. `"multi"`: handlers run **concurrently** on a worker pool, so one blocking handler (a slow download, a slow RPC) no longer stalls other callers — but **you** own thread-safety. See [§1.6 Concurrent dispatch](#16-concurrent-dispatch). |
|
||
| `view` | Yes (`ui_qml`) | -- | Relative path to the QML entry file (e.g. `Main.qml`). Required for `ui_qml` modules. |
|
||
| `dependencies` | No | `[]` | Other Logos module names this depends on. Each entry must match the `name` field in that dependency's `metadata.json`. |
|
||
| `provides` | No | `[]` | Intents this module can service, as an **array of objects**: `[{"intent": "chat.group.open"}]`. Each entry may also carry `params` describing the payload it expects, which the shell enforces before dispatch — see §8.5. Intent **names** are carried into the signed `.lgx` manifest (0.5.0+) so a catalog can answer "which installable package provides X?"; `params` stays here, in `metadata.json`, which is the copy the shell reads. See §8.5. |
|
||
| `uses` | No | `[]` | Intents this module may request, as an **array of objects**: `[{"intent": "wallet.sign", "cardinality": "single"}]`. Mandatory to request one — an undeclared request fails `not_declared`. `cardinality` is optional; only `single` is accepted today (`all` is reserved). ⚠ A bare string array is silently ignored — see §8.5. |
|
||
| `interface_dependencies` | No | `[]` | Header *interfaces* this module binds at runtime, decoupled from any concrete module. Each entry is `{ name, file, impl_class?, input? }` — see [Dependency interfaces](#dependency-interfaces) and the [tutorial](tutorial-interface-dependencies.md). |
|
||
| `dependency_overrides` | No | `{}` | Per-dependency LIDL-contract source overrides, keyed by dependency name → `{ file, input?, impl_class? }`. Forces where a dependency's interface is read from; normally auto-resolved from the dep's `lidl` output. See [§9.2 Module Dependencies](#92-module-dependencies). |
|
||
| `include` | No | `[]` | Additional files (e.g. shared libraries like `libwaku.so`, `libwaku.dylib`) to bundle alongside the plugin in the output. |
|
||
| `nix.packages.build` | No | `[]` | Nix packages for build time |
|
||
| `nix.packages.runtime` | No | `[]` | Nix packages for runtime |
|
||
| `nix.external_libraries` | No | `[]` | External C/C++ libraries to wrap. Each entry is an object — see [configuration reference](https://github.com/logos-co/logos-module-builder/blob/master/docs/configuration.md#nixexternal_libraries) for fields (`name`, `vendor_path`, `build_command`, etc.). |
|
||
| `nix.cmake.find_packages` | No | `[]` | CMake `find_package()` calls |
|
||
| `nix.cmake.extra_sources` | No | `[]` | Additional source files to compile |
|
||
| `nix.cmake.extra_include_dirs` | No | `[]` | Additional include directories |
|
||
| `nix.cmake.extra_link_libraries` | No | `[]` | Additional libraries to link |
|
||
|
||
### 1.4 Understanding the Module Code
|
||
|
||
The recommended way to write a core module is the **pure-C++ pattern** (`"interface": "universal"` in `metadata.json`). You write a single plain C++ class — `src/<name>_impl.h` and `src/<name>_impl.cpp` — with **no Qt, no `Q_OBJECT`, no `Q_PLUGIN_METADATA`, no interface header**. At build time `logos-cpp-generator --from-header` parses your header and generates the Qt plugin wrapper, the interface, and the inter-module glue into `generated_code/`. You never see or edit that generated code.
|
||
|
||
A minimal impl class looks like this:
|
||
|
||
```cpp
|
||
// src/my_module_impl.h
|
||
#pragma once
|
||
|
||
#include <cstdint>
|
||
#include <string>
|
||
|
||
#include <logos_module_context.h> // optional: events + inter-module calls
|
||
|
||
class MyModuleImpl : public LogosModuleContext {
|
||
public:
|
||
// Every public method is exposed: discoverable by `lm`, callable by
|
||
// `logoscore call`, and reachable from other modules.
|
||
std::string greet(const std::string& name);
|
||
int64_t add(int64_t a, int64_t b);
|
||
|
||
// Events are declared like Qt signals. The generator emits the body;
|
||
// calling the method delivers the typed args to subscribers.
|
||
logos_events:
|
||
void greeted(const std::string& name);
|
||
};
|
||
```
|
||
|
||
```cpp
|
||
// src/my_module_impl.cpp
|
||
#include "my_module_impl.h"
|
||
|
||
std::string MyModuleImpl::greet(const std::string& name) {
|
||
greeted(name); // emit the event
|
||
return "Hello, " + name + "!";
|
||
}
|
||
|
||
int64_t MyModuleImpl::add(int64_t a, int64_t b) { return a + b; }
|
||
```
|
||
|
||
**How it works:**
|
||
|
||
1. **Any `public` method is exposed** — discoverable by `lm`, callable by `logoscore call`, and accessible from other modules. `private` members are not.
|
||
2. **Use the supported types** so the generator can translate them onto the wire: `void`, `bool`, `int64_t`, `uint64_t`, `double`, `std::string`, `std::vector<std::string>`, `std::vector<uint8_t>`, `LogosMap`/`LogosList` (from `<logos_json.h>`), and `StdLogosResult` (from `<logos_result.h>`). Use `int64_t` for integers, not `int`.
|
||
3. **Events** are declared in a `logos_events:` section (the class must inherit `LogosModuleContext`). Calling the event method routes the typed args to subscribers via the host's `eventResponse` channel — outside a host (unit tests) it's a safe no-op.
|
||
4. **Inter-module calls** also go through `LogosModuleContext`: from a method body, `modules().other_module.someMethod(arg)` calls another module using std types, with no raw `LogosAPI` and no Qt. Declare the dependency in `metadata.json`'s `dependencies` and as a flake input.
|
||
|
||
You do **not** write `initLogos`, `name()`/`version()` (read from `metadata.json`), `Q_INVOKABLE`, or the `eventResponse` signal — all are generated. `name()` is taken from `metadata.json`'s `name`, so they can never drift out of sync.
|
||
|
||
> **Older Qt-plugin pattern.** As of this writing the scaffolding templates still emit a hand-written Qt plugin (`*_interface.h` + `*_plugin.h` + `*_plugin.cpp` with `QObject`, `Q_PLUGIN_METADATA`, `Q_INVOKABLE`, and an `initLogos(LogosAPI*)` you store). That pattern still builds and is what `ui_qml` C++ backends use (see [Building a C++ UI Module](tutorial-cpp-ui-app.md)). For a new core module, prefer the pure-C++ pattern above — replace the template's `src/` files with your `*_impl.h`/`*_impl.cpp` and add `"interface": "universal"` to `metadata.json`. The [C-library tutorial](tutorial-wrapping-c-library.md) walks through this end to end.
|
||
|
||
### 1.5 Building Your Module
|
||
|
||
```bash
|
||
# Nix requires all source files to be tracked by git
|
||
git init && git add -A
|
||
|
||
# Build everything (library + generated SDK headers)
|
||
nix build
|
||
|
||
# Build just the plugin shared library (.so / .dylib)
|
||
nix build .#lib
|
||
|
||
# Build just the generated SDK headers (for other modules to use)
|
||
nix build .#include
|
||
|
||
# Emit a ready-to-build codebase: runs every code generator that is part of the
|
||
# build and writes the module source + a fully-populated generated_code/ to
|
||
# result/. Build it from `nix develop` without re-running any generator.
|
||
nix build .#generate
|
||
|
||
# Enter the dev shell for manual CMake builds (see: https://nix.dev/tutorials/first-steps/dev-environment)
|
||
# The shell provides cmake, ninja, Qt, the Logos SDK, and all build dependencies.
|
||
nix develop
|
||
cmake -B build -GNinja && cmake --build build
|
||
```
|
||
|
||
**Build outputs:**
|
||
|
||
```
|
||
result/
|
||
├── lib/
|
||
│ └── my_module_plugin.so # (or .dylib on macOS)
|
||
└── include/
|
||
├── my_module_api.h # Generated type-safe wrapper header
|
||
└── my_module_api.cpp # Generated wrapper implementation
|
||
```
|
||
|
||
---
|
||
|
||
### 1.6 Concurrent dispatch
|
||
|
||
By default every call to a module is dispatched **one at a time** — the module's
|
||
methods run on a single thread (the event loop), so you never have to think about
|
||
thread-safety. This is the right default and stays the default. The downside: a
|
||
handler that **blocks** — a download that runs for minutes, a slow RPC — stalls
|
||
*every other caller* of that module until it returns.
|
||
|
||
Set **`"concurrency": "multi"`** in `metadata.json` to opt that module into
|
||
**concurrent dispatch**: each incoming call runs on its own worker, so a blocking
|
||
handler no longer holds up the others (e.g. a downloader can serve two downloads
|
||
at once). In exchange, **you own thread-safety** — your handlers run in parallel,
|
||
so any state they share must be synchronized.
|
||
|
||
The generated code enforces the contract differently per language:
|
||
|
||
- **Rust** (`interface: "cdylib"`, rust-first). In `multi` mode the generated
|
||
trait takes **`&self`** (not `&mut self`) and is **`Send + Sync`**, and the
|
||
instance is shared behind an `Arc`. The compiler makes the rule unavoidable: a
|
||
handler that mutates a plain field won't build — wrap mutated state in interior
|
||
mutability (`Mutex`, `RwLock`, `Atomic*`, `DashMap`, …). `on_context_ready`
|
||
also becomes `&self`.
|
||
|
||
```rust
|
||
pub trait Downloader: Send + Sync + 'static {
|
||
fn fetch(&self, url: String) -> String; // &self — runs concurrently
|
||
}
|
||
#[derive(Default)]
|
||
struct Impl { jobs: std::sync::Mutex<Vec<String>> } // guard shared state
|
||
```
|
||
|
||
- **C++** (`interface: "universal"` / `"cdylib"`). In `multi` mode each call runs
|
||
on a worker thread, so your impl's methods may execute concurrently — treat them
|
||
as re-entrant and guard any shared members (`std::atomic`, `std::mutex`). The
|
||
`LogosModuleContext` accessors and the event-emit path are already thread-safe.
|
||
|
||
**How it works (and its limits).** `multi` is realized **entirely by the code
|
||
generator** — there is no new transport and, crucially, **no change to the
|
||
provider/host ABI**. A `multi` module's generated glue does not block in its
|
||
dispatch entry point: it hands the handler to a worker and immediately returns a
|
||
small *pending* marker, then pushes the real result back as a completion event
|
||
once the worker finishes. The consumer side awaits that completion transparently,
|
||
so generated clients are unchanged. The decomposition is *serialized dispatch +
|
||
concurrent processing + serialized responses*, and it works over the default
|
||
transport (QtRO) as well as the plain transport. Because the host merely forwards
|
||
the marker and the completion, **an existing (older) daemon or app loads and runs
|
||
a `multi` module unmodified** — this is logos-protocol **0.2**, an additive,
|
||
backward-compatible minor bump (same MAJOR ⇒ still compatible). Caveats: (1) a
|
||
`single` module is unchanged and pays zero overhead; (2) events your handlers emit
|
||
are delivered safely but their order **relative to method replies is not
|
||
guaranteed** — don't rely on "event X always arrives before method Y returns";
|
||
(3) a *caller* built against logos-protocol < 0.2 will see the raw pending marker
|
||
instead of the result — rebuild callers against ≥ 0.2 (still compatible with every
|
||
existing module) to consume a `multi` module concurrently.
|
||
|
||
---
|
||
|
||
## Part 2: Inspecting Your Module
|
||
|
||
### 2.1 The `lm` CLI Tool
|
||
|
||
The **`lm`** tool (from `logos-module`) lets you inspect compiled module binaries without loading them into the full runtime. It reads metadata and enumerates methods via Qt's meta-object system.
|
||
|
||
#### Building lm
|
||
|
||
```bash
|
||
nix build 'github:logos-co/logos-module#lm' --out-link ./lm
|
||
```
|
||
|
||
#### Viewing Metadata
|
||
|
||
```bash
|
||
# Human-readable metadata
|
||
./lm/bin/lm metadata ./result/lib/my_module_plugin.so
|
||
|
||
# JSON output
|
||
./lm/bin/lm metadata ./result/lib/my_module_plugin.so --json
|
||
```
|
||
|
||
Example JSON output:
|
||
|
||
```json
|
||
{
|
||
"name": "my_module",
|
||
"display_name": "My Module",
|
||
"version": "1.0.0",
|
||
"description": "My first Logos module",
|
||
"author": "",
|
||
"type": "core",
|
||
"dependencies": []
|
||
}
|
||
```
|
||
|
||
`display_name` is omitted when unset; consumers fall back to `name`.
|
||
|
||
#### Viewing Methods
|
||
|
||
```bash
|
||
# Human-readable method list
|
||
./lm/bin/lm methods ./result/lib/my_module_plugin.so
|
||
|
||
# JSON output
|
||
./lm/bin/lm methods ./result/lib/my_module_plugin.so --json
|
||
```
|
||
|
||
**The type names depend on which kind of module you are inspecting**, because
|
||
two different things publish this JSON:
|
||
|
||
- A **universal / cdylib module** (`"interface": "universal"`, the style used
|
||
throughout this guide and in the tutorials) publishes its **LIDL contract**
|
||
types — `tstr`, `int`, `uint`, `bstr`, `[tstr]`, `{tstr: any}`, `? uint`,
|
||
`result`, and a record's declared name. The module is Qt-free, so the
|
||
contract is the only vocabulary in which the question has one answer, and
|
||
a Rust module implementing the same contract answers identically.
|
||
- A **handwritten Qt plugin** publishes what its `QMetaObject` says — `QString`,
|
||
`QVariantList`, `QVariantMap` — because there the metaobject *is* the
|
||
contract.
|
||
|
||
Example JSON output, for a universal module with
|
||
`method doSomething(input: tstr) -> tstr`:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"name": "doSomething",
|
||
"signature": "doSomething(tstr)",
|
||
"returnType": "tstr",
|
||
"isInvokable": true,
|
||
"parameters": [{ "name": "input", "type": "tstr" }]
|
||
},
|
||
{
|
||
"name": "name",
|
||
"signature": "name()",
|
||
"returnType": "tstr",
|
||
"isInvokable": true,
|
||
"description": "The module's name, as declared in its metadata."
|
||
}
|
||
]
|
||
```
|
||
|
||
`name` and `version` are **derived**: the generator emits them from
|
||
`metadata.json`, so every module answers them without the author writing them,
|
||
and they appear in every listing.
|
||
|
||
The same listing from a handwritten Qt plugin would instead read:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"name": "doSomething",
|
||
"signature": "doSomething(QString)",
|
||
"returnType": "QString",
|
||
"isInvokable": true,
|
||
"parameters": [{ "name": "input", "type": "QString" }]
|
||
}
|
||
]
|
||
```
|
||
|
||
### 2.2 The logos-module-viewer
|
||
|
||
The **logos-module-viewer** is a graphical tool for inspecting loaded modules.
|
||
|
||
```bash
|
||
# Build the viewer
|
||
nix build 'github:logos-co/logos-module-viewer#app' --out-link ./logos-viewer
|
||
|
||
# Run it with your module
|
||
./logos-viewer/bin/logos-module-viewer -m ./result/lib/my_module_plugin.so
|
||
```
|
||
|
||
This opens a window showing the module's metadata, methods, and allows interactive method invocation.
|
||
|
||
---
|
||
|
||
## Part 3: Testing UI Modules
|
||
|
||
For `ui_qml` modules (both QML-only and C++ backend), `logos-module-builder` provides automatic integration testing using the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) QML inspector.
|
||
|
||
### 3.1 How It Works
|
||
|
||
The test infrastructure has three layers:
|
||
|
||
1. **QML Inspector** — a TCP server compiled into `logos-standalone-app` that exposes the QML object tree
|
||
2. **MCP Server** — a Node.js bridge that translates test commands into inspector calls
|
||
3. **Test Framework** — a JavaScript API for writing UI assertions (`expectTexts`, `click`, `waitFor`, etc.)
|
||
|
||
When you run `nix build .#integration-test`, the builder:
|
||
|
||
- Launches `logos-standalone-app` with your plugin in headless mode (`QT_QPA_PLATFORM=offscreen`)
|
||
- Connects to the QML inspector
|
||
- Runs all `.mjs` test files in your `tests/` directory
|
||
|
||
### 3.2 Writing Tests
|
||
|
||
Create `.mjs` files in `tests/`. Each file imports the test framework and defines test cases:
|
||
|
||
```javascript
|
||
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" },
|
||
);
|
||
});
|
||
|
||
test("my_module: click button", async (app) => {
|
||
await app.click("Submit");
|
||
await app.expectTexts(["Result:"]);
|
||
});
|
||
|
||
run();
|
||
```
|
||
|
||
Key test APIs:
|
||
|
||
- `app.expectTexts(["text1", "text2"])` — assert text is visible in the UI
|
||
- `app.click("Button Text")` — find an element by text and click it
|
||
- `app.waitFor(fn, opts)` — retry an assertion until it passes or times out
|
||
- `app.screenshot()` — capture the current UI state (returns a base64 PNG; write it to a file to embed it in docs)
|
||
|
||
> In the executable-tutorial specs (`tests/*.test.yaml`), you don't call `app.screenshot()` directly — add a `screenshot: "name.png"` field to any UI-test action and the runner captures the headless app to `outputs/images/` and embeds it in the generated tutorial. See [`docs/spec.md`](docs/spec.md).
|
||
|
||
### 3.3 Running Tests
|
||
|
||
```bash
|
||
# Hermetic CI test (builds everything, no display needed)
|
||
nix build .#integration-test -L
|
||
|
||
# Interactive: build the test framework locally (one-time)
|
||
nix build .#test-framework -o result-mcp
|
||
|
||
# Start the app (inspector listens on localhost:3768)
|
||
nix run .
|
||
|
||
# Run tests against the running app (in another terminal)
|
||
node tests/ui-tests.mjs
|
||
```
|
||
|
||
Multiple test files in `tests/` are discovered and run automatically. You can organize tests by concern (e.g., `tests/smoke.mjs`, `tests/interactions.mjs`).
|
||
|
||
> **Note:** The integration test infrastructure requires `logos-standalone-app` with QML inspector support. This is provided automatically by `logos-module-builder` — no extra flake inputs needed.
|
||
|
||
---
|
||
|
||
## Part 4: Packaging Your Module
|
||
|
||
Before you can run your module with `logoscore` or install it into `logos-basecamp`, you need to package the build output into an `.lgx` package and install it into a `modules/` directory.
|
||
|
||
### 4.1 The LGX Package Format
|
||
|
||
Logos modules are distributed as **`.lgx` packages**. An LGX file is a gzip-compressed tar archive with a specific internal structure:
|
||
|
||
```
|
||
mymodule.lgx (tar.gz)
|
||
├── manifest.json # Package metadata
|
||
├── variants/
|
||
│ ├── linux-amd64/
|
||
│ │ └── my_module_plugin.so
|
||
│ ├── darwin-arm64/
|
||
│ │ └── my_module_plugin.dylib
|
||
│ └── darwin-arm64-dev/
|
||
│ └── my_module_plugin.dylib
|
||
├── docs/ # Optional
|
||
└── licenses/ # Optional
|
||
```
|
||
|
||
The **manifest.json** is auto-generated from your module's `metadata.json` by the bundler. It maps each variant to its main entry point.
|
||
|
||
It is not a copy of `metadata.json`. The bundler projects a fixed set of fields
|
||
across — including `name`, `version`, `type`, `dependencies`, `view`, `icon` and
|
||
`provides` — and anything else stays behind in `metadata.json`. Two consequences
|
||
worth knowing:
|
||
|
||
- **`manifest.json` is signed; `metadata.json` is not.** The signature covers
|
||
the manifest bytes, so whatever reaches the manifest is attested by whoever
|
||
signed the package.
|
||
- **`provides` is carried; `uses` is not.** A catalog needs to know what an
|
||
uninstalled package *can do* to suggest it; nobody outside the shell needs to
|
||
know what it *wants to call*.
|
||
|
||
`manifestVersion` tracks the manifest schema, separately from your module's
|
||
`version`:
|
||
|
||
| Schema | Adds |
|
||
| --- | --- |
|
||
| `0.2.x` | plain-string dependencies |
|
||
| `0.3.x` | dependency version ranges + signer DIDs |
|
||
| `0.4.x` | root-level `assets/icon.png` (the 256×256 PNG contract) |
|
||
| `0.5.x` | `provides` |
|
||
|
||
Every addition so far has been an **optional** field, so a client reading a newer
|
||
manifest ignores what it does not recognise rather than refusing the package.
|
||
That is why a package built before 0.5.0 simply has no `provides` — and why the
|
||
version was bumped rather than reused, so "declares no intents" stays
|
||
distinguishable from "predates the field".
|
||
|
||
### 4.2 Building LGX Packages
|
||
|
||
There are two ways to create `.lgx` packages. The preferred approach uses the built-in Nix derivation that comes with `logos-module-builder`. Alternatively, you can use the `nix bundle` command directly.
|
||
|
||
#### Built-in Nix Derivation (Preferred)
|
||
|
||
When your module uses `logos-module-builder`, LGX package outputs are automatically available as part of your flake (the builder includes `nix-bundle-lgx` internally):
|
||
|
||
```bash
|
||
# Dev variant (uses /nix/store references, for local development)
|
||
nix build .#lgx
|
||
|
||
# Portable variant (self-contained, all dependencies bundled)
|
||
nix build .#lgx-portable
|
||
|
||
```
|
||
|
||
This produces a `my_module-<version>.lgx` file in the `result/` directory.
|
||
|
||
This works because `logos-module-builder` includes `nix-bundle-lgx` as its own dependency and both `mkLogosModule` and `mkLogosQmlModule` automatically create the `lgx` and `lgx-portable` package outputs. No extra configuration is needed — it is part of the standard module template:
|
||
|
||
```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;
|
||
};
|
||
}
|
||
```
|
||
|
||
#### Using `nix bundle` (Alternative)
|
||
|
||
You can also create `.lgx` packages using the `nix bundle` command directly. This is useful if your module does not use `logos-module-builder`, or if you need the `dual` bundling mode (both dev and portable in a single `.lgx` file) which is only available via the `nix bundle` command:
|
||
|
||
```bash
|
||
# Dev variant
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx .#lib
|
||
|
||
# Portable variant
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx#portable .#lib
|
||
|
||
# Dual variant (both dev and portable in one .lgx file)
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx#dual .#lib
|
||
```
|
||
|
||
This produces a `my_module-<version>.lgx` file in the current directory.
|
||
|
||
**Bundling modes:**
|
||
|
||
| Mode | Built-in Command | `nix bundle` Command | Variant Created | Use Case |
|
||
| ------------ | ----------------------------- | ----------------------------------------- | --------------------- | -------------------------------------------- |
|
||
| **Dev** | `nix build .#lgx` | `nix bundle --bundler ...#default .#lib` | `darwin-arm64-dev` | Local development (requires Nix store) |
|
||
| **Portable** | `nix build .#lgx-portable` | `nix bundle --bundler ...#portable .#lib` | `darwin-arm64` | Distribution (self-contained, no Nix needed) |
|
||
| **Dual** | _(not available as built-in)_ | `nix bundle --bundler ...#dual .#lib` | Both dev and portable | One package for both environments |
|
||
|
||
**Variant naming:**
|
||
|
||
| Nix System | Dev Variant | Portable Variant |
|
||
| ---------------- | ------------------ | ---------------- |
|
||
| `aarch64-darwin` | `darwin-arm64-dev` | `darwin-arm64` |
|
||
| `x86_64-darwin` | `darwin-amd64-dev` | `darwin-amd64` |
|
||
| `aarch64-linux` | `linux-arm64-dev` | `linux-arm64` |
|
||
| `x86_64-linux` | `linux-amd64-dev` | `linux-amd64` |
|
||
| `x86_64-windows` | `windows-x86_64-dev` | `windows-x86_64` |
|
||
|
||
> **Important:** The variant type matters when installing into `logos-basecamp`. A dev build of basecamp expects dev variants, and a portable build expects portable variants. Use the `dual` bundler to produce packages that work with both.
|
||
|
||
> **Windows is cross-built only.** `x86_64-windows` is a pseudo-system: there is no Nix daemon for Windows, so the package is produced on a Linux (or macOS) machine targeting `x86_64-w64-mingw32` and copied across. Note the variant is spelled `windows-x86_64`, not `windows-amd64` — unlike Linux, it has no alias, so a package labelled `windows-amd64` will not install.
|
||
|
||
---
|
||
|
||
## Part 5: Installing and Managing Modules
|
||
|
||
### 5.1 The `lgpm` CLI
|
||
|
||
The **`lgpm`** CLI (Logos Package Manager) installs, searches, and manages module packages. Installing a package extracts it into a `modules/` directory that `logoscore` and `logos-basecamp` can load from.
|
||
|
||
#### Building lgpm
|
||
|
||
```bash
|
||
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./package-manager
|
||
```
|
||
|
||
#### Commands
|
||
|
||
`lgpm` manages **locally-available** `.lgx` packages. It does not download packages from the network — use `lgpd` (logos-package-downloader) for that.
|
||
|
||
```bash
|
||
# Install from a local .lgx file
|
||
./package-manager/bin/lgpm --modules-dir ./modules install --file ./my_module.lgx
|
||
|
||
# Install all .lgx files in a directory
|
||
./package-manager/bin/lgpm --modules-dir ./modules install --dir ./packages/
|
||
|
||
# List installed packages
|
||
./package-manager/bin/lgpm --modules-dir ./modules list
|
||
|
||
# Show installed package details
|
||
./package-manager/bin/lgpm --modules-dir ./modules info my_module
|
||
```
|
||
|
||
#### Global Options
|
||
|
||
| Option | Description |
|
||
| ------------------------- | ------------------------------------------- |
|
||
| `--modules-dir <path>` | Target directory for installed core modules |
|
||
| `--ui-plugins-dir <path>` | Target directory for UI plugins |
|
||
| `--json` | Output in JSON format |
|
||
| `--platform <variant>` | Install for a platform other than this machine (see below) |
|
||
| `-h, --help` | Show help |
|
||
|
||
#### Installing for another platform
|
||
|
||
By default `lgpm` derives the variant from the machine it is running on, and
|
||
**refuses** a package that does not provide it:
|
||
|
||
```
|
||
Error: Package does not contain variant for platform: linux-x86_64-dev
|
||
(package provides: windows-x86_64-dev)
|
||
```
|
||
|
||
That refusal is the protection against installing a package built for one
|
||
platform onto another, so it is deliberately fail-closed. Cross-building needs an
|
||
explicit opt-out — the Nix install bundler, for instance, runs `lgpm` on a Linux
|
||
builder to lay out a Windows package:
|
||
|
||
```bash
|
||
lgpm --modules-dir ./modules install --platform windows-x86_64 --file ./my_module.lgx
|
||
```
|
||
|
||
`--platform` applies to `install`, `list` and `info` alike, so all three agree on
|
||
which platform is being managed, and `lgpm` prints the override to stderr when it
|
||
is in effect — a silent platform switch would defeat the very check it bypasses.
|
||
|
||
> **Do not reach for `--platform` to resolve a dev/portable variant mismatch.**
|
||
> If a package provides `darwin-arm64` and your basecamp wants
|
||
> `darwin-arm64-dev`, the fix is to build the right variant (or use the `dual`
|
||
> bundler), not to override the platform — forcing it installs a package the
|
||
> runtime cannot load, turning a clear install-time error into a confusing
|
||
> load-time one.
|
||
|
||
### 5.2 Installing from Local Files
|
||
|
||
```bash
|
||
# Install a locally built .lgx package into a modules/ directory
|
||
./package-manager/bin/lgpm --modules-dir ./modules install --file ./my_module-1.0.0.lgx
|
||
```
|
||
|
||
After installation, the `modules/` directory contains your extracted module:
|
||
|
||
```
|
||
modules/
|
||
└── my_module/
|
||
├── manifest.json
|
||
├── my_module_plugin.dylib # (or .so on Linux)
|
||
└── variant
|
||
```
|
||
|
||
### 5.3 Downloading and Installing from a Registry
|
||
|
||
To download packages from the online catalog and then install them locally, use `lgpd` (logos-package-downloader) followed by `lgpm`:
|
||
|
||
```bash
|
||
# Build lgpd
|
||
nix build 'github:logos-co/logos-package-downloader#cli' --out-link ./downloader
|
||
|
||
# Search for packages
|
||
./downloader/bin/lgpd search waku
|
||
|
||
# List all available packages
|
||
./downloader/bin/lgpd list
|
||
|
||
# Download a package
|
||
./downloader/bin/lgpd download my_module -o ./packages/
|
||
|
||
# Download from a specific release
|
||
./downloader/bin/lgpd --release v2.0.0 download my_module -o ./packages/
|
||
|
||
# Install the downloaded package locally
|
||
./package-manager/bin/lgpm --modules-dir ./modules install --file ./packages/my_module.lgx
|
||
```
|
||
|
||
`lgpd` handles the network side (browsing, searching, downloading), while `lgpm` handles local installation.
|
||
|
||
---
|
||
|
||
## Part 6: Running Your Module
|
||
|
||
Once your module is packaged and installed into a `modules/` directory (see Parts 3 and 4), you can run it with `logoscore`.
|
||
|
||
### 6.1 Running with `logoscore`
|
||
|
||
The **`logoscore`** CLI (from `logos-liblogos`) is a headless runtime that can load modules and invoke their methods from the command line.
|
||
|
||
#### Building logoscore
|
||
|
||
```bash
|
||
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
|
||
```
|
||
|
||
#### Daemon Mode
|
||
|
||
`logoscore` runs as a daemon that stays alive to host modules. Start it with `-D`:
|
||
|
||
```bash
|
||
# Start the daemon with a modules directory
|
||
./logos/bin/logoscore -D -m ./modules
|
||
```
|
||
|
||
Once the daemon is running, use commands from another terminal:
|
||
|
||
```bash
|
||
# Load a module
|
||
./logos/bin/logoscore load-module my_module
|
||
|
||
# Call a method on a loaded module
|
||
./logos/bin/logoscore call my_module doSomething hello
|
||
|
||
# List loaded modules
|
||
./logos/bin/logoscore list-modules --loaded
|
||
|
||
# Show module details
|
||
./logos/bin/logoscore module-info my_module
|
||
|
||
# Watch events from a module
|
||
./logos/bin/logoscore watch my_module
|
||
|
||
# Show daemon and module health
|
||
./logos/bin/logoscore status
|
||
|
||
# Stop the daemon
|
||
./logos/bin/logoscore stop
|
||
```
|
||
|
||
#### One-shot execution
|
||
|
||
There is no separate single-process mode — start a clean daemon, load the
|
||
module(s) with `load-module`, call methods with `call`, then stop the daemon:
|
||
|
||
```bash
|
||
# Start a clean daemon (loads nothing on its own)
|
||
./logos/bin/logoscore -D -m ./modules &
|
||
|
||
# Wait until the daemon is accepting commands
|
||
until ./logos/bin/logoscore status >/dev/null 2>&1; do sleep 0.2; done
|
||
|
||
# Load the module(s) you need (dependencies resolved automatically)
|
||
./logos/bin/logoscore load-module my_module
|
||
|
||
# Call methods (positional args; @file reads a parameter from a file)
|
||
./logos/bin/logoscore call my_module doSomething hello
|
||
./logos/bin/logoscore call my_module init @config.json
|
||
./logos/bin/logoscore call my_module start
|
||
|
||
# Stop the daemon when done
|
||
./logos/bin/logoscore stop
|
||
```
|
||
|
||
> **Note:** The legacy inline mode (`-c "module.method(args)"` / `--quit-on-finish`,
|
||
> which ran calls in one short-lived process) has been removed, as has the
|
||
> `-l/--load-modules` autoload flag — the daemon starts clean and modules are
|
||
> loaded with `load-module`. `-m`/`--persistence-path` configure daemon startup (`-D`).
|
||
|
||
**Daemon startup flags:**
|
||
|
||
| Flag | Description |
|
||
| ---------------------------------- | ---------------------------------------------------- |
|
||
| `-D` | Start the daemon |
|
||
| `-m, --modules-dir <dir>` | Directory containing module libraries (repeatable) |
|
||
| `--persistence-path <dir>` | Base directory for module instance persistence |
|
||
| `--config-dir <dir>` | Isolate this daemon's config/state/tokens dir (run multiple instances; the client must use the same `--config-dir`) |
|
||
| `@file.json` (as a `call` arg) | Pass a file's contents as a method argument |
|
||
|
||
**Daemon commands:**
|
||
|
||
| Command | Description |
|
||
| ------------------------------- | -------------------------------- |
|
||
| `status` | Show daemon and module health |
|
||
| `load-module <name>` | Load a module into the daemon |
|
||
| `unload-module <name>` | Unload a module |
|
||
| `reload-module <name>` | Reload (unload + load) a module |
|
||
| `list-modules [--loaded]` | List available or loaded modules |
|
||
| `module-info <name>` | Show detailed module information |
|
||
| `call <module> <method> [args]` | Call a method on a loaded module |
|
||
| `watch <module> [--event]` | Watch events from a module |
|
||
| `stats` | Show module resource usage |
|
||
| `stop` | Stop the daemon |
|
||
|
||
---
|
||
|
||
## Part 7: Running in logos-basecamp
|
||
|
||
### 7.1 Building logos-basecamp
|
||
|
||
logos-basecamp produces two binary variants:
|
||
|
||
- **development** (depends on `/nix/store`)
|
||
- **portable** (self-contained, used in distributed builds and `.app` bundles)
|
||
|
||
```bash
|
||
# Build the development version
|
||
nix build 'github:logos-co/logos-basecamp#app' --out-link ./logos-basecamp
|
||
|
||
# Run the dev binary
|
||
./logos-basecamp/bin/logos-basecamp
|
||
|
||
# Build the portable/distributed version
|
||
nix build 'github:logos-co/logos-basecamp#portable' --out-link ./logos-basecamp-portable
|
||
|
||
# Or build platform-specific distributions:
|
||
nix build 'github:logos-co/logos-basecamp#bin-bundle-dir' # Flat directory bundle
|
||
nix build 'github:logos-co/logos-basecamp#bin-appimage' # Linux AppImage
|
||
nix build 'github:logos-co/logos-basecamp#bin-macos-app' # macOS .app bundle
|
||
```
|
||
|
||
> **Note:** When installing modules into logos-basecamp, the LGX variant type must match the build type. Dev builds of basecamp expect **dev** LGX variants (e.g., `darwin-arm64-dev`), while portable builds expect **portable** variants (e.g., `darwin-arm64`). Use the `dual` bundler (see [3.2](#32-bundling-with-nix-bundle-lgx)) to produce packages that work with both.
|
||
|
||
### 7.2 Module Types in logos-basecamp
|
||
|
||
The application supports three types of modules:
|
||
|
||
#### Core Modules (Backend)
|
||
|
||
These are non-UI modules that provide backend functionality. They run in isolated `logos_host` processes and communicate via Qt Remote Objects.
|
||
|
||
- Loaded via `logos_core_load_plugin()`
|
||
- Placed in the **modules directory** (`--modules-dir`)
|
||
- Have `"type": "core"` in metadata
|
||
|
||
#### ui_qml with C++ Backend (Process-Isolated)
|
||
|
||
These have `"type": "ui_qml"` with both `"main"` (backend plugin) and `"view"` (QML entry point) in `metadata.json`. The C++ backend runs in a separate `ui-host` process; the QML view loads in the host app.
|
||
|
||
The remote interface is defined in a **`.rep` file** (Qt Remote Objects definition):
|
||
|
||
```rep
|
||
class CalcUiCpp
|
||
{
|
||
PROP(QString status READWRITE) // auto-synced to QML replica
|
||
SLOT(int add(int a, int b)) // callable from QML, returns via Promise
|
||
SIGNAL(errorOccurred(QString msg)) // one-shot events
|
||
}
|
||
```
|
||
|
||
The `.rep` file is the **single source of truth** — `repc` generates:
|
||
|
||
- `CalcUiCppSimpleSource` — base class the C++ backend inherits
|
||
- `CalcUiCppReplica` — typed replica the QML view uses via `logos.module()`
|
||
- A separate `_replica_factory` plugin for typed remoting
|
||
|
||
The C++ plugin inherits from the generated source + `ViewPluginBase`:
|
||
|
||
```cpp
|
||
class CalcUiCppPlugin : public CalcUiCppSimpleSource,
|
||
public CalcUiCppInterface,
|
||
public CalcUiCppViewPluginBase { ... };
|
||
```
|
||
|
||
QML accesses the backend via a typed replica:
|
||
|
||
```qml
|
||
readonly property var backend: logos.module("calc_ui_cpp")
|
||
// Properties auto-sync:
|
||
Text { text: backend.status }
|
||
// Return values via Promise:
|
||
logos.watch(backend.add(1, 2), function(v) { ... })
|
||
```
|
||
|
||
- Scaffold: `nix flake init -t github:logos-co/logos-module-builder#ui-qml-backend`
|
||
- See [Tutorial Part 3](tutorial-cpp-ui-app.md) for a complete walkthrough
|
||
|
||
#### ui_qml QML-Only (In-Process)
|
||
|
||
These have `"type": "ui_qml"` with `"view"` but no `"main"` — pure QML, no C++ compilation, no process isolation:
|
||
|
||
- QML view loads directly in the host app (basecamp / standalone)
|
||
- No `.rep` file needed
|
||
- Call core modules via the `logos` bridge: `logos.callModule("module", "method", [args])`
|
||
- Network access denied, filesystem restricted to module directory
|
||
- Scaffold: `nix flake init -t github:logos-co/logos-module-builder#ui-qml`
|
||
- See [Tutorial Part 2](tutorial-qml-ui-app.md) for a complete walkthrough
|
||
|
||
---
|
||
|
||
## Part 8: Inter-Module Communication
|
||
|
||
### 8.1 The LogosAPI
|
||
|
||
Every module receives a `LogosAPI*` pointer when `initLogos()` is called. This is your gateway to communicating with other modules.
|
||
|
||
```cpp
|
||
void MyModulePlugin::initLogos(LogosAPI* logosAPIInstance)
|
||
{
|
||
logosAPI = logosAPIInstance;
|
||
|
||
// Get a client for calling another module
|
||
LogosAPIClient* client = logosAPI->getClient("other_module");
|
||
|
||
// Synchronous call (blocks until result is returned)
|
||
QVariant result = client->invokeRemoteMethod(
|
||
"other_module", // target module name
|
||
"someMethod", // method name
|
||
arg1, arg2 // arguments (up to 5 positional args)
|
||
);
|
||
|
||
// Async call (preferred -- non-blocking, result delivered via callback)
|
||
client->invokeRemoteMethodAsync(
|
||
"other_module",
|
||
"someMethod",
|
||
[](QVariant result) {
|
||
// Handle result (called on the main thread)
|
||
if (result.isValid()) {
|
||
qDebug() << "Got result:" << result;
|
||
}
|
||
},
|
||
arg1, arg2
|
||
);
|
||
}
|
||
```
|
||
|
||
> **Prefer async calls.** Synchronous `invokeRemoteMethod` blocks the caller's thread until the remote module responds. Use `invokeRemoteMethodAsync` to avoid blocking, especially in UI modules.
|
||
|
||
### 8.2 The C++ SDK Code Generator
|
||
|
||
The `logos-cpp-generator` tool (from `logos-cpp-sdk`) inspects a compiled module and generates typed C++ wrapper classes, so you get compile-time type safety instead of raw `invokeRemoteMethod` calls.
|
||
|
||
#### Getting logos-cpp-generator
|
||
|
||
The generator is bundled with `logos-cpp-sdk`. It is automatically available:
|
||
|
||
- **In `nix develop`** -- the module dev shell includes the SDK on PATH
|
||
- **Build it directly:**
|
||
```bash
|
||
nix build 'github:logos-co/logos-cpp-sdk#cpp-generator' --out-link ./cpp-gen
|
||
./cpp-gen/bin/logos-cpp-generator --help
|
||
```
|
||
|
||
#### Generating Wrappers
|
||
|
||
```bash
|
||
# Generate wrappers for a single module, from the CONTRACT it ships beside its
|
||
# plugin. `--events-from` names that contract, and the wrapper's typed methods,
|
||
# record structs and typed on<Event>() accessors all come from it.
|
||
logos-cpp-generator /path/to/my_module_plugin.so --output-dir ./generated \
|
||
--events-from /path/to/share/logos/my_module.lidl
|
||
|
||
# A handcrafted Qt module publishes no contract; omit the flag and the wrapper
|
||
# comes from the plugin's Qt metaobject, which is then the only description of
|
||
# its API that exists.
|
||
logos-cpp-generator /path/to/handcrafted_plugin.so --output-dir ./generated
|
||
|
||
# Generate a wrapper per dependency, each from that dependency's LIDL contract
|
||
logos-cpp-generator --metadata metadata.json --general-only --output-dir ./generated \
|
||
--dep waku_module=/path/to/waku_module.lidl
|
||
|
||
# Generate only module files (no umbrella headers)
|
||
logos-cpp-generator /path/to/plugin.so --module-only --output-dir ./generated \
|
||
--events-from /path/to/share/logos/my_module.lidl
|
||
|
||
# Generate only umbrella SDK files (assumes module files exist)
|
||
logos-cpp-generator --metadata metadata.json --general-only --output-dir ./generated
|
||
```
|
||
|
||
> **Why `--events-from` is not optional for a module that has a contract.** A
|
||
> module built with `interface: "universal"` or `"cdylib"` publishes its
|
||
> `getMethods()` metadata in the LIDL contract vocabulary (`tstr`, `[uint]`,
|
||
> `result`) — that listing is what `lm` and `logoscore` show a human, and Qt
|
||
> type names would be the wrong answer for a Qt-free module. The wrapper
|
||
> emitter reads Qt type names, so generating from that listing would silently
|
||
> produce a wrapper of `QVariant` / `LogosMap`. It refuses instead, naming the
|
||
> contract to pass. Nix builds pass it for you: `buildHeaders.nix` finds
|
||
> `<module>/share/logos/<name>.lidl`, which `buildPlugin.nix` installed.
|
||
|
||
#### Using Generated Wrappers
|
||
|
||
After generation, you get typed wrapper classes with both synchronous and asynchronous methods:
|
||
|
||
```cpp
|
||
#include "logos_sdk.h" // Umbrella header
|
||
|
||
// In your module's initLogos():
|
||
void MyModulePlugin::initLogos(LogosAPI* api) {
|
||
logosAPI = api;
|
||
|
||
// Create the typed SDK wrapper
|
||
LogosModules* logos = new LogosModules(api);
|
||
|
||
// Synchronous call (blocks until result)
|
||
QString result = logos->other_module.doSomething("hello");
|
||
|
||
// Async call (preferred -- non-blocking)
|
||
logos->other_module.doSomethingAsync("hello", [](QVariant result) {
|
||
qDebug() << "Got:" << result;
|
||
});
|
||
}
|
||
```
|
||
|
||
The generated `LogosModules` struct provides a member for each module, with methods matching the module's `Q_INVOKABLE` methods. For every method `foo()`, an async variant `fooAsync()` is also generated that takes a callback parameter.
|
||
|
||
> **Prefer async wrappers.** Use `doSomethingAsync(...)` instead of `doSomething(...)` to avoid blocking the caller's thread. Synchronous calls can cause hangs if the target module is slow to respond.
|
||
|
||
### Dependency Interfaces
|
||
|
||
A regular dependency couples a module to **one concrete provider**: you list `other_module` in `dependencies`, and the generated `modules().other_module` wrapper bakes that name into every call. A **dependency interface** instead lets a module declare a *contract* — a list of methods and events — that **any** module exposing a superset of it can satisfy, and bind that contract to a concrete module **chosen at runtime**.
|
||
|
||
Declare interfaces in `metadata.json` under `interface_dependencies`, alongside (or instead of) `dependencies`:
|
||
|
||
```json
|
||
"interface_dependencies": [
|
||
{ "name": "calculator", "file": "interfaces/calculator.h", "impl_class": "ICalculator" }
|
||
]
|
||
```
|
||
|
||
| Field | Required | Meaning |
|
||
| ------------ | --------------- | ---------------------------------------------------------------------------------------------------- |
|
||
| `name` | Yes | Interface identifier → bound wrapper class (`Calculator`) and the `bind_<name>` factory |
|
||
| `file` | Yes | Path to the contract: a pure-C++ `.h` (methods + a `logos_events:` block) or a `.lidl` file |
|
||
| `impl_class` | For `.h` files | The class inside the header whose signatures define the contract |
|
||
| `input` | No | A flake-input name hosting the interface (same wiring as `dependencies`); omit for a local file |
|
||
|
||
The contract is written in the module's own language — for a universal module, a plain header:
|
||
|
||
```cpp
|
||
// interfaces/calculator.h
|
||
class ICalculator {
|
||
public:
|
||
int64_t add(int64_t a, int64_t b);
|
||
std::string libVersion();
|
||
logos_events:
|
||
void versionReady(const std::string& version);
|
||
};
|
||
```
|
||
|
||
The generator emits a **bound** wrapper whose target module is a constructor argument (not baked in), exposed on `LogosModules` as a `bind_<name>(moduleName)` factory. Bind once, then call as usual:
|
||
|
||
```cpp
|
||
#include "logos_sdk.h"
|
||
|
||
// moduleName is chosen at runtime — config, discovery, user pick, etc.
|
||
auto calc = modules().bind_calculator("calc_module");
|
||
int64_t sum = calc.add(3, 5); // synchronous
|
||
calc.fibonacciAsync(20, [](int64_t v){ ... }); // async (generated alongside)
|
||
calc.onVersionReady([](const std::string& v){ ... }); // typed event subscription
|
||
```
|
||
|
||
Binding is **not validated**: a module that does not satisfy the interface surfaces an ordinary remote-call error (a default-valued result), never a crash — so you can swap providers just by changing the bound name. The provider must be loaded at runtime; declaring it in `dependencies` is one way to ensure that, but the interface itself names no module.
|
||
|
||
See the [Dependency Interfaces tutorial](tutorial-interface-dependencies.md) for an end-to-end walkthrough, and [Composing Modules](tutorial-composing-modules.md) for the concrete-dependency counterpart.
|
||
|
||
### 8.3 LogosResult
|
||
|
||
Many module methods return `LogosResult` for structured success/error handling:
|
||
|
||
```cpp
|
||
LogosResult result = logos->my_module.someMethod();
|
||
|
||
if (result.success) {
|
||
// Access the value
|
||
QString value = result.getString();
|
||
int number = result.getInt();
|
||
bool flag = result.getBool();
|
||
QVariantMap map = result.getMap();
|
||
QVariantList list = result.getList();
|
||
|
||
// Access nested values by key (for map results)
|
||
QString name = result.getString("name");
|
||
int count = result.getInt("count", 0); // with default
|
||
|
||
// Generic typed access
|
||
auto custom = result.getValue<MyType>();
|
||
} else {
|
||
// Access the error
|
||
QString error = result.getError();
|
||
}
|
||
```
|
||
|
||
To return a `LogosResult` from your module:
|
||
|
||
```cpp
|
||
Q_INVOKABLE LogosResult MyModulePlugin::fetchData(const QString& id) {
|
||
if (id.isEmpty()) {
|
||
return {false, QVariant(), "ID cannot be empty"};
|
||
}
|
||
|
||
QVariantMap data;
|
||
data["id"] = id;
|
||
data["name"] = "Example";
|
||
data["count"] = 42;
|
||
return {true, data};
|
||
}
|
||
```
|
||
|
||
### 8.4 Communication Modes
|
||
|
||
The SDK supports two communication modes:
|
||
|
||
| Mode | Use Case | Mechanism |
|
||
| -------------------- | --------------------------- | ----------------------------------------- |
|
||
| **Remote** (default) | Desktop apps | Qt Remote Objects (IPC between processes) |
|
||
| **Local** | Mobile apps, single-process | In-process `PluginRegistry` |
|
||
|
||
Set the mode before creating any `LogosAPI` instances:
|
||
|
||
```cpp
|
||
// For mobile / embedded (all modules in one process)
|
||
LogosModeConfig::setMode(LogosMode::Local);
|
||
|
||
// For desktop (each module in its own process) -- this is the default
|
||
LogosModeConfig::setMode(LogosMode::Remote);
|
||
```
|
||
|
||
### 8.5 App-to-App Intents
|
||
|
||
Everything above is a module calling a **named** module: you know who you want
|
||
and you call it. Intents are the other shape — you name a **capability** and let
|
||
the shell find a provider.
|
||
|
||
```qml
|
||
// "Somebody show this chat group." The requester never learns who did.
|
||
logos.request("chat.group.open", { groupId: "abc123" }, function (res) {
|
||
if (res.ok) console.log("opened by", res.data.provider);
|
||
else console.log("failed:", res.error);
|
||
});
|
||
```
|
||
|
||
Use `callModule` when you depend on a specific module. Use an intent when you
|
||
want a capability and any qualified app will do — that is what lets a second
|
||
wallet be installed and picked without the calling app knowing it exists.
|
||
|
||
#### Declaring intents
|
||
|
||
Both keys go in `metadata.json`, and both are **arrays of objects**:
|
||
|
||
```json
|
||
"provides": [ { "intent": "chat.group.open" } ],
|
||
"uses": [ { "intent": "wallet.sign", "cardinality": "single" } ]
|
||
```
|
||
|
||
> **The most common mistake.** A bare string array is silently ignored:
|
||
>
|
||
> ```json
|
||
> "uses": ["wallet.sign"] ← WRONG. Parsed, discarded, no build error.
|
||
> "uses": [{"intent": "wallet.sign"}] ← right
|
||
> ```
|
||
>
|
||
> The request then fails with `not_declared` and nothing points at the manifest.
|
||
> Check the shell's log for `IntentRegistry:` lines, which name every
|
||
> declaration that was skipped and why.
|
||
|
||
`uses` is mandatory: an app may only request intents it declared. That bounds an
|
||
app's reachable capabilities to a set fixed when the package was built, so a
|
||
compromised view cannot reach for something the package never asked for.
|
||
|
||
The `logos.` prefix is reserved for capabilities the shell itself provides and
|
||
is refused from any installed package.
|
||
|
||
#### The three QML symbols
|
||
|
||
| Symbol | Direction |
|
||
| --- | --- |
|
||
| `logos.request(intent, params, callback)` | ask for a capability |
|
||
| `logos.respond(requestId, ok, data, error)` | answer one you provide |
|
||
| `intentRequested(requestId, intent, params, requesterName)` | signal: someone asked you |
|
||
|
||
A provider handles requests like any other signal:
|
||
|
||
```qml
|
||
Connections {
|
||
target: logos
|
||
function onIntentRequested(requestId, intent, params, requesterName) {
|
||
// Show UI, let the user decide, then answer. Answering later is normal
|
||
// and expected — you are not obliged to respond synchronously.
|
||
logos.respond(requestId, true, ({ provider: "my_app" }), "");
|
||
}
|
||
}
|
||
```
|
||
|
||
Three properties of the callback worth relying on:
|
||
|
||
- **Exactly once.** Every request terminates, including timeouts.
|
||
- **Always asynchronous**, even for an immediate failure. No app can come to
|
||
depend on a synchronous reply.
|
||
- **Real JS values.** `res.data.groupId` works; there is no JSON string to parse.
|
||
|
||
If you declare `provides` but never connect `intentRequested`, requests to you
|
||
end in `timeout` rather than hanging — the shell counts receivers to detect it.
|
||
|
||
#### The six error codes
|
||
|
||
`res.error` is one of exactly six values:
|
||
|
||
| Code | Meaning |
|
||
| --- | --- |
|
||
| `not_declared` | you did not list this intent in your own `uses` |
|
||
| `unavailable` | no provider could service it |
|
||
| `bad_request` | your `params` were rejected — fix what you sent |
|
||
| `cancelled` | the user dismissed the chooser, or the provider cancelled |
|
||
| `timeout` | a provider was reached but never answered |
|
||
| `failed` | the provider reported a failure |
|
||
|
||
A provider may only report `cancelled`, `timeout`, `failed` or `bad_request`.
|
||
Anything else it returns is coerced to `failed`. `not_declared` and
|
||
`unavailable` are the shell's alone, because both reveal whether a provider
|
||
exists at all.
|
||
|
||
`bad_request` is the one code both the shell and a provider can mint, and that
|
||
is deliberate. The shell mints it when `params` cannot cross an app boundary at
|
||
all — nested past eight levels, a string over 64 KB, a `QObject*`, a function.
|
||
A provider mints it when the values are well-formed but unusable: a missing
|
||
required field, an address that is not an address. If only the shell could mint
|
||
it, receiving it would prove no provider was ever consulted, and that is an
|
||
existence oracle of exactly the kind `unavailable` exists to prevent. Because
|
||
both can mint it, the shell's own `bad_request` is held to the same timing floor
|
||
as `unavailable` — you cannot tell from the delay which side rejected you.
|
||
|
||
The distinction from `failed` is what you should do next. `failed` means the
|
||
world did not cooperate; retrying is reasonable. `bad_request` means you sent
|
||
the wrong thing; retrying unchanged will fail identically. Check the provider's
|
||
`provides[].params` in its `metadata.json` (§4.2) to see the shape it expects.
|
||
|
||
#### Describing what an intent needs — `provides[].params`
|
||
|
||
A provider can say what payload it expects, alongside the capability itself:
|
||
|
||
```json
|
||
"provides": [
|
||
{
|
||
"intent": "wallet.send",
|
||
"params": [
|
||
{ "name": "to", "type": "string", "required": true,
|
||
"description": "Destination address" },
|
||
{ "name": "amount", "type": "number", "required": true },
|
||
{ "name": "memo", "type": "string", "required": false }
|
||
]
|
||
}
|
||
]
|
||
```
|
||
|
||
`type` is one of `string`, `number`, `bool`, `object`, `array`.
|
||
|
||
The shell checks the payload against this immediately **before dispatch**, and
|
||
refuses with `bad_request` if a required field is missing or a value has the
|
||
wrong type. The provider never sees a payload it declared unusable.
|
||
|
||
Three rules worth knowing:
|
||
|
||
- **Undeclared extra fields pass.** A caller written against a newer version of
|
||
a provider must not be broken by an older description, and a provider may
|
||
accept more than it lists.
|
||
- **No `params` means undescribed, not "takes nothing".** Nothing is validated.
|
||
- **Checked after a provider is chosen, never at submit.** Two providers of one
|
||
intent may describe it differently, so there is no single spec to check at
|
||
submit time — and testing all of them would reveal how many exist.
|
||
|
||
This is per-*provider*, not per-*intent*: it describes what one app wants, not
|
||
what the name means. A published registry of intent definitions is the intended
|
||
successor; until then, this is where you look to find out how to call something.
|
||
|
||
#### When two apps provide the same thing
|
||
|
||
The shell raises a chooser. What you can rely on as an app author:
|
||
|
||
- **You never see the list.** Providers are named and drawn entirely by the
|
||
shell, using the same labels and icons as the sidebar. A requesting app cannot
|
||
influence how a provider is presented, and a provider cannot dress itself up
|
||
in the chooser.
|
||
- **The list is sorted**, so the order is stable across runs.
|
||
- **"Always use this app"** persists the pick for that (requester, intent) pair.
|
||
It is re-checked at use, so an app being upgraded does not lose the
|
||
preference — and if the remembered provider stops declaring the intent, the
|
||
chooser simply comes back.
|
||
- **Dismissing gives `cancelled`**, not `unavailable`, so you can distinguish
|
||
"the user said no" from "there was nobody to ask". Treat `cancelled` as a
|
||
normal outcome, not an error to report.
|
||
|
||
The chosen provider is brought to the foreground **and left there.** The shell
|
||
does not navigate back when your request completes; returning is ordinary
|
||
navigation the user drives. Do not write your app expecting to regain focus.
|
||
|
||
Clearing remembered choices lives in the shell's settings, not in your app.
|
||
|
||
**`unavailable` is deliberately uninformative.** "Nobody provides this" and "you
|
||
were not allowed" are the same answer, delivered on the same timing floor, so an
|
||
app cannot use intents to enumerate what you have installed. Do not build logic
|
||
that tries to tell them apart — instead, let the request fail and let the shell
|
||
handle the fallback.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## Part 9: Advanced Topics
|
||
|
||
### 9.1 Tutorials
|
||
|
||
For hands-on walkthroughs of module development patterns, see the dedicated tutorials:
|
||
|
||
- **[Wrapping a C Library](tutorial-wrapping-c-library.md)** — create `calc_module` wrapping a vendored C library. Covers external library configuration in `metadata.json`.
|
||
- **[Building a QML UI App](tutorial-qml-ui-app.md)** — create `calc_ui`, a QML-only UI plugin that calls a core module via the `logos.callModule()` bridge.
|
||
- **[Building a C++ UI Module](tutorial-cpp-ui-app.md)** — build `calc_ui_cpp`, a C++ + QML view module that combines a QML frontend with a C++ backend. The backend exposes `Q_INVOKABLE` methods using the generated typed SDK; the QML view calls them via `logos.callModuleAsync()`.
|
||
|
||
### 9.2 Module Dependencies
|
||
|
||
Declare dependencies in your `metadata.json`:
|
||
|
||
```json
|
||
{
|
||
"name": "my_module",
|
||
"dependencies": ["package_manager", "waku_module"]
|
||
}
|
||
```
|
||
|
||
Each entry in `dependencies` must match the `name` field in that module's own `metadata.json`. When adding a dependency as a flake input, the **input attribute name** must also match the dependency name — e.g., `waku_module.url = "github:logos-co/logos-waku-module"`. The URL can point to any repo, but the attribute name is how the builder resolves dependencies.
|
||
|
||
When your module is installed via `lgpm`, its dependencies are automatically resolved and installed first. When loaded via `logos-basecamp`, core module dependencies are loaded before your module.
|
||
|
||
#### How dependencies are consumed — the LIDL contract
|
||
|
||
Each module publishes a small, language-neutral **LIDL interface contract** as a cheap flake output (`packages.<system>.lidl`), generated from its source with no plugin compile. When you depend on a module, the builder generates the typed `modules().<dep>` wrapper **from that published LIDL** — so building (or packaging) your module **does not build the dependency module**. The only step that still builds and bundles dependency plugins is the standalone-app run (`nix run` / `#run`), which has to, because it loads them.
|
||
|
||
This is the same `logos-cpp-generator` from [§8.2](#82-the-c-sdk-code-generator), just driven by the dependency's LIDL contract — the same kind of `.lidl`/`.h` contract `interface_dependencies` uses — instead of inspecting a compiled plugin. Inspecting a compiled plugin (as §8.2 describes) is the manual/standalone path; for declared module dependencies the builder uses the contract path, which is why no dependency plugin is built.
|
||
|
||
Because the contract is LIDL, the dependency's implementation language doesn't matter: the pipeline is `source → LIDL → C++` for a C++ module today, and `Rust → LIDL → C++` for a Rust module tomorrow — the same generated `modules().<dep>` wrapper either way.
|
||
|
||
> **Transitional fallback.** A dependency built by an older `logos-module-builder` won't expose a `lidl` output yet; for those the builder falls back to the previous behavior (build the dependency and copy its generated headers), so mixed dependency graphs keep working.
|
||
|
||
To force a specific contract source for a dependency — a committed `.lidl`, a header in another repo, etc. — add a `dependency_overrides` entry keyed by the dependency name:
|
||
|
||
```json
|
||
"dependencies": ["calc_module"],
|
||
"dependency_overrides": {
|
||
"calc_module": { "file": "interfaces/calc.lidl" }
|
||
}
|
||
```
|
||
|
||
Each override is `{ file, input?, impl_class? }`: `file` is the `.lidl`/`.h` path (relative to this repo, or to the flake `input` if given), and `impl_class` is required for a `.h` file. Most modules never need this — auto-resolution from the dependency's `lidl` output is the default.
|
||
|
||
### 9.3 Exposing OpenMetrics / Prometheus Metrics
|
||
|
||
Infra operators monitor logos.dev nodes with Prometheus. The
|
||
[`openmetrics`](https://github.com/logos-co/openmetrics-module) module serves an
|
||
[OpenMetrics](https://prometheus.io/docs/specs/om/open_metrics_spec/) `/metrics`
|
||
HTTP endpoint by querying a configured set of modules — it does not discover
|
||
modules or read platform stats, it only calls the modules you list.
|
||
|
||
To make your module scrapeable, implement one method by convention:
|
||
|
||
```
|
||
collectMetrics() -> LogosMap
|
||
```
|
||
|
||
returning openmetrics-like fields:
|
||
|
||
```json
|
||
{
|
||
"metrics": [
|
||
{ "name": "storage_blocks_total", "type": "counter", "help": "Total blocks stored", "value": 42 },
|
||
{ "name": "storage_peers_connected", "type": "gauge", "help": "Connected peers", "value": 7, "labels": { "protocol": "libp2p" } }
|
||
]
|
||
}
|
||
```
|
||
|
||
| Field | Meaning |
|
||
| -------- | ----------------------------------------------------------------------------------- |
|
||
| `name` | metric name (for counters, the OpenMetrics `_total` sample suffix is handled) |
|
||
| `type` | `counter`, `gauge`, `histogram`, or `summary` (unknown/missing → `unknown`) |
|
||
| `help` | short description |
|
||
| `value` | number (bools map to 1/0; numeric strings pass through) |
|
||
| `labels` | optional string→string label pairs |
|
||
|
||
The metrics server adds a `module="<name>"` label to every series automatically.
|
||
Modules that don't implement `collectMetrics` (or that error/time out) are skipped, so
|
||
one module never breaks a scrape.
|
||
|
||
**Universal (plain C++) module:**
|
||
|
||
```cpp
|
||
// in <module>_impl.h: LogosMap collectMetrics();
|
||
LogosMap MyModuleImpl::collectMetrics() {
|
||
LogosMap metrics = LogosMap::array();
|
||
metrics.push_back({
|
||
{"name", "storage_blocks_total"}, {"type", "counter"},
|
||
{"help", "Total blocks stored"}, {"value", m_blockCount}
|
||
});
|
||
return {{"metrics", metrics}};
|
||
}
|
||
```
|
||
|
||
**Legacy (Qt) module** — add `Q_INVOKABLE QVariantMap collectMetrics();` returning the
|
||
same `{ "metrics": [...] }` shape as a `QVariantMap`/`QVariantList`.
|
||
|
||
Then run the metrics server alongside your module and point it at you (daemon
|
||
mode passes the JSON arg intact):
|
||
|
||
```bash
|
||
# --config-dir isolates this daemon instance (config/state/tokens) from the
|
||
# default ~/.logoscore, so it can run alongside others. -D runs in the
|
||
# foreground, so background it and wait for it to be ready.
|
||
logoscore -D -m <modules-dir> --config-dir /tmp/om &
|
||
until logoscore --config-dir /tmp/om status >/dev/null 2>&1; do sleep 0.2; done
|
||
logoscore --config-dir /tmp/om load-module my_module
|
||
logoscore --config-dir /tmp/om load-module openmetrics
|
||
logoscore --config-dir /tmp/om call openmetrics start '{"port":9090,"modules":["my_module"]}'
|
||
curl http://localhost:9090/metrics
|
||
```
|
||
|
||
---
|
||
|
||
## Reference: Repository Map
|
||
|
||
| Repository | What It Provides | Key Outputs |
|
||
| -------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------- |
|
||
| [logos-module-builder](https://github.com/logos-co/logos-module-builder) | Build system / scaffolding | `mkLogosModule`, `mkLogosQmlModule` Nix functions, `LogosModule.cmake`, templates |
|
||
| [logos-module](https://github.com/logos-co/logos-module) | Plugin introspection | `liblogos_module.a` (static lib), `lm` (CLI) |
|
||
| [logos-cpp-sdk](https://github.com/logos-co/logos-cpp-sdk) | SDK + code generator | `LogosAPI`, `LogosResult`, `logos-cpp-generator`, `PluginInterface` |
|
||
| [logos-liblogos](https://github.com/logos-co/logos-liblogos) | Core library | `logos_host`, `liblogos_core` |
|
||
| [logos-logoscore-cli](https://github.com/logos-co/logos-logoscore-cli) | Headless CLI runtime | `logoscore` (CLI) |
|
||
| [logos-package](https://github.com/logos-co/logos-package) | Package format | `lgx` (CLI), `liblgx` (library) |
|
||
| [logos-package-manager](https://github.com/logos-co/logos-package-manager) | Local package management | `lgpm` (CLI) |
|
||
| [logos-package-downloader](https://github.com/logos-co/logos-package-downloader) | Online catalog + downloads | `lgpd` (CLI) |
|
||
| [logos-standalone-app](https://github.com/logos-co/logos-standalone-app) | Minimal UI module runner | `logos-standalone-app` (loads a single UI plugin for testing) |
|
||
| [logos-basecamp](https://github.com/logos-co/logos-basecamp) | Desktop app shell | `LogosApp` (GUI), MDI workspace, plugin loader |
|
||
|
||
## Reference: CLI Tools Summary
|
||
|
||
### `lm` -- Module Inspector
|
||
|
||
```bash
|
||
lm <plugin-file> # Show metadata + methods
|
||
lm metadata <plugin-file> [--json] # View module metadata
|
||
lm methods <plugin-file> [--json] # List Q_INVOKABLE methods
|
||
```
|
||
|
||
### `logoscore` -- Headless Runtime
|
||
|
||
```bash
|
||
# Daemon mode
|
||
logoscore -D -m <modules-dir> # Start daemon
|
||
logoscore load-module <name> # Load a module
|
||
logoscore call <module> <method> [args] # Call a method
|
||
logoscore list-modules [--loaded] # List modules
|
||
logoscore module-info <name> # Show module details
|
||
logoscore status # Daemon health
|
||
logoscore stop # Stop daemon
|
||
```
|
||
|
||
### `lgpm` -- Local Package Manager
|
||
|
||
```bash
|
||
./package-manager/bin/lgpm --modules-dir <path> install --file <path.lgx> # Install from local .lgx file
|
||
./package-manager/bin/lgpm --modules-dir <path> install --dir <dir> # Install all .lgx files in a directory
|
||
./package-manager/bin/lgpm --modules-dir <path> list # List installed packages
|
||
./package-manager/bin/lgpm --modules-dir <path> info <pkg> # Show installed package details
|
||
```
|
||
|
||
### `lgpd` -- Package Downloader
|
||
|
||
```bash
|
||
./downloader/bin/lgpd search <query> # Search packages by name/description
|
||
./downloader/bin/lgpd list [--category <cat>] # List available packages
|
||
./downloader/bin/lgpd categories # List available categories
|
||
./downloader/bin/lgpd releases # List recent GitHub releases (up to 30)
|
||
./downloader/bin/lgpd info <pkg> # Show package details from catalog
|
||
./downloader/bin/lgpd download <pkg> [-o <dir>] # Download .lgx package
|
||
./downloader/bin/lgpd --release <tag> download <pkg> # Download from specific release
|
||
```
|
||
|
||
### `logos-cpp-generator` -- SDK Code Generator
|
||
|
||
```bash
|
||
logos-cpp-generator <plugin-file> [--output-dir <dir>] [--module-only] [--events-from <name>.lidl]
|
||
logos-cpp-generator --metadata <metadata.json> --general-only --dep <name>=<name>.lidl [--output-dir <dir>]
|
||
logos-cpp-generator --metadata <metadata.json> --general-only [--output-dir <dir>]
|
||
```
|
||
|
||
### `nix-bundle-lgx` -- LGX Bundler
|
||
|
||
```bash
|
||
# Preferred: built-in derivation (logos-module-builder includes nix-bundle-lgx)
|
||
nix build .#lgx # Dev variant
|
||
nix build .#lgx-portable # Portable variant
|
||
|
||
# Alternative: nix bundle command
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx .#lib # Dev variant
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx#portable .#lib # Portable variant
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx#dual .#lib # Both variants
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
### "experimental features" error with Nix
|
||
|
||
If you see errors about experimental features, either pass the flag:
|
||
|
||
```bash
|
||
nix --extra-experimental-features "nix-command flakes" build
|
||
```
|
||
|
||
Or add to `~/.config/nix/nix.conf`:
|
||
|
||
```
|
||
experimental-features = nix-command flakes
|
||
```
|
||
|
||
### Module loads but LogosAPI is not available
|
||
|
||
This happens when running a module outside the full Logos runtime (e.g., in the module viewer). The `LogosAPI` is only available when the module is loaded by `logoscore` or `logos-basecamp`.
|
||
|
||
### Module not discovered by logos-basecamp
|
||
|
||
Check that:
|
||
|
||
1. The module binary is in the correct directory (modules dir for core, plugins dir for UI)
|
||
2. The `metadata.json` file is present alongside the binary
|
||
3. The `name` field in metadata matches the binary name (e.g., `my_module_plugin.so` for module named `my_module`)
|
||
|
||
### lgpm install fails
|
||
|
||
- Check your internet connection (lgpm fetches from GitHub Releases)
|
||
- Try specifying a release: `./package-manager/bin/lgpm --release v1.0.0 install my_module`
|
||
- For local files: `./package-manager/bin/lgpm install --file ./my_module.lgx`
|
||
- Check the target directory is writable: `./package-manager/bin/lgpm --modules-dir ./modules install my_module`
|
||
|
||
### Checking if a module loaded successfully
|
||
|
||
Use `logoscore` to verify your module loads and its methods are callable:
|
||
|
||
```bash
|
||
# Start the daemon (runs in the foreground, so background it and wait)
|
||
./logos/bin/logoscore -D -m ./modules &
|
||
until ./logos/bin/logoscore status >/dev/null 2>&1; do sleep 0.2; done
|
||
|
||
# Check if the module is listed as loaded
|
||
./logos/bin/logoscore list-modules --loaded
|
||
|
||
# Inspect the module
|
||
./logos/bin/logoscore module-info my_module
|
||
|
||
# Quick check: load the module, call a method, then stop the daemon
|
||
./logos/bin/logoscore load-module my_module
|
||
./logos/bin/logoscore call my_module greet test
|
||
./logos/bin/logoscore stop
|
||
```
|
||
|
||
### UI module `nix run` fails to load dependencies
|
||
|
||
When running a UI module with `nix run`, the standalone app automatically bundles all module dependencies declared in `metadata.json`. If dependencies fail to load, check the following requirements:
|
||
|
||
**Requirements for auto-bundled dependencies:**
|
||
|
||
1. **Module type must be `"ui"` or use `mkLogosQmlModule`** — only UI modules get `apps.default` wired up with the standalone app.
|
||
|
||
2. **Dependencies must be listed in `metadata.json`** under the `"dependencies"` array:
|
||
|
||
```json
|
||
{
|
||
"name": "my_ui_module",
|
||
"type": "ui",
|
||
"dependencies": ["calc_module", "storage_module"]
|
||
}
|
||
```
|
||
|
||
3. **Each dependency must have a matching flake input** — the flake input name must exactly match the dependency name in `metadata.json`:
|
||
|
||
```nix
|
||
inputs = {
|
||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||
calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
|
||
storage_module.url = "github:logos-co/logos-storage-module";
|
||
};
|
||
```
|
||
|
||
4. **Module names must be consistent** — the `"name"` field in each dependency's `metadata.json` must match its flake input name. The build system uses this name to locate the plugin binary (`{name}_plugin.so` / `{name}_plugin.dylib`).
|
||
|
||
**What changed (no more `logos-standalone-app` input):**
|
||
|
||
- `logos-standalone-app` is now bundled inside `logos-module-builder` — UI module flakes no longer need it as a separate input.
|
||
- No `logosStandalone` parameter is needed in `mkLogosQmlModule`, `mkLogosModule`, or `mkLogosQmlModule` calls.
|
||
- Dependencies (including transitive ones) are automatically resolved from the flake input tree, bundled as LGX packages at build time, and extracted into the modules directory at runtime.
|
||
- The standalone app uses `logos_core_load_plugin_with_dependencies()` which resolves the full transitive dependency graph via metadata.json files.
|
||
|
||
**Example C++ UI module `flake.nix` (view module — C++ backend + QML view):**
|
||
|
||
```nix
|
||
{
|
||
description = "My UI module";
|
||
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;
|
||
flakeInputs = inputs;
|
||
};
|
||
}
|
||
```
|
||
|
||
**Example QML UI module `flake.nix`:**
|
||
|
||
```nix
|
||
{
|
||
description = "My QML UI module";
|
||
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;
|
||
flakeInputs = inputs;
|
||
};
|
||
}
|
||
```
|
||
|
||
If the module doesn't appear, check:
|
||
|
||
1. The `modules/` directory contains a subdirectory for your module with `manifest.json` and the plugin binary
|
||
2. The variant in the manifest matches your platform (e.g., `darwin-arm64-dev` for dev builds on Apple Silicon)
|
||
3. Use `lm` to verify the plugin binary is a valid Qt plugin: `./lm/bin/lm ./modules/my_module/my_module_plugin.dylib`
|
||
|
||
### Capability module not found
|
||
|
||
logos-basecamp requires the `capability` module to be installed. It is bundled with basecamp and installed on first launch. If you see errors about it:
|
||
|
||
1. Check that the `modules/` and `plugins/` directories exist next to `bin/` and `lib/` in the basecamp build output
|
||
2. Check that the capability module was extracted to the modules directory
|
||
3. Verify the LGX variant type matches your basecamp build (dev variant for dev build, portable for portable build)
|
||
|
||
### LGX variant mismatch
|
||
|
||
If a module installs but fails to load, the variant type may not match:
|
||
|
||
- **Dev build** of logos-basecamp needs **dev** LGX variants (`darwin-arm64-dev`)
|
||
- **Portable build** needs **portable** variants (`darwin-arm64`)
|
||
- Use `nix build .#lgx` and `nix build .#lgx-portable` to produce each variant separately, or `nix bundle --bundler github:logos-co/nix-bundle-lgx#dual .#lib` for a single package with both variants
|
||
|
||
### Cross-platform builds
|
||
|
||
Build on each target platform separately to create `.lgx` packages:
|
||
|
||
```bash
|
||
# On each platform, the built-in derivation produces the correct variant automatically:
|
||
nix build .#lgx-portable
|
||
|
||
# Or using nix bundle for dual variant:
|
||
nix bundle --bundler github:logos-co/nix-bundle-lgx#dual .#lib
|
||
|
||
# Then merge platform-specific .lgx files into one:
|
||
./lgx/bin/lgx merge my_module-linux.lgx my_module-macos.lgx -o my_module.lgx
|
||
```
|
||
|
||
**Windows is the exception: it is cross-built, never built natively.** There is no
|
||
Nix daemon for Windows, so "build on the target platform" does not apply. Build
|
||
the `x86_64-windows` target from a Linux machine instead:
|
||
|
||
```bash
|
||
nix build .#packages.x86_64-windows.lgx-portable
|
||
```
|
||
|
||
Two consequences worth knowing before you try it:
|
||
|
||
- The resulting package declares `windows-x86_64`. Because `lgpm` on the builder
|
||
is a Linux binary, laying that package out on the builder needs the explicit
|
||
`--platform windows-x86_64` opt-out described in [5.1](#51-the-lgpm-cli).
|
||
- A Windows developer runs the same cross-build inside WSL2 and copies the
|
||
artifacts out to a Windows path — the toolchain is Linux either way.
|