diff --git a/logos-calc-module/.gitignore b/logos-calc-module/.gitignore new file mode 100644 index 0000000..a7683c0 --- /dev/null +++ b/logos-calc-module/.gitignore @@ -0,0 +1,10 @@ +# Nix build output +result +result-* + +# CMake build directory +build/ + +# Compiled C library (built from source by the Nix external-lib derivation, +# or by `make shared` for a local sanity check) +lib/build/ diff --git a/logos-calc-module/flake.lock b/logos-calc-module/flake.lock index 9f1e372..f2f7c81 100644 --- a/logos-calc-module/flake.lock +++ b/logos-calc-module/flake.lock @@ -1,5 +1,17 @@ { "nodes": { + "calc-src": { + "flake": false, + "locked": { + "path": "./lib", + "type": "path" + }, + "original": { + "path": "./lib", + "type": "path" + }, + "parent": [] + }, "logos-capability-module": { "inputs": { "logos-module-builder": "logos-module-builder_2" @@ -7703,6 +7715,7 @@ }, "root": { "inputs": { + "calc-src": "calc-src", "logos-module-builder": "logos-module-builder" } } diff --git a/logos-calc-module/flake.nix b/logos-calc-module/flake.nix index 7cce705..310cf28 100644 --- a/logos-calc-module/flake.nix +++ b/logos-calc-module/flake.nix @@ -3,12 +3,25 @@ inputs = { logos-module-builder.url = "github:logos-co/logos-module-builder/tutorial-v2"; + + # The C library source, built from source by Nix (see metadata.json's + # build_command). flake = false means "just give me the source tree". + calc-src = { + url = "path:./lib"; + flake = false; + }; }; - outputs = inputs@{ logos-module-builder, ... }: + outputs = inputs@{ logos-module-builder, calc-src, ... }: logos-module-builder.lib.mkLogosModule { src = ./.; configFile = ./metadata.json; flakeInputs = inputs; + + # Hand the C source to the builder. The attribute name (calc) must match + # the external_libraries[].name in metadata.json. + externalLibInputs = { + calc = calc-src; + }; }; } diff --git a/logos-calc-module/lib/Makefile b/logos-calc-module/lib/Makefile new file mode 100644 index 0000000..934f81a --- /dev/null +++ b/logos-calc-module/lib/Makefile @@ -0,0 +1,27 @@ +# Builds libcalc as a shared library. Invoked by the Nix build via the +# `build_command` in metadata.json ("make shared"), and usable standalone for +# a quick local sanity check. + +# Pick the platform-correct shared-library extension. +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + EXT := dylib +else + EXT := so +endif + +# Extra optimization/compile flags — overridable from the environment (the Nix +# stdenv exports CC, and may set CFLAGS/LDFLAGS). The shared-library flags +# (-shared -fPIC) are passed literally on the recipe line below, NOT via +# CFLAGS, so an env-provided CFLAGS can't accidentally drop them and turn this +# into a plain executable named libcalc.so/.dylib. +CFLAGS ?= -O2 + +shared: + mkdir -p build + $(CC) -shared -fPIC $(CFLAGS) $(LDFLAGS) -o build/libcalc.$(EXT) libcalc.c + +clean: + rm -rf build + +.PHONY: shared clean diff --git a/logos-calc-module/metadata.json b/logos-calc-module/metadata.json index 819669b..4b6a4cf 100644 --- a/logos-calc-module/metadata.json +++ b/logos-calc-module/metadata.json @@ -15,7 +15,8 @@ "external_libraries": [ { "name": "calc", - "vendor_path": "lib" + "build_command": "make shared", + "output_pattern": "build/libcalc.*" } ], "cmake": { diff --git a/tutorial-cpp-ui-app.md b/tutorial-cpp-ui-app.md index 32f12df..292da9f 100644 --- a/tutorial-cpp-ui-app.md +++ b/tutorial-cpp-ui-app.md @@ -492,7 +492,7 @@ The `calc_module` input attribute name must match the dependency name in `metada - **`github:`** — fetches from a remote GitHub repo. Use for CI or when `calc_module` is published. - **`path:`** — points to a local directory on disk (e.g., `path:../logos-calc-module`). Use during local development. -> **Important:** Whichever URL scheme you use, `calc_module` must be built with its shared library (`.so` on Linux, `.dylib` on macOS) present in `lib/`. If it's missing, the nix build will fail with linker errors. See [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library). +> **Important:** `calc_module` bundles a small C library (`libcalc`) compiled **from source** during its own Nix build — there's no prebuilt `.so`/`.dylib` to stage. Building `calc_module` (which happens automatically when this app pulls it in) compiles it for you. See [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-add-a-makefile-to-build-the-library). `mkLogosQmlModule` handles everything: compiles the C++ backend (because `main` is set), bundles the QML view, generates LGX packages, and wires up `nix run`. @@ -500,11 +500,10 @@ The `calc_module` input attribute name must match the dependency name in `metada ## Step 8: Build and Run -First, make sure your local `calc_module` is built and its `.so`/`.dylib` is present in `lib/` (see [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)): +First, make sure your local `calc_module` builds cleanly — its C library is compiled from source during the Nix build (from `lib/libcalc.c` via `lib/Makefile`), so there's nothing to pre-build by hand (see [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-add-a-makefile-to-build-the-library)): ```bash -ls ../logos-calc-module/lib/libcalc.so # Linux -ls ../logos-calc-module/lib/libcalc.dylib # macOS +cd ../logos-calc-module && git add -A && nix build && cd - ``` Then build and run. Choose the approach that matches your `flake.nix` setup: diff --git a/tutorial-qml-ui-app.md b/tutorial-qml-ui-app.md index 608900b..ef13caa 100644 --- a/tutorial-qml-ui-app.md +++ b/tutorial-qml-ui-app.md @@ -347,7 +347,7 @@ The `calc_module.url` can be either: - **`github:`** — fetches from a remote GitHub repo. Use this for CI or when `calc_module` has been published. - **`path:`** — points to a local directory on disk. Use this during development when both repos live side by side (e.g., `path:../logos-calc-module`). -> **Important:** Whichever URL scheme you use, `calc_module` must be built with its shared library (`.so` on Linux, `.dylib` on macOS) present in `lib/`. If the library is missing, the nix build will fail with linker errors. See [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library) for build instructions. +> **Important:** `calc_module` bundles a small C library (`libcalc`) that is compiled **from source** during its own Nix build — there's no prebuilt `.so`/`.dylib` to stage. Building `calc_module` (which happens automatically when this app pulls it in as a dependency) compiles it for you. See [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-add-a-makefile-to-build-the-library) for how that works. `mkLogosQmlModule` handles everything — it stages QML files, metadata, and icons into a plugin directory, bundles all module dependencies (direct and transitive) from their LGX packages, and automatically wires up `apps.default` so `nix run .` launches the UI in a standalone window with all required backend modules self-contained. `flakeInputs = inputs` passes all inputs so that dependencies declared in `metadata.json` are resolved automatically. @@ -370,36 +370,20 @@ The app opens immediately. No modules are loaded, so clicking buttons shows "Log ### 5.2 Full functionality (with modules) -The standalone app automatically bundles and loads all module dependencies declared in `metadata.json`. To test with your local `calc_module` from Part 1, you first need to make sure it has been built and its shared library (`.so` on Linux, `.dylib` on macOS) is present. +The standalone app automatically bundles and loads all module dependencies declared in `metadata.json`. To test with your local `calc_module` from Part 1, just make sure it builds cleanly — its C library is compiled from source as part of that build, so there's nothing to pre-build by hand. -#### Ensure `calc_module` is built +#### Ensure `calc_module` builds -Go back to your `logos-calc-module` directory and verify the shared library exists: - -```bash -ls ../logos-calc-module/lib/libcalc.so # Linux -ls ../logos-calc-module/lib/libcalc.dylib # macOS -``` - -If the file is missing, build it first (as covered in [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)): - -```bash -cd ../logos-calc-module/lib -gcc -shared -fPIC -o libcalc.so libcalc.c # Linux -# gcc -shared -fPIC -o libcalc.dylib libcalc.c # macOS -cd ../../logos-calc-ui -``` - -Also make sure the module itself builds successfully: +`calc_module`'s C library is compiled from source during its Nix build (from `lib/libcalc.c` via `lib/Makefile`, see [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-add-a-makefile-to-build-the-library)), so there's no `.so`/`.dylib` to build or place by hand. Confirm the module builds: ```bash cd ../logos-calc-module -git add -A +git add -A # Nix only sees git-tracked files nix build cd ../logos-calc-ui ``` -The `nix build` produces `result/lib/calc_module_plugin.so` (or `.dylib`), which is the compiled Qt plugin. The `lib/libcalc.so` (or `.dylib`) inside the source tree is the underlying C library that gets linked in during the build. +The `nix build` produces `result/lib/calc_module_plugin.so` (or `.dylib`), the compiled Qt plugin, alongside `result/lib/libcalc.so` (or `.dylib`) — the C library built from source and staged next to the plugin. #### Option A: Use `--override-input` (quick, no flake.nix edits) diff --git a/tutorial-wrapping-c-library.md b/tutorial-wrapping-c-library.md index 1960440..ea6e2ec 100644 --- a/tutorial-wrapping-c-library.md +++ b/tutorial-wrapping-c-library.md @@ -23,7 +23,7 @@ This tutorial walks you through wrapping a C shared library (`.so` on Linux, `.d ```bash nix flake --help >/dev/null 2>&1 && echo "Flakes enabled" || echo "Flakes NOT enabled — check nix.conf" ``` -- **A C compiler** (gcc or clang) for building the C library. Only needed if you're building the `.so`/`.dylib` yourself rather than using a pre-built library. +- **A C compiler** (gcc or clang) — only needed for the optional local sanity check in Step 1.5. The Nix build compiles the C library itself, so you don't need a compiler on hand for the actual module build. - Basic familiarity with C and C++. --- @@ -139,28 +139,56 @@ const char* calc_version(void) } ``` -### 1.5 Build the shared library +### 1.5 Add a Makefile to build the library + +The Nix build (Step 3) compiles `libcalc` from source for you — you don't ship a +pre-built binary. To do that it needs a build command, so add a small +`lib/Makefile` that produces the shared library with the platform-correct +extension: + +```make +# Pick the platform-correct shared-library extension. +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + EXT := dylib +else + EXT := so +endif + +# Extra compile flags — overridable from the environment (the Nix stdenv +# exports CC, and may set CFLAGS/LDFLAGS). Note that -shared/-fPIC are passed +# literally on the recipe line, NOT via CFLAGS: if they lived in `CFLAGS ?=`, +# an environment-provided CFLAGS would drop them and you'd build a plain +# executable named libcalc.so/.dylib instead of a shared library. +CFLAGS ?= -O2 + +shared: + mkdir -p build + $(CC) -shared -fPIC $(CFLAGS) $(LDFLAGS) -o build/libcalc.$(EXT) libcalc.c + +clean: + rm -rf build + +.PHONY: shared clean +``` + +> Recipe lines in a `Makefile` must be indented with a **tab**, not spaces. + +**Optional sanity check.** Compile it once locally to confirm your C code builds +and exports the expected symbols (the Nix build does this for real in Step 3): ```bash cd lib - -# Linux -gcc -shared -fPIC -o libcalc.so libcalc.c - -# macOS -# gcc -shared -fPIC -o libcalc.dylib libcalc.c - +make shared # → build/libcalc.so on Linux, build/libcalc.dylib on macOS cd .. ``` -Verify the symbols are exported: - ```bash # Linux -nm -D lib/libcalc.so | grep calc +nm -D lib/build/libcalc.so | grep calc # macOS -# nm -gU lib/libcalc.dylib | grep calc +# nm -gU lib/build/libcalc.dylib | grep calc ``` You should see each symbol marked with `T` (text/code section). Addresses will vary: @@ -173,7 +201,10 @@ You should see each symbol marked with `T` (text/code section). Addresses will v 0000000000001299 T calc_version ``` -> **Wrapping a third-party library?** If you're wrapping an existing library (e.g., from a system package or a GitHub repo), you don't need to write the C code — just place the pre-built `.so`/`.dylib` and its header file in `lib/`. +> **Wrapping a third-party library you already have as a binary?** You can skip +> building from source and ship the pre-built `.so`/`.dylib` instead — see the +> staging note in [Step 2.1](#21-metadatajson--module-configuration). Just +> remember Nix only sees git-tracked files, so the binary **must** be committed. --- @@ -198,7 +229,7 @@ logos-calc-module/ ├── lib/ │ ├── libcalc.h # C library header │ ├── libcalc.c # C library source -│ └── libcalc.so # Pre-built shared library +│ └── Makefile # Builds libcalc from source (run by the Nix build) └── src/ ├── calc_module_interface.h # Interface declaration ├── calc_module_plugin.h # Plugin header @@ -229,7 +260,8 @@ This is the single source of truth for your module. It is embedded into the plug "external_libraries": [ { "name": "calc", - "vendor_path": "lib" + "build_command": "make shared", + "output_pattern": "build/libcalc.*" } ], "cmake": { @@ -246,10 +278,23 @@ This is the single source of truth for your module. It is embedded into the plug | Field | What it does | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | Module name — must be a valid C identifier (used in filenames, method calls) | -| `nix.external_libraries[].name` | Library name **without the `lib` prefix** — the builder looks for `lib.so` / `lib.dylib` in the directory specified by `vendor_path`. So `name: calc` matches the file `libcalc.so` / `libcalc.dylib`. This follows the standard Unix library naming convention where `-lcalc` links against `libcalc`. | -| `nix.external_libraries[].vendor_path` | Where to find the pre-built library. `"lib"` means the `lib/` directory in your project root | -| `nix.cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "lib/libcalc.h"` | +| `name` | Module name — must be a valid C identifier (used in filenames, method calls) | +| `nix.external_libraries[].name` | Library name **without the `lib` prefix**. So `name: calc` corresponds to `libcalc.so` / `libcalc.dylib`, following the standard Unix convention where `-lcalc` links against `libcalc`. The builder uses it to name the built artifact and to match `EXTERNAL_LIBS calc` in `CMakeLists.txt`. | +| `nix.external_libraries[].build_command` | Shell command that builds the library **from source**. It runs in the source tree passed via `externalLibInputs` in `flake.nix` (see [Step 2.3](#23-flakenix--nix-build-config)). Here it invokes the `lib/Makefile`'s `make shared` target. The Nix stdenv exports `$CC`/`$CXX`, and `make`/`pkg-config` are on `PATH`. | +| `nix.external_libraries[].output_pattern` | Glob (relative to the build dir) the builder uses to locate the compiled library. `"build/libcalc.*"` matches the `build/libcalc.so` / `build/libcalc.dylib` that `make shared` produces. | +| `nix.cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "lib/libcalc.h"` | + +> **Build from source vs. ship a binary — and why you must `git add` a binary.** +> This example builds `libcalc` from source on every Nix build, so there's +> nothing to commit but the `.c`/`.h`/`Makefile` sources. If instead you receive +> a library **pre-built** (a third-party `.so`/`.dylib`), drop `build_command` +> and `output_pattern`, set `"vendor_path": "lib"`, and place the binary in +> `lib/` — **but you must `git add` it.** Nix flakes only see git-tracked files, +> so an un-staged binary is invisible to the build, and the failure is *silent*: +> `find_library` misses it (a CMake warning, not an error), the plugin still +> links because the C symbols resolve lazily, and it only crashes when something +> tries to load it. Always commit a vendored binary, or build it from source as +> shown here. ### 2.2 `CMakeLists.txt` — Build File @@ -291,11 +336,11 @@ The `if/elseif/else` block above it is boilerplate — don't change it. > **Common mistake:** If `NAME` doesn't match `name` in `metadata.json`, the build will succeed but the install phase will fail because it looks for `_plugin.dylib` based on `metadata.json`. -**How `EXTERNAL_LIBS calc` works:** The `logos_module()` CMake function searches `lib/` for `libcalc.so` (Linux) or `libcalc.dylib` (macOS), links it to your plugin, and sets up RPATH so the library is found at runtime. +**How `EXTERNAL_LIBS calc` works:** Before CMake runs, the builder has compiled `libcalc` from source (Step 2.1) and staged the result into `lib/`. The `logos_module()` CMake function then finds `libcalc.so` (Linux) or `libcalc.dylib` (macOS) there, links it to your plugin, and sets up RPATH so the library is found at runtime. ### 2.3 `flake.nix` — Nix Build Config -> **Edit:** Change `description`. Add flake inputs here if your module depends on other modules or fetches a library from source (see [Advanced: Wrapping a Library from a Flake Input](#advanced-wrapping-a-library-from-a-flake-input)). +> **Edit:** Change `description`, and point `externalLibInputs` at your library's source. Add other flake inputs here too if your module depends on other modules (see [Advanced: Wrapping a Library from a Flake Input](#advanced-wrapping-a-library-from-a-flake-input)). ```nix { @@ -303,18 +348,38 @@ The `if/elseif/else` block above it is boilerplate — don't change it. inputs = { logos-module-builder.url = "github:logos-co/logos-module-builder/tutorial-v2"; + + # The C library source, built from source by Nix (see metadata.json's + # build_command). flake = false means "just give me the source tree". + calc-src = { + url = "path:./lib"; + flake = false; + }; }; - outputs = inputs@{ logos-module-builder, ... }: + outputs = inputs@{ logos-module-builder, calc-src, ... }: logos-module-builder.lib.mkLogosModule { src = ./.; configFile = ./metadata.json; flakeInputs = inputs; + + # Hand the C source to the builder. The attribute name (calc) must match + # the external_libraries[].name in metadata.json. + externalLibInputs = { + calc = calc-src; + }; }; } ``` -That's it — `mkLogosModule` handles all the Nix complexity (fetching Qt, the SDK, the code generator, setting up include paths, etc.). Note that `configFile` points to `metadata.json` (the single source of truth) and `flakeInputs = inputs` passes all flake inputs to the builder so that dependencies declared in `metadata.json` are resolved automatically. +That's it — `mkLogosModule` handles all the Nix complexity (fetching Qt, the SDK, the code generator, setting up include paths, etc.). `configFile` points to `metadata.json` (the single source of truth), `flakeInputs = inputs` passes all flake inputs to the builder so module dependencies resolve automatically, and `externalLibInputs` hands the builder the C source it compiles via `build_command`. + +> **Bundled source via `path:./lib`.** The `calc-src` input is a relative +> [path input](https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake#path-like-syntax) +> pointing at the `lib/` directory in this same repo, so the C source stays +> bundled with the module — no separate repository needed. To pull the source +> from a subfolder of a remote repo instead, use the `?dir=` locator, e.g. +> `url = "github:you/your-repo?dir=lib";`. > **Naming flake inputs:** When adding module dependencies, the flake input attribute name **must match** the `name` field in that dependency's `metadata.json`. For example, if you depend on a module whose `metadata.json` has `"name": "waku_module"`, your flake input must be `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. @@ -518,6 +583,10 @@ result-* # CMake build directory build/ + +# Compiled C library (built from source by the Nix external-lib derivation, +# or by `make shared` for a local sanity check) +lib/build/ ``` Then initialise the repo: @@ -555,7 +624,7 @@ You should see two files (extensions depend on your platform): ``` # Linux calc_module_plugin.so # Your Logos module plugin -libcalc.so # The C library (copied alongside) +libcalc.so # The C library (built from source, staged alongside) # macOS calc_module_plugin.dylib @@ -885,7 +954,7 @@ Q_INVOKABLE QString getData() { ## Advanced: Wrapping a Library from a Flake Input -Instead of pre-building the library and placing it in `lib/`, you can have Nix fetch and build it from source. This is useful for libraries hosted on GitHub. +The main example already builds its library from source via `externalLibInputs`, using a relative `path:./lib` input for the bundled `lib/` directory. The same mechanism works when the library source lives in its **own GitHub repository** — you just point the input at the remote instead of a local path. This is the common case when wrapping a third-party C/C++ library. ### flake.nix with external library input @@ -1058,8 +1127,10 @@ The first `nix build` downloads Qt 6, the Logos C++ SDK, the code generator, and ### Symbol not found errors -If you get "undefined symbol" errors for your C library functions: +If the plugin fails to load with "undefined symbol" errors for your C library functions, the library wasn't linked. The build does **not** fail in this case — `find_library` only prints a warning and the plugin links anyway, so the breakage surfaces at load time. Check: -1. Verify the `.so`/`.dylib` is in `lib/` before building -2. Verify the header has `extern "C"` guards -3. Check the symbols are exported: `nm -D lib/libcalc.so | grep calc` +1. The library actually built — the build log should show a `logos-external-` derivation running your `build_command` and a `Found: build/lib...` line. +2. `output_pattern` matches what the build produces (`build/libcalc.*` ↔ `build/libcalc.{so,dylib}`). +3. The header has `extern "C"` guards. +4. The symbols are exported: `nm -gU lib/build/libcalc.dylib | grep calc` (macOS) / `nm -D lib/build/libcalc.so | grep calc` (Linux). +5. If you vendored a **prebuilt** binary (`vendor_path`) instead of building from source, confirm it is committed — Nix can't see an un-staged file, and `find_library` misses it silently. Verify the plugin actually links the library: `otool -L result/lib/calc_module_plugin.dylib | grep calc` (macOS) / `ldd result/lib/calc_module_plugin.so | grep calc` (Linux) should list `libcalc`. If it doesn't, the library wasn't found at build time.