mirror of
https://github.com/logos-co/logos-module-builder.git
synced 2026-08-29 19:21:10 +00:00
modulePreConfigure.nix switches the `--backend ui` invocation to
logos-view-generator (logos-view-module), which now owns that emitter and
the logos_ui_plugin_context.h it pairs with.
THE REASON THIS IS NOT COSMETIC. This builder pins logos-qt-sdk at 4a1104c,
which predates b7b82e5 — the commit that added the module teardown hook to
the ui emitter. At 4a1104c BOTH halves lack it, so every build has been
green and self-consistent, and the hook has NEVER REACHED A SINGLE SHIPPED
ui_qml MODULE. Measured on the same module, before and after, by loading the
built plugin with QPluginLoader and dumping its QMetaObject:
BASELINE MIGRATED
initLogos(LogosAPI*) unloadFinished() [signal]
initLogos(LogosAPI*)
int aboutToUnload() [invokable]
So this delivers the hook for the first time rather than preserving it.
LOGOS_VIEW_INCLUDE_DIR is added BEFORE the qt-sdk root in
LogosModule.cmake, so the emitter and its header resolve from ONE pin. That
ordering is load-bearing until logos-qt-sdk stops installing its copy of
logos_ui_plugin_context.h — anything going through logos_module() is safe;
a hand-run cmake putting LOGOS_QT_SDK_ROOT first would silently get the
wrong header. Called out in the code rather than left implicit.
Blast radius is 7 modules, not one: package_manager_ui, chat_ui, wallet_ui,
test_uiqml_probe, test_fullapi_ui, calc_ui_cpp and templates/ui-qml-backend
all match ui_qml + interface:universal. None override aboutToUnload(), and
the base default is Synchronous, so every one answers 0 and no host waits —
the change is additive. Only package_manager_ui was built end to end.
Three assertions in tests/test-module-pre-configure.nix pin the binary
choice; repointing at logos-qt-generator fails them. Also corrects a comment
there that claimed "the ui backend still legitimately uses qt-sdk's".
7/7 checks green, 399 unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
296 lines
12 KiB
Nix
296 lines
12 KiB
Nix
# mkLogosModuleTests — build and run unit tests for a Logos module
|
|
#
|
|
# This integrates with logos-test-framework and the module build system.
|
|
# Tests are built as a standalone executable using LogosTest.cmake,
|
|
# with the SDK in mock mode and optional C library mocking.
|
|
#
|
|
# Usage in a module's flake.nix:
|
|
#
|
|
# checks.${system}.unit-tests = logos-module-builder.lib.mkLogosModuleTests {
|
|
# src = ./.;
|
|
# testDir = ./tests;
|
|
# configFile = ./metadata.json;
|
|
# flakeInputs = inputs;
|
|
# mockCLibs = ["gowalletsdk"]; # optional
|
|
# };
|
|
{ nixpkgs, lib, common, parseMetadata, logos-cpp-sdk, logos-protocol, logos-qt-sdk, logos-plugin-qt ? null, logos-view-module, logos-test-framework }:
|
|
|
|
let
|
|
modulePreConfigure = import ./modulePreConfigure.nix { inherit lib; };
|
|
in
|
|
|
|
{
|
|
# Required: Path to the module source
|
|
src,
|
|
|
|
# Required: Path to the tests directory
|
|
testDir,
|
|
|
|
# Optional: Path to metadata.json
|
|
configFile ? null,
|
|
|
|
# Optional: All flake inputs — module dependencies resolved from metadata
|
|
flakeInputs ? {},
|
|
|
|
# Optional: Additional flake inputs for external libraries (same format as mkLogosModule)
|
|
externalLibInputs ? {},
|
|
|
|
# Optional: C libraries to mock (won't link the real lib)
|
|
mockCLibs ? [],
|
|
|
|
# Optional: Custom preConfigure hook
|
|
preConfigure ? "",
|
|
|
|
# Optional: Extra build inputs
|
|
extraBuildInputs ? [],
|
|
|
|
# Optional: Extra CMake flags
|
|
extraCmakeFlags ? [],
|
|
}:
|
|
|
|
let
|
|
forAllSystems = f: lib.genAttrs common.systems (system: f system);
|
|
|
|
# Parse config if available (defaults must satisfy parseModuleConfig shape consumers).
|
|
#
|
|
# Per-system, because everything this file reads off it — nix_packages.runtime,
|
|
# dependencies, go_static_lib_names — is platform-keyable, and all of it is
|
|
# read inside `checks` below where the target IS known. The literal fallback
|
|
# takes a platform too: it can never contain a `platforms` block, but leaving
|
|
# one call site on the old shape would leave a permanently-unmigrated example
|
|
# in the tree for the next person to copy.
|
|
configFor = system: parseMetadata.parseModuleConfig {
|
|
platform = parseMetadata.platformForSystem system;
|
|
json = if configFile != null
|
|
then builtins.readFile configFile
|
|
else ''{"name":"unknown","version":"0.0.0"}'';
|
|
};
|
|
|
|
checks = forAllSystems (system:
|
|
let
|
|
pkgs = common.mkPkgs system;
|
|
config = configFor system;
|
|
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
|
# Build-platform half of the SDK. logos-cpp-generator is invoked by BARE
|
|
# NAME from a build phase (logos-plugin-qt/lib/buildPlugin.nix:145), so it
|
|
# must run on the builder. Under cross, packages.x86_64-windows.default
|
|
# carries no runnable generator at all -- logos-cpp-sdk/nix/bin.nix:39
|
|
# silently skips the mingw .exe -- hence "command not found".
|
|
#
|
|
# `logosSdk` deliberately stays TARGET-typed: it is ALSO the header and
|
|
# CMake-package root passed to LOGOS_CPP_SDK_ROOT, and those must keep
|
|
# coming from the Windows set. Splitting the two roles is the whole point;
|
|
# pointing the headers at the build system would produce a build that
|
|
# SUCCEEDS while linking the wrong architecture.
|
|
#
|
|
# buildSystemFor is the identity on every native system, so this is a
|
|
# no-op off the Windows target.
|
|
logosSdkBuild = logos-cpp-sdk.packages.${common.buildSystemFor system}.default;
|
|
logosQtSdk = logos-qt-sdk.packages.${system}.default;
|
|
# The Qt HOST RUNTIME a test binary links (LogosAPI and the provider
|
|
# objects). logos-test-framework's LogosTest.cmake takes it from
|
|
# LOGOS_QT_HOST_ROOT and from nowhere else; LOGOS_QT_SDK_ROOT is passed
|
|
# alongside purely for the Qt-typed headers logos-qt-sdk alone ships.
|
|
logosQtHost = logos-plugin-qt.packages.${system}.logos-qt-host;
|
|
# The Qt glue generator (universal/cdylib/ui backends) — Qt code is
|
|
# the Qt layer's product; logos-cpp-generator keeps Qt-free outputs.
|
|
logosQtGenerator = logos-qt-sdk.packages.${common.buildSystemFor system}.logos-qt-generator;
|
|
# The cdylib Qt-plugin glue generator lives in logos-plugin-qt (the Qt
|
|
# plugin BACKEND owns the glue; the SDK does not). logos-qt-sdk still
|
|
# ships an older copy of the SAME emitter, and calling that one is not a
|
|
# compile error — it silently emits STALE glue. That is how a
|
|
# host-services grant went undelivered while every build stayed green.
|
|
logosQtHostGenerator =
|
|
logos-plugin-qt.packages.${common.buildSystemFor system}.logos-qt-host-generator;
|
|
# The VIEW plugin glue generator (`--backend ui`), from logos-view-module.
|
|
# Needed HERE too, not just in the plugin build: compose below runs
|
|
# autoCodegen, which for a `type: ui_qml` module is the ui backend. Before
|
|
# the emitter moved, this path got it for free from logos-qt-generator.
|
|
logosViewGenerator =
|
|
logos-view-module.packages.${common.buildSystemFor system}.logos-view-generator;
|
|
# Same pin as the generator: the emitted glue calls into this header, so
|
|
# the two must never come from different revisions. See buildCppPlugin.nix.
|
|
logosViewInclude =
|
|
logos-view-module.packages.${common.buildSystemFor system}.include;
|
|
logosProtocolPkg = logos-protocol.packages.${system}.default;
|
|
testFramework = logos-test-framework.packages.${system}.default;
|
|
|
|
# Resolve runtime packages from metadata (e.g. nlohmann_json)
|
|
runtimePkgNames = config.nix_packages.runtime or [];
|
|
runtimePkgs = builtins.filter (p: p != null) (map (name:
|
|
pkgs.${name} or null
|
|
) runtimePkgNames);
|
|
|
|
# Resolve module dependencies
|
|
moduleInputs = lib.filterAttrs (n: _: builtins.elem n config.dependencies) flakeInputs;
|
|
resolvedModuleDeps = lib.mapAttrs (_: input:
|
|
if input ? packages.${system}.default then input.packages.${system}.default else input
|
|
) moduleInputs;
|
|
|
|
# Copy include files from module deps
|
|
depIncludeSetup = lib.concatMapStringsSep "\n" (name:
|
|
let dep = resolvedModuleDeps.${name} or null;
|
|
in if dep != null then ''
|
|
if [ -d "${dep}/include" ]; then
|
|
echo "Copying include files from ${name}..."
|
|
cp -r "${dep}/include"/* ./generated_code/ 2>/dev/null || true
|
|
fi
|
|
'' else ""
|
|
) config.dependencies;
|
|
|
|
# Resolve testDir to a relative path within the source tree.
|
|
# testDir is typically a nix path like ./tests; we need to find
|
|
# which subdirectory name it corresponds to.
|
|
testDirName = builtins.baseNameOf (builtins.toString testDir);
|
|
|
|
# Mocked C libraries are replaced at link time by the module's mock
|
|
# sources, so the real external library must never be referenced — let
|
|
# alone realized — for the test derivation. Resolving a mocked lib here
|
|
# would force Nix to build the upstream library (e.g. a risc0/Metal crate
|
|
# that can't compile in the sandbox) even though the test never links it.
|
|
nonMockedExternalLibInputs =
|
|
lib.filterAttrs (name: _: ! lib.elem name mockCLibs) externalLibInputs;
|
|
|
|
resolvedExternalLibs = lib.mapAttrs (name: value:
|
|
if builtins.isAttrs value && value ? input
|
|
then value.input.packages.${system}.${value.packages.default or "default"}
|
|
else value
|
|
) nonMockedExternalLibInputs;
|
|
|
|
externalLibRpath = lib.concatMapStringsSep ":" (name:
|
|
"${resolvedExternalLibs.${name}}/lib"
|
|
) (builtins.attrNames resolvedExternalLibs);
|
|
|
|
# Loader search path for the test run. Test binaries execute in place during
|
|
# buildPhase, so any runtime dependency that a real (non-mocked) library
|
|
# dlopens by bare name — e.g. libpq pulled in by a module's C library — must
|
|
# be reachable via the dynamic loader. Those deps are the runtime nix
|
|
# packages declared in metadata (nix.packages.runtime) plus the resolved
|
|
# external libraries. Linked deps already resolve via CMAKE_INSTALL_RPATH;
|
|
# this covers the dlopen-by-bare-name case the rpath cannot.
|
|
testRuntimeLibPath =
|
|
lib.makeLibraryPath (runtimePkgs ++ lib.attrValues resolvedExternalLibs);
|
|
|
|
userPreConfigure =
|
|
if builtins.isFunction preConfigure
|
|
then preConfigure { externalLibs = resolvedExternalLibs; }
|
|
else preConfigure;
|
|
|
|
preConfigureStr = modulePreConfigure.compose {
|
|
inherit config;
|
|
externalLibs = resolvedExternalLibs;
|
|
userPre = userPreConfigure;
|
|
fixDarwin = true;
|
|
copyExternals = true;
|
|
};
|
|
|
|
goLibsForTest = lib.filter (n: ! lib.elem n mockCLibs) config.go_static_lib_names;
|
|
goCmakeTestFlags = lib.optionals (goLibsForTest != []) [
|
|
"-DLOGOS_MODULE_GO_STATIC_LIBS=${lib.concatStringsSep ";" goLibsForTest}"
|
|
];
|
|
|
|
in {
|
|
unit-tests = pkgs.stdenv.mkDerivation {
|
|
pname = "logos-${config.name}-tests";
|
|
version = config.version;
|
|
|
|
src = src;
|
|
|
|
nativeBuildInputs = with pkgs; [
|
|
cmake
|
|
ninja
|
|
pkg-config
|
|
qt6.wrapQtAppsNoGuiHook
|
|
logosSdkBuild
|
|
logosQtGenerator
|
|
logosQtHostGenerator
|
|
logosViewGenerator
|
|
] ++ extraBuildInputs;
|
|
|
|
buildInputs = with pkgs; [
|
|
qt6.qtbase
|
|
qt6.qtremoteobjects
|
|
logosSdk
|
|
logosQtSdk
|
|
logosQtHost
|
|
logosProtocolPkg
|
|
testFramework
|
|
] ++ runtimePkgs;
|
|
|
|
dontUseCmakeConfigure = true;
|
|
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
|
|
# Set up generated code directory
|
|
mkdir -p ./generated_code
|
|
|
|
# Copy dependency includes
|
|
${depIncludeSetup}
|
|
|
|
# Run logos-cpp-generator if metadata available
|
|
${lib.optionalString (configFile != null) ''
|
|
if command -v logos-cpp-generator &>/dev/null; then
|
|
echo "Running logos-cpp-generator..."
|
|
logos-cpp-generator --metadata "${configFile}" --general-only --output-dir ./generated_code || true
|
|
fi
|
|
''}
|
|
|
|
# Custom preConfigure hook
|
|
${preConfigureStr}
|
|
|
|
# CMake configure + build from within the source tree
|
|
mkdir -p build && cd build
|
|
cmake ../${testDirName} \
|
|
-DLOGOS_CPP_SDK_ROOT=${logosSdk} \
|
|
-DLOGOS_QT_SDK_ROOT=${logosQtSdk} \
|
|
-DLOGOS_QT_HOST_ROOT=${logosQtHost} \
|
|
-DLOGOS_PROTOCOL_ROOT=${logosProtocolPkg} \
|
|
-DLOGOS_VIEW_INCLUDE_DIR=${logosViewInclude} \
|
|
-DLOGOS_TEST_FRAMEWORK_ROOT=${testFramework} \
|
|
-DCMAKE_MODULE_PATH=${testFramework}/cmake \
|
|
${lib.optionalString (externalLibRpath != "") "-DCMAKE_INSTALL_RPATH=${externalLibRpath} -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON"} \
|
|
${lib.concatMapStringsSep " " (f: f) (goCmakeTestFlags ++ extraCmakeFlags)}
|
|
cmake --build . --parallel $NIX_BUILD_CORES
|
|
|
|
${lib.optionalString (testRuntimeLibPath != "") ''
|
|
# Make runtime dependencies (e.g. libpq) discoverable for real
|
|
# libraries that dlopen them by bare name during the test run.
|
|
export LD_LIBRARY_PATH="${testRuntimeLibPath}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
|
export DYLD_LIBRARY_PATH="${testRuntimeLibPath}''${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
|
|
''}
|
|
|
|
# Run all test binaries (unit tests first, integration tests last)
|
|
echo "Running ${config.name} tests..."
|
|
{
|
|
find . -maxdepth 1 -type f -executable \( -name "*_tests" -o -name "*_test" \) ! -name "*integration*"
|
|
find . -maxdepth 1 -type f -executable \( -name "*_tests" -o -name "*_test" \) -name "*integration*"
|
|
} | while read bin; do
|
|
echo "Executing: $bin"
|
|
"$bin"
|
|
done
|
|
|
|
# Save build directory for installPhase (avoid global /tmp — sandbox issues)
|
|
echo "$(pwd)" > "$TMPDIR/logos-test-build-dir-path"
|
|
|
|
runHook postBuild
|
|
'';
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
mkdir -p $out/bin
|
|
|
|
BUILD_DIR="$(cat "$TMPDIR/logos-test-build-dir-path" 2>/dev/null || echo build)"
|
|
find "$BUILD_DIR" -maxdepth 1 -type f -executable \( -name "*_tests" -o -name "*_test" \) | while read bin; do
|
|
cp "$bin" $out/bin/
|
|
done
|
|
runHook postInstall
|
|
'';
|
|
|
|
doCheck = false;
|
|
};
|
|
}
|
|
);
|
|
|
|
in checks
|