Modulus
Modulus is the shared C++23 foundation library and build tooling used by the khodo Product Vamp repositories. It provides small runtime utilities, Conan profiles, CMake package integration, editor configuration generation, typed LSP compile-database setup, and bridge targets for the external libraries needed by Product Vamp.
The core modulus::modulus target requires only Boost.JSON. Optional bridges are loaded when requested:
find_package(modulus CONFIG REQUIRED COMPONENTS openssl)
target_link_libraries(app PRIVATE modulus::modulus modulus::openssl)
Available components are boost_json, openssl, sqlite, raylib, and miniaudio. Existing package discovery can also be followed by modulus_require_openssl() (or the corresponding bridge helper). Source builds opt in with MODULUS_WITH_OPENSSL, MODULUS_WITH_SQLITE, MODULUS_WITH_RAYLIB, and MODULUS_WITH_MINIAUDIO.
Conan consumers must enable the matching modulus/*:with_openssl, with_sqlite, with_raylib, or with_miniaudio option to add that dependency to the graph. Applications combining several bridge consumers should set the union of these options on their direct Modulus requirement. All four options default to false.
The Python CLI dispatches to separate build, LSP, run, and packaging modules. Build context, subprocess execution, Conan access, publishing policy, and container identity/execution are lower-level utilities shared by those commands and editor adapters. The bootstrap entrypoint remains modulus.cli.main; project configuration and command-line flags are unchanged.
Build
Use the checked-in tools directly inside CI or a compatible build environment:
MODULUS_DIRECT=1 ./tools/configure.py --target x86_64_gcc12_release
./tools/build.py --target x86_64_gcc12_release
./tools/test.py --target x86_64_gcc12_release
Outside that environment, downstream repos use python3 modulus.py <command> to fetch the public modulus/latest package from the khodo Conan registry and export the tool wrappers into build/build_tools/.
GCC 12 profiles select versioned native and ARM compiler executables. Install gcc-12-aarch64-linux-gnu and g++-12-aarch64-linux-gnu for ARM builds; the unversioned packages can select a different compiler on newer distributions. ARM profiles include the selected compiler executables in the internal libraries' Conan package IDs so previously mislabeled binaries are not reused. Third-party package IDs are unchanged. Publish Modulus before rerunning the sibling libraries' package workflows so they export the corrected profiles.
build automatically configures a target when CMake metadata is missing. configure --update refreshes dependencies and, in consumer repos, the exported Python tooling. Failed downloads/exports leave the previous tooling in place. --no-remote uses only the project-local cache, including initial bootstrap; it never downloads packages. Each consumer's tracked modulus.py is copied unchanged from modulus/bootstrap.py. Update that canonical source and synchronize consumer copies together.
Projects can select a tracked build recipe using tooling.docker.dockerfile and list additional build-context inputs in tooling.docker.inputs. Those files, the Dockerfile, and the shared formatting requirements determine the image fingerprint used by builds and clangd. Paths are project-relative. tooling.docker.socket_commands optionally permits host Docker socket access for build and/or package; the default is no socket access. Editor, configure, and test commands never receive the socket.
Refresh clangd compile databases with:
./tools/lsp.py
Product repos can add typed extra CMake compile databases under tooling.lsp.compile_databases; this is intended for cases like Product Vamp's Pico firmware tree.
Build Python
Build infrastructure is available as exported modulus modules. modulus.filesystem.publish_directories() replaces validated directories with rollback; modulus.artifacts owns archive naming, checksums, and explicit generic-registry publishing. Product-specific generators and asset policies stay in the consumer.
Projects needing asset tooling opt in with "tooling": {"python": {"groups": ["assets"]}}. The pinned Pillow, defusedxml, FontTools, and aggdraw requirements live in modulus/assets_requirements.txt. Configure prepares an isolated environment under build/build_common/python, fingerprinted by the interpreter, platform, and dependency manifest. Builds and offline commands reuse it without downloading; missing or stale dependencies require configure. C++-only repos use no optional environment, and Python dependencies never enter runtime packages.
Run ./modulus.py python -m generator (or another project module) through the exported launcher. It adds the exported Modulus package before the consumer's modulus.py, without changing the working directory. Configure supplies Python3_EXECUTABLE, MODULUS_PYTHON_RUNNER, and MODULUS_PYTHON_ROOT for CMake commands. Conan consumers can use python_cmake_variables(recipe) from the Modulus Python requirement.
./modulus.py publish validates all configured archives and checksums before accessing the registry. Credentials are forwarded only for explicit publishing or enabled package uploads, never for configure/build downloads. Project-owned artifact and build paths use underscores; dependency names, CLI flags, Git URLs, and standard tool filenames retain their upstream spelling.
Editor Configuration
modulus.json is the source for generated .clangd, .clang-format, and pyrightconfig.json files. Configure, LSP refresh, and tooling export regenerate them only when their content changes. Keep these generated files ignored; customize tooling.editor instead:
{
"tooling": {
"editor": {
"clangd": [
{"CompileFlags": {"CompilationDatabase": "build/native"}},
{"If": {"PathMatch": "src/firmware/.*"}, "CompileFlags": {"CompilationDatabase": "build/firmware"}}
],
"clang_format": {"BasedOnStyle": "LLVM", "IndentWidth": 2, "ColumnLimit": 0, "SortIncludes": "Never"},
"pyright": {"include": ["tools", "conanfile.py"]}
}
}
}
Each supplied entry replaces that file's entire configuration. Omitted entries use Modulus defaults: the default target's build directory for clangd, -Wall/-Wextra, disabled clang-tidy, the compact formatting policy below, and Python paths for the bootstrap, recipe, and exported tools. Entries use the editors' own configuration keys. LLVM files use JSON-compatible YAML, with separate documents for clangd fragments.
Shared Code Style
Run python3 tools/format.py in Modulus, or ./modulus.py format in a consumer. Optional repository-relative paths restrict the operation. format --check reports layout differences and prohibited aliases without writing files; CI runs this check. format --stdin-filename /absolute/buffer.cpp reads stdin and writes only formatted text, for editor integrations.
The build image pins clang-format 19.1.7, autopep8 2.3.2, and the syntax parser dependencies in tools/docker/format_requirements.txt. Python uses two-space indentation for both blocks and continuations, including multiline tuples, lists, dictionaries, imports, and calls. Continuations indent two spaces from their opening line rather than aligning to a column; closing brackets align with the opening line. Line-length and aggressive semantic fixes are disabled. C++ has no column limit, uses braces, and preserves include order and groups. The shared syntax-aware pass joins simple signatures/calls, removes blank lines inside functions and class/struct bodies, separates definitions outside classes by one blank line, and pads namespace interiors without indenting their bodies. Class members, including access labels, defaulted/deleted constructors, inline methods, and nested types, have no intervening blank lines. Blank lines within literal and comment contents are preserved. Comments, multiline strings, structured literals, and preprocessor content are not joined. Python formatting verifies AST equivalence before writing. C++ formatting supplements clang-format using concrete syntax boundaries, including common test macros; it is not a C++ type checker.
Use full type/module names rather than C++ type/namespace aliases, Python type aliases, or renamed imports. The existing std::*_t portability definitions in include/modulus/types.hpp are the sole local exception; third-party SDK definitions are not rewritten. Formatting changes layout only, so alias diagnostics require explicit code changes.
Discovery includes Git-tracked and nonignored untracked C/C++ and Python sources. Generated/build/vendor directories and symlinks are excluded. Add project-relative glob patterns with tooling.format.exclude; C++ settings remain configurable through tooling.editor.clang_format. A batch validates every input before it writes any output.
Neovim uses Conform for C++ and Python in registered Modulus projects, retaining clangd and Pyright for language features. LazyVim's manual command, format-on-save handling, and global/buffer toggles remain in control. Other projects retain their formatter selections. There is no editor-triggered download, image build, host formatter fallback, or compilation-database requirement. Missing/stale images fail on stderr without replacing the buffer. Configure first to prepare the image. MODULUS_DIRECT=1 is for provisioned CI environments, which install the same pinned requirements; it is not set by the editor.
The authored cmake/ directory belongs in Git. CMakeFiles/, compiler caches, and generated sources belong in build directories. Modulus rejects project/source-root output directories before clearing configure or LSP caches. Conan may still use its own isolated build folders. Existing build caches are retained; they are not source files.
Container Clangd
All managed repos use clangd 19 in their build image. configure and lsp rebuild images whose Dockerfile fingerprint has changed and prepare container headers on the host. lsp also refreshes extra databases, such as Pico firmware. Run these commands on the host before starting your editor. Direct/container-only builds do not prepare a host header cache.
Neovim 0.11+ with exrc enabled loads the generated, ignored .nvim.lua after you review and trust it. The shared Lua helper finishes LazyVim's LSP setup before changing clangd's command and project routing; completion capabilities, keybindings, and attach handlers are preserved. Non-Modulus projects keep their existing server. Content-stable generation avoids repeated trust prompts. An existing hand-authored .nvim.lua is never overwritten; it can call dofile(root .. "/build/build_tools/nvim/modulus.lua").setup(root) (use /tools/nvim/ in Modulus itself). Set tooling.editor.nvim.enabled to false to opt out.
Other editors can launch python3 modulus.py clangd in a consumer repo, or python3 tools/clangd.py in Modulus. This is a stdio-only transport: no TTY, downloads, builds, credential environment forwarding, Docker socket, or host-server fallback. Startup fails on stderr if the image, database, or header cache needs preparation. Workspace paths stay identical in the container; sources are read-only and build/ is writable. The container exits on EOF/shutdown and is removed on launcher termination.
Server settings are separate from .clangd's parsing/diagnostic settings:
{
"tooling": {
"lsp": {
"clangd": {
"executable": "/usr/bin/clangd-19",
"arguments": ["--background-index", "--completion-style=detailed", "--header-insertion=never"],
"query_drivers": ["/usr/bin/gcc-12", "/usr/bin/g++-12", "/usr/bin/aarch64-linux-gnu-gcc-12", "/usr/bin/aarch64-linux-gnu-g++-12"],
"header_roots": []
}
}
}
}
Omitted settings use these defaults; supplied lists replace defaults. Compiler paths must be explicit, trusted absolute paths, not wildcards. Add embedded compiler paths and SDK header roots in product config. --query-driver and --path-mappings are managed by the launcher, not the argument list. The default image uses Debian's clangd layout; custom images must provide /usr/lib/llvm-<major>/lib/clang/<major>/include for built-in headers.
Header-only snapshots are shared by immutable image ID and header settings under $XDG_CACHE_HOME/modulus/clangd (default ~/.cache/modulus/clangd). Preparation is locked and atomic. Symlinks are dereferenced in the container, and extracted files are read-only. clangd maps definition locations to these local files; Neovim additionally opens them unmodifiable. Old image caches can be removed when no editor uses them; run lsp to recreate a removed cache. Toolchains themselves are not copied to the host.
Package
Create Conan packages with:
./tools/package.py --all
Publishing requires credentials only for the upload step:
CONAN_USER=<user> \
CONAN_TOKEN=<token> \
MODULUS_CONAN_UPLOAD=1 \
MODULUS_ALLOW_PACKAGE_REPLACE=1 \
./tools/package.py --all --replace
The default public Conan remote is:
https://git.thyme-labs.com/api/packages/khodo/conan
Generic application artifacts default to:
https://git.thyme-labs.com/api/packages/khodo/generic
Application archives have stable names, <package>-<target>.tar.gz, with matching SHA-256 files. Each build replaces the corresponding local artifact. Generic uploads use the registry's required latest slot; application packaging has no release number or version argument. Conan library references and external dependency pins remain unchanged.
Configure/build/test do not require package credentials for public packages. Read operations log out only the configured public remote in the project-local Conan home and strip credential environment variables, including native Conan username/password variables. Authentication failures are noninteractive errors, not password prompts. Package contents and global Conan configuration are untouched. Credentials are validated only for publishing, redacted in command logs, and the remote is logged out in a finally cleanup after every upload attempt so CI caches do not retain the session.
License
Modulus is released under the MIT License.