mirror of
https://github.com/logos-co/logos-module-builder.git
synced 2026-08-27 18:21:16 +00:00
281 lines
11 KiB
Nix
281 lines
11 KiB
Nix
# ui_qml module builder — QML view + optional C++ backend (process-isolated).
|
|
# Calls buildCppPlugin only when config.main is set; the resulting `combined`
|
|
# output bundles the plugin .so (when present) with the QML view directory.
|
|
{ nixpkgs, lib, common, parseMetadata, logos-cpp-sdk, logos-protocol ? null, logos-qt-sdk ? null, logos-module, uiBackend, coreBackend, nix-bundle-lgx, nix-bundle-logos-module-install, logos-standalone-app }:
|
|
|
|
{
|
|
# Required: Path to the module source
|
|
src,
|
|
|
|
# Required: Path to the metadata.json configuration file
|
|
configFile,
|
|
|
|
# Optional: all flake inputs — dependencies in metadata.json are resolved automatically
|
|
flakeInputs ? {},
|
|
|
|
# Optional: Additional flake inputs for external libraries
|
|
externalLibInputs ? {},
|
|
|
|
# Optional: Extra build inputs to add
|
|
extraBuildInputs ? [],
|
|
|
|
# Optional: Extra native build inputs to add
|
|
extraNativeBuildInputs ? [],
|
|
|
|
# Optional: Override any config values
|
|
configOverrides ? {},
|
|
|
|
# Optional: Custom preConfigure hook
|
|
preConfigure ? "",
|
|
|
|
# Optional: Custom postInstall hook
|
|
postInstall ? "",
|
|
|
|
# Optional: override the logos-standalone-app used for `nix run`.
|
|
logosStandalone ? null,
|
|
}:
|
|
|
|
let
|
|
# Parse metadata first so we can decide whether to build a C++ backend at all.
|
|
rawConfig = parseMetadata.parseModuleConfig (builtins.readFile configFile);
|
|
config = common.recursiveMerge [ rawConfig configOverrides ];
|
|
|
|
# Validate: view modules must be type "ui_qml" with a "view" field.
|
|
# The "main" backend library is OPTIONAL — if absent, the module is QML-only and
|
|
# is loaded directly in-process by basecamp/standalone (no ui-host process).
|
|
_ = assert config.type == "ui_qml" || builtins.throw
|
|
"mkLogosQmlModule: metadata.json type must be \"ui_qml\", got \"${config.type}\"";
|
|
assert config.view != null || builtins.throw
|
|
"mkLogosQmlModule: metadata.json must specify a \"view\" field (e.g. \"qml/Main.qml\")";
|
|
null;
|
|
|
|
# Whether this module has a backend C++ plugin. QML-only modules omit "main".
|
|
hasBackend = config.main != null;
|
|
|
|
# Delegate compilation to the shared build pipeline (only when there's a backend).
|
|
buildCppPlugin = import ./buildCppPlugin.nix {
|
|
inherit nixpkgs lib common parseMetadata logos-cpp-sdk logos-protocol logos-qt-sdk logos-module uiBackend coreBackend nix-bundle-lgx nix-bundle-logos-module-install;
|
|
};
|
|
|
|
built =
|
|
if hasBackend
|
|
then buildCppPlugin {
|
|
inherit src configFile flakeInputs externalLibInputs extraBuildInputs
|
|
extraNativeBuildInputs configOverrides preConfigure postInstall;
|
|
}
|
|
else null;
|
|
|
|
# The QML view directory is derived from the "view" field (e.g. "qml/Main.qml" -> "qml")
|
|
viewDir = builtins.dirOf config.view;
|
|
|
|
mkStandaloneApp = import ./mkStandaloneApp.nix;
|
|
|
|
forAllSystems = f: lib.genAttrs common.systems (system: f system);
|
|
|
|
# pkgs accessor that works whether or not buildCppPlugin ran
|
|
pkgsFor = system:
|
|
if hasBackend
|
|
then built.perSystem.${system}.pkgs
|
|
else import nixpkgs { inherit system; };
|
|
|
|
# Helper: create a combined derivation from a plugin lib + QML view from source.
|
|
# Used for both default and portable variants. For QML-only modules, pluginLib is null.
|
|
iconFiles = lib.optional (config.icon != null) (src + "/${config.icon}");
|
|
|
|
mkCombined = system: pluginLib: suffix:
|
|
let pkgs = pkgsFor system;
|
|
iconInstall = pkgs.lib.concatStringsSep "\n" (map (icon: ''
|
|
install -D -m644 ${icon} $out/lib/${config.icon}
|
|
'') iconFiles);
|
|
in (pkgs.runCommand "logos-${config.name}-module${suffix}" {} ''
|
|
mkdir -p $out/lib
|
|
|
|
${lib.optionalString (pluginLib != null) ''
|
|
# Copy library files (not symlinks)
|
|
if [ -d "${pluginLib}/lib" ]; then
|
|
cp -rL ${pluginLib}/lib/* $out/lib/
|
|
fi
|
|
''}
|
|
|
|
# Include metadata.json and icons in the output
|
|
cp ${configFile} $out/lib/metadata.json
|
|
${iconInstall}
|
|
|
|
# Copy QML view files from source.
|
|
# C++ modules keep QML under src/ (e.g. src/qml/Main.qml);
|
|
# QML-only modules may keep them at the project root (e.g. Main.qml or qml/Main.qml).
|
|
# When viewDir is "." we only copy the single QML file to avoid pulling in
|
|
# the entire project root (which would conflict with metadata.json above).
|
|
if [ -d "${src}/src/${viewDir}" ] && [ "${viewDir}" != "." ]; then
|
|
mkdir -p "$out/lib/${viewDir}"
|
|
cp -r "${src}/src/${viewDir}/." "$out/lib/${viewDir}/"
|
|
echo "Copied QML view directory from src/${viewDir}"
|
|
elif [ -d "${src}/${viewDir}" ] && [ "${viewDir}" != "." ]; then
|
|
mkdir -p "$out/lib/${viewDir}"
|
|
cp -r "${src}/${viewDir}/." "$out/lib/${viewDir}/"
|
|
echo "Copied QML view directory from ${viewDir}"
|
|
elif [ -f "${src}/src/${config.view}" ]; then
|
|
cp "${src}/src/${config.view}" "$out/lib/${config.view}"
|
|
echo "Copied QML entry file from src/${config.view}"
|
|
elif [ -f "${src}/${config.view}" ]; then
|
|
cp "${src}/${config.view}" "$out/lib/${config.view}"
|
|
echo "Copied QML entry file: ${config.view}"
|
|
else
|
|
echo "Warning: QML view '${config.view}' not found in source"
|
|
fi
|
|
|
|
# Auto-generate a qmldir at the entry directory declaring a unique
|
|
# per-module URI. Qt reads this qmldir when the entry file's implicit
|
|
# "." import loads, and uses its `module` line as the import's URI —
|
|
# non-empty and per-module. Without this, Qt's process-global
|
|
# composite-type name cache (keyed by (name, uri)) would let
|
|
# same-basename types (e.g. two Card.qml files in two different
|
|
# modules) cross-match across engines loaded in the same host process,
|
|
# producing the "Invalid null URL" cascade
|
|
#
|
|
# Skipped if the author already shipped a qmldir at the entry dir.
|
|
QMLDIR_TARGET="$out/lib/${viewDir}/qmldir"
|
|
if [ ! -f "$QMLDIR_TARGET" ]; then
|
|
mkdir -p "$(dirname "$QMLDIR_TARGET")"
|
|
echo "module com.logos.module.${config.name}" > "$QMLDIR_TARGET"
|
|
echo "Generated qmldir at ${viewDir}/qmldir (module com.logos.module.${config.name})"
|
|
else
|
|
echo "Author-provided qmldir at ${viewDir}/qmldir preserved"
|
|
fi
|
|
'') // { inherit src; version = config.version; };
|
|
|
|
# Package outputs
|
|
packages = forAllSystems (system:
|
|
let
|
|
moduleLib =
|
|
if hasBackend then built.perSystem.${system}.moduleLib else null;
|
|
moduleLibPortable =
|
|
if hasBackend then built.perSystem.${system}.moduleLibPortable else null;
|
|
|
|
combined = mkCombined system moduleLib "";
|
|
combinedPortable =
|
|
if moduleLibPortable != null
|
|
then mkCombined system moduleLibPortable "-portable"
|
|
else null;
|
|
|
|
in {
|
|
# Default: lib/ layout for both backend and QML-only modules.
|
|
default = combined;
|
|
} // lib.optionalAttrs hasBackend {
|
|
"${config.name}-lib" = moduleLib;
|
|
lib = moduleLib;
|
|
|
|
# Ready-to-build codebase: all code generators run, emitted as a source
|
|
# tree (nix build .#generate). Only for modules with a C++ backend —
|
|
# QML-only modules have no generators to run.
|
|
generate = built.perSystem.${system}.moduleGenerate;
|
|
"${config.name}-generate" = built.perSystem.${system}.moduleGenerate;
|
|
} // lib.optionalAttrs (moduleLibPortable != null) {
|
|
"${config.name}-lib-portable" = moduleLibPortable;
|
|
lib-portable = moduleLibPortable;
|
|
}
|
|
);
|
|
|
|
# LGX packages — bundle the combined output (plugin + QML), not just moduleLib.
|
|
# This ensures the QML view directory is included in the .lgx package so that
|
|
# lgpm install and mkStandaloneApp LGX extraction both have the QML files.
|
|
lgxPackages = forAllSystems (system:
|
|
let
|
|
bundleLgx = nix-bundle-lgx.bundlers.${system}.default;
|
|
bundleLgxPortable = nix-bundle-lgx.bundlers.${system}.portable;
|
|
installDev = nix-bundle-logos-module-install.bundlers.${system}.dev;
|
|
installPortable = nix-bundle-logos-module-install.bundlers.${system}.portable;
|
|
|
|
moduleLib =
|
|
if hasBackend then built.perSystem.${system}.moduleLib else null;
|
|
moduleLibPortable =
|
|
if hasBackend then built.perSystem.${system}.moduleLibPortable else null;
|
|
|
|
combined = mkCombined system moduleLib "";
|
|
# Use the portable-linked plugin + QML for portable bundles when available
|
|
combinedForPortable =
|
|
if moduleLibPortable != null
|
|
then mkCombined system moduleLibPortable "-portable"
|
|
else combined;
|
|
in {
|
|
lgx = bundleLgx combined;
|
|
install = installDev combined;
|
|
lgx-portable = bundleLgxPortable combinedForPortable;
|
|
install-portable = installPortable combinedForPortable;
|
|
}
|
|
);
|
|
|
|
# Resolve the standalone app: explicit override > built-in from module-builder
|
|
resolvedStandalone =
|
|
if logosStandalone != null then logosStandalone
|
|
else logos-standalone-app;
|
|
|
|
apps = forAllSystems (system:
|
|
let
|
|
pkgs = import nixpkgs { inherit system; };
|
|
# Collect all module dependencies (direct + transitive) for bundling
|
|
allDeps = common.collectAllModuleDeps system flakeInputs config.dependencies;
|
|
in {
|
|
default = mkStandaloneApp {
|
|
inherit pkgs;
|
|
standalone = resolvedStandalone.packages.${system}.default;
|
|
plugin = packages.${system}.default;
|
|
metadataFile = configFile;
|
|
dirName = "logos-${config.name}-plugin-dir";
|
|
format = if hasBackend then "qt-plugin" else "qml";
|
|
moduleDeps = allDeps;
|
|
};
|
|
}
|
|
);
|
|
|
|
# Auto-detect UI integration tests: scan tests/ for .mjs files and produce
|
|
# an integration-test package using logos-standalone-app's mkPluginTest.
|
|
testsDir = src + "/tests";
|
|
hasTestsDir = builtins.pathExists testsDir;
|
|
testFiles =
|
|
if hasTestsDir then
|
|
let
|
|
entries = builtins.attrNames (builtins.readDir testsDir);
|
|
mjsFiles = builtins.filter (name: lib.hasSuffix ".mjs" name) entries;
|
|
in map (name: testsDir + "/${name}") mjsFiles
|
|
else [];
|
|
hasUiTests = testFiles != [];
|
|
|
|
integrationTestPackages = lib.optionalAttrs hasUiTests (forAllSystems (system:
|
|
let
|
|
mkPluginTest = resolvedStandalone.lib.${system}.mkPluginTest;
|
|
pkgs = pkgsFor system;
|
|
allDeps = common.collectAllModuleDeps system flakeInputs config.dependencies;
|
|
in {
|
|
integration-test = mkPluginTest {
|
|
inherit pkgs testFiles;
|
|
pluginPkg = packages.${system}.default;
|
|
moduleDeps = allDeps;
|
|
name = "${config.name}-integration-test";
|
|
};
|
|
|
|
# Expose logos-qt-mcp so modules can build the test framework locally:
|
|
# nix build .#test-framework -o result-mcp
|
|
test-framework = resolvedStandalone.packages.${system}.logos-qt-mcp;
|
|
}
|
|
));
|
|
|
|
# Merge view-module-specific LGX outputs and integration tests into packages
|
|
mergedPackages = lib.mapAttrs (system: sysPkgs:
|
|
sysPkgs // (lgxPackages.${system} or {}) // (integrationTestPackages.${system} or {})
|
|
) packages;
|
|
|
|
in {
|
|
packages = mergedPackages;
|
|
checks = lib.mapAttrs (_: sysPkgs: {
|
|
integration-test = sysPkgs.integration-test;
|
|
}) integrationTestPackages;
|
|
devShells =
|
|
if hasBackend
|
|
then built.devShells
|
|
else lib.genAttrs common.systems (system:
|
|
{ default = (pkgsFor system).mkShell {}; });
|
|
inherit apps config;
|
|
metadataJson = builtins.readFile configFile;
|
|
}
|