mirror of
https://github.com/logos-co/logos-plugin-qt.git
synced 2026-08-27 08:51:07 +00:00
Three changes, all needed before any module can target mingw: * meta.platforms gained windows in the three generated derivations. Without it every module fails at EVALUATION with "not available on the requested hostPlatform". * getPluginFilename returned ".so" on Windows while getLibExtension two lines above already answered "dll". They disagreed because each carried its own copy of the platform branch, so the attribute set is now `rec` and getPluginFilename REUSES getLibExtension rather than repeating it. * wrapQtAppsNoGuiHook is gated behind !isWindows and dontWrapQtApps is set on both plugin derivations. Both halves are required and neither is optional: the hook does not even EVALUATE for a mingw host, and would be inert anyway (wrap-qt-apps-hook skips anything that is not ELF or Mach-O), while qtbase's own setup hook hard-errors in qtPreHook unless dontWrapQtApps is set.
414 lines
16 KiB
Nix
414 lines
16 KiB
Nix
# Builder for the module library (Qt plugin .so/.dylib)
|
|
# This is the Qt-specific implementation of the plugin build step.
|
|
{ lib, common }:
|
|
|
|
let
|
|
# Shared generation logic for `build` and `generate`.
|
|
#
|
|
# Produces the shell snippet that runs every code generator that is part of a
|
|
# module's build — `logos-cpp-generator --general-only` (+ dependency/interface
|
|
# wrappers) and the module-builder-level `${preConfigure}` (LIDL, Qt glue,
|
|
# C-ABI dispatch, UI plugin glue) — leaving a fully-populated `generated_code/`
|
|
# in the (source) working directory. `build` runs it from `preConfigure` and
|
|
# then compiles; `generate` runs the very same snippet and snapshots the tree
|
|
# instead of compiling, so the emitted source is guaranteed identical to what a
|
|
# real build generates.
|
|
mkGeneration = {
|
|
pkgs,
|
|
src,
|
|
config,
|
|
moduleDeps ? {},
|
|
interfaceDeps ? [],
|
|
staticDeps ? [],
|
|
externalLibs ? {},
|
|
preConfigure ? "",
|
|
}:
|
|
let
|
|
pluginFilename = common.getPluginFilename pkgs config.name;
|
|
libExt = common.getLibExtension pkgs;
|
|
staticDepNames = builtins.map (e: e.name) staticDeps;
|
|
|
|
# Pick the API style up-front from this module's `interface`. Each
|
|
# dep already ships pre-built header variants (`.headers-qt` and
|
|
# `.headers-lp` — see mkLogosModule.nix's `buildHeaders` calls),
|
|
# so we just copy from the right one. No codegen at consume time.
|
|
# cdylib AND core universal modules get the Qt-free outbound surface: the
|
|
# generated LogosModules umbrella + dep wrappers call the logos-protocol C
|
|
# ABI (lp_*) directly, so the module's own TUs never include Qt (universal
|
|
# is now a header-first cdylib — see modulePreConfigure.universalCodegen).
|
|
# UI universal backends (type: ui_qml) are NOT modules — they derive a Qt
|
|
# SimpleSource whose .rep slots are Qt-typed, so they get the Qt surface
|
|
# too: their LogosUiPluginContext.modules() dep wrappers come out Qt-typed,
|
|
# matching the view, with no std<->Qt conversions at the boundary.
|
|
# Everything else (legacy / handcrafted Qt) is Qt-typed as well.
|
|
apiStyle = if config.interface == "cdylib" then "lp"
|
|
else if config.interface == "universal" && (config.type or "core") != "ui_qml" then "lp"
|
|
else "qt";
|
|
|
|
# TRANSITIONAL: header-copy fallback for dependencies that don't publish a
|
|
# LIDL contract yet (mkLogosModule only puts such deps in `moduleDeps`).
|
|
# Copying a dep's prebuilt headers forces that dep's plugin to be built.
|
|
# Deps that DO publish LIDL come via `staticDeps`/`--dep` instead and are
|
|
# skipped here. Remove this block once every module exposes a `lidl` output.
|
|
moduleDepIncludes = lib.concatMapStringsSep "\n" (name:
|
|
let
|
|
dep = moduleDeps.${name} or null;
|
|
depHeaders =
|
|
if builtins.elem name staticDepNames then null # generated from LIDL
|
|
else if dep == null then null
|
|
else if dep ? "headers-${apiStyle}" then dep."headers-${apiStyle}"
|
|
else dep;
|
|
in if depHeaders != null then ''
|
|
if [ -d "${depHeaders}/include" ]; then
|
|
echo "Copying ${apiStyle}-typed include files from ${name} (legacy header-copy)..."
|
|
cp -r "${depHeaders}/include"/* ./generated_code/ 2>/dev/null || true
|
|
fi
|
|
'' else ""
|
|
) config.dependencies;
|
|
|
|
# --interface flags for logos-cpp-generator, one per interface
|
|
# dependency. Paths were resolved by mkLogosModule (local files from
|
|
# `src`, remote files from a flake input), so the generator never has to
|
|
# know about flake inputs — it just parses each definition file and emits
|
|
# a runtime-bound wrapper. `impl_class` is appended only for .h files.
|
|
# Each composed spec is passed through escapeShellArg so a name/path/class
|
|
# containing spaces or shell metacharacters can't break arg parsing.
|
|
interfaceArgs = lib.concatMapStringsSep " " (e:
|
|
lib.escapeShellArg ("--interface=" + e.name + "=" + e.path
|
|
+ (lib.optionalString (e.impl_class != null) ("=" + e.impl_class)))
|
|
) interfaceDeps;
|
|
|
|
# --dep flags: concrete dependencies generated from their published LIDL
|
|
# (BindMode::Static, name-baked `modules().<dep>`). No dep plugin is built;
|
|
# the path is the dep's `lidl` output (or an override). Same escaping as
|
|
# interfaceArgs.
|
|
depArgs = lib.concatMapStringsSep " " (e:
|
|
lib.escapeShellArg ("--dep=" + e.name + "=" + e.path
|
|
+ (lib.optionalString ((e.impl_class or null) != null) ("=" + e.impl_class)))
|
|
) staticDeps;
|
|
|
|
# Copy external libraries to lib/
|
|
externalLibCopies = lib.concatMapStringsSep "\n" (extLib:
|
|
let
|
|
libInfo = externalLibs.${extLib.name} or null;
|
|
in if libInfo != null then ''
|
|
echo "Copying flake-input library ${extLib.name}..."
|
|
mkdir -p lib
|
|
if [ -d "${libInfo}/lib" ]; then
|
|
cp -r "${libInfo}/lib"/* lib/ 2>/dev/null || true
|
|
fi
|
|
if [ -f "${libInfo}" ]; then
|
|
cp "${libInfo}" lib/ 2>/dev/null || true
|
|
fi
|
|
if [ -d "${libInfo}/include" ]; then
|
|
echo "Copying headers from ${extLib.name}..."
|
|
cp -r "${libInfo}/include"/* lib/ 2>/dev/null || true
|
|
fi
|
|
'' else if extLib ? vendor_path then ''
|
|
echo "Staging vendor library ${extLib.name} from ${extLib.vendor_path}..."
|
|
mkdir -p lib
|
|
for f in "${src}/${extLib.vendor_path}"/lib*; do
|
|
[ -f "$f" ] && cp "$f" lib/ || true
|
|
done
|
|
'' else ""
|
|
) config.external_libraries;
|
|
|
|
# The full generation snippet. Runs in the (source) working directory and
|
|
# writes generated_code/ + stages external libs into lib/. Shared verbatim
|
|
# by `build` (compiles afterwards) and `generate` (snapshots afterwards).
|
|
generationScript = ''
|
|
# Remember source dir — cmake's out-of-tree build will cd into build/
|
|
export LOGOS_MODULE_SOURCE_DIR="$(pwd)"
|
|
|
|
# Create generated_code directory for generated files
|
|
mkdir -p ./generated_code
|
|
|
|
# Copy include files from module dependencies
|
|
${moduleDepIncludes}
|
|
|
|
# Copy external libraries
|
|
${externalLibCopies}
|
|
|
|
# Run logos-cpp-generator with metadata.json and --general-only.
|
|
# `--api-style` picks the type surface of the generated <Module>
|
|
# client wrappers + the umbrella LogosModules struct: std for
|
|
# universal modules (pure-C++ impl + Qt glue, no Qt at the call
|
|
# site), qt for legacy / handcrafted Qt modules (the historical
|
|
# default — backward-compatible with every existing consumer).
|
|
echo "Running logos-cpp-generator (api-style=${apiStyle})..."
|
|
${lib.optionalString (interfaceDeps != [])
|
|
("echo " + lib.escapeShellArg ("Binding interfaces: "
|
|
+ lib.concatMapStringsSep ", " (e: e.name) interfaceDeps))}
|
|
${lib.optionalString (staticDeps != [])
|
|
("echo " + lib.escapeShellArg ("Generating deps from LIDL: "
|
|
+ lib.concatMapStringsSep ", " (e: e.name) staticDeps))}
|
|
logos-cpp-generator --metadata metadata.json --general-only \
|
|
--api-style ${apiStyle} \
|
|
--output-dir ./generated_code ${interfaceArgs} ${depArgs}
|
|
|
|
# Check what was generated by logos-cpp-generator
|
|
echo "Checking generated files in generated_code:"
|
|
ls -la ./generated_code/ 2>/dev/null || echo "No generated files"
|
|
|
|
# Create include directory and organize generated files
|
|
if [ -f "./generated_code/core_manager_api.h" ] || [ -f "./generated_code/logos_sdk.h" ]; then
|
|
echo "Creating include directory and moving generated files..."
|
|
mkdir -p ./generated_code/include
|
|
# Move generated header files to include directory
|
|
for file in ./generated_code/*.h; do
|
|
if [ -f "$file" ]; then
|
|
mv "$file" ./generated_code/include/
|
|
fi
|
|
done
|
|
# Also copy generated .cpp files to include directory
|
|
for file in ./generated_code/*.cpp; do
|
|
if [ -f "$file" ]; then
|
|
cp "$file" ./generated_code/include/
|
|
fi
|
|
done
|
|
echo "Generated include directory:"
|
|
ls -la ./generated_code/include/ 2>/dev/null || echo "No include files"
|
|
fi
|
|
|
|
# Run any custom preConfigure hook
|
|
${preConfigure}
|
|
'';
|
|
in {
|
|
inherit generationScript libExt pluginFilename apiStyle;
|
|
};
|
|
|
|
in {
|
|
build = {
|
|
pkgs,
|
|
src,
|
|
config,
|
|
commonArgs,
|
|
logosSdk,
|
|
moduleDeps ? {},
|
|
interfaceDeps ? [],
|
|
staticDeps ? [],
|
|
externalLibs ? {},
|
|
preConfigure ? "",
|
|
postInstall ? "",
|
|
}:
|
|
let
|
|
gen = mkGeneration {
|
|
inherit pkgs src config moduleDeps interfaceDeps staticDeps externalLibs preConfigure;
|
|
};
|
|
libExt = gen.libExt;
|
|
|
|
in pkgs.stdenv.mkDerivation (commonArgs // {
|
|
# Required wherever the Qt wrapper hooks are absent (Windows -- see
|
|
# common.nix): qtbase's setup hook hard-errors in qtPreHook with
|
|
# "depends on qtbase, but no wrapping behavior was specified" unless one of
|
|
# the two is present. Harmless elsewhere: these are plugins, not
|
|
# applications, so there is nothing to wrap.
|
|
dontWrapQtApps = true;
|
|
|
|
pname = "${commonArgs.pname}-lib";
|
|
|
|
inherit src;
|
|
|
|
# Qt embeds plugin metadata in a special section (.note.qt.metadata on ELF,
|
|
# __TEXT,__qt_pluginmeta on Mach-O). Stripping can remove it on macOS.
|
|
dontStrip = true;
|
|
|
|
preConfigure = ''
|
|
runHook prePreConfigure
|
|
|
|
${gen.generationScript}
|
|
|
|
runHook postPreConfigure
|
|
'';
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
|
|
mkdir -p $out/lib
|
|
|
|
# Find and copy the built plugin library
|
|
if [ -f modules/${config.name}_plugin.dylib ]; then
|
|
cp modules/${config.name}_plugin.dylib $out/lib/
|
|
elif [ -f modules/${config.name}_plugin.so ]; then
|
|
cp modules/${config.name}_plugin.so $out/lib/
|
|
elif [ -f ${config.name}_plugin.dylib ]; then
|
|
cp ${config.name}_plugin.dylib $out/lib/
|
|
elif [ -f ${config.name}_plugin.so ]; then
|
|
cp ${config.name}_plugin.so $out/lib/
|
|
else
|
|
echo "Error: No plugin library file found"
|
|
echo "Searching for any plugin files..."
|
|
find . -name "*_plugin.*" -type f 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
# Optional: typed replica factory plugin (generated by logos_module REP_FILE)
|
|
for _rf in \
|
|
modules/${config.name}_replica_factory.dylib \
|
|
modules/${config.name}_replica_factory.so \
|
|
${config.name}_replica_factory.dylib \
|
|
${config.name}_replica_factory.so; do
|
|
if [ -f "$_rf" ]; then
|
|
echo "Copying replica factory plugin: $(basename $_rf)"
|
|
cp "$_rf" $out/lib/
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Copy external libraries staged by externalLibCopies during preConfigure.
|
|
# CMake's out-of-tree build (cd build/) means CWD != source dir, so use
|
|
# the path saved earlier.
|
|
_ext_lib_dir="''${LOGOS_MODULE_SOURCE_DIR:-$(pwd)}/lib"
|
|
if [ -d "$_ext_lib_dir" ]; then
|
|
echo "Checking $_ext_lib_dir for external libraries..."
|
|
for libfile in "$_ext_lib_dir"/*; do
|
|
if [ -f "$libfile" ] && [[ "$libfile" == *.${libExt} ]]; then
|
|
echo "Copying external library: $(basename $libfile)"
|
|
cp "$libfile" $out/lib/
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Copy external libraries from source lib/ directory (platform-specific only)
|
|
if [ -d "${src}/lib" ]; then
|
|
echo "Checking source lib/ directory..."
|
|
for libfile in "${src}"/lib/*; do
|
|
if [ -f "$libfile" ] && [[ "$libfile" == *.${libExt} ]]; then
|
|
basename_file=$(basename "$libfile")
|
|
echo "Copying source library: $basename_file"
|
|
cp "$libfile" $out/lib/
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Fix library paths on macOS
|
|
${lib.optionalString pkgs.stdenv.hostPlatform.isDarwin ''
|
|
# Fix install name for the plugin (must succeed — a broken id causes runtime load failures)
|
|
if [ -f "$out/lib/${config.name}_plugin.dylib" ]; then
|
|
${pkgs.darwin.cctools}/bin/install_name_tool -id "@rpath/${config.name}_plugin.dylib" "$out/lib/${config.name}_plugin.dylib"
|
|
fi
|
|
if [ -f "$out/lib/${config.name}_replica_factory.dylib" ]; then
|
|
${pkgs.darwin.cctools}/bin/install_name_tool -id "@rpath/${config.name}_replica_factory.dylib" "$out/lib/${config.name}_replica_factory.dylib"
|
|
fi
|
|
|
|
# Fix install_name for all external libraries
|
|
for dylib in $out/lib/*.dylib; do
|
|
if [ -f "$dylib" ]; then
|
|
libname=$(basename "$dylib")
|
|
${pkgs.darwin.cctools}/bin/install_name_tool -id "@rpath/$libname" "$dylib" 2>/dev/null || true
|
|
fi
|
|
done
|
|
|
|
# Fix references to external libraries in the plugin.
|
|
# Libraries may be referenced by bare name (e.g. "libcalc.dylib") or
|
|
# by absolute build/nix-store paths. Rewrite any reference whose
|
|
# basename matches a library shipped in $out/lib/ to @rpath/<name>.
|
|
for plugin in $out/lib/*_plugin.dylib; do
|
|
if [ -f "$plugin" ]; then
|
|
PLUGIN_NAME=$(basename "$plugin")
|
|
for libfile in $out/lib/*.dylib; do
|
|
LIBNAME=$(basename "$libfile")
|
|
[ "$LIBNAME" = "$PLUGIN_NAME" ] && continue
|
|
# Fix bare-name reference (e.g. "libcalc.dylib" -> "@rpath/libcalc.dylib")
|
|
${pkgs.darwin.cctools}/bin/install_name_tool -change "$LIBNAME" "@rpath/$LIBNAME" "$plugin" 2>/dev/null || true
|
|
# Fix any absolute-path reference ending with this library name
|
|
${pkgs.darwin.cctools}/bin/otool -L "$plugin" | awk "{print \$1}" | { grep "/$LIBNAME" || true; } | while read OLD_REF; do
|
|
echo "Fixing reference: $OLD_REF -> @rpath/$LIBNAME"
|
|
${pkgs.darwin.cctools}/bin/install_name_tool -change "$OLD_REF" "@rpath/$LIBNAME" "$plugin" 2>/dev/null || true
|
|
done
|
|
done
|
|
fi
|
|
done
|
|
''}
|
|
|
|
# Install generated include files
|
|
if [ -d "./generated_code/include" ]; then
|
|
mkdir -p $out/include
|
|
cp -r ./generated_code/include/* $out/include/
|
|
echo "Installed generated include files:"
|
|
ls -la $out/include/ 2>/dev/null || echo "No files"
|
|
fi
|
|
|
|
# Ship the LIDL events sidecar (emitted by `--from-header` codegen
|
|
# for universal modules that declare any `logos_events:` block).
|
|
# The sidecar is read by buildHeaders.nix to generate typed
|
|
# `on<EventName>(callback)` accessors on the consumer wrapper.
|
|
_LIDL_SIDECAR="$LOGOS_MODULE_SOURCE_DIR/generated_code/${config.name}.lidl"
|
|
if [ -f "$_LIDL_SIDECAR" ]; then
|
|
mkdir -p $out/share/logos
|
|
cp "$_LIDL_SIDECAR" "$out/share/logos/${config.name}.lidl"
|
|
echo "Installed LIDL events sidecar: $out/share/logos/${config.name}.lidl"
|
|
fi
|
|
|
|
# Run any custom postInstall hook
|
|
${postInstall}
|
|
|
|
runHook postInstall
|
|
'';
|
|
});
|
|
|
|
# Generate-only build: run the exact same code generators as `build` (via the
|
|
# shared generationScript) but snapshot the resulting source tree instead of
|
|
# compiling it. The output ($out) is a ready-to-build codebase — the module
|
|
# source plus a fully-populated generated_code/ (and any staged lib/) — which
|
|
# cmake compiles without re-running any generator (installed-layout path in
|
|
# LogosModule.cmake). Build it from the module's `nix develop` shell, which
|
|
# exports LOGOS_*_ROOT.
|
|
generate = {
|
|
pkgs,
|
|
src,
|
|
config,
|
|
commonArgs,
|
|
logosSdk,
|
|
moduleDeps ? {},
|
|
interfaceDeps ? [],
|
|
staticDeps ? [],
|
|
externalLibs ? {},
|
|
preConfigure ? "",
|
|
postInstall ? "",
|
|
}:
|
|
let
|
|
gen = mkGeneration {
|
|
inherit pkgs src config moduleDeps interfaceDeps staticDeps externalLibs preConfigure;
|
|
};
|
|
|
|
in pkgs.stdenv.mkDerivation (commonArgs // {
|
|
# Required wherever the Qt wrapper hooks are absent (Windows -- see
|
|
# common.nix): qtbase's setup hook hard-errors in qtPreHook with
|
|
# "depends on qtbase, but no wrapping behavior was specified" unless one of
|
|
# the two is present. Harmless elsewhere: these are plugins, not
|
|
# applications, so there is nothing to wrap.
|
|
dontWrapQtApps = true;
|
|
|
|
pname = "${commonArgs.pname}-generated";
|
|
|
|
inherit src;
|
|
|
|
# No cmake configure, no compile, no ELF/dylib fixup — we only run the
|
|
# generators and snapshot the tree.
|
|
dontConfigure = true;
|
|
dontFixup = true;
|
|
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
|
|
${gen.generationScript}
|
|
|
|
runHook postBuild
|
|
'';
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
|
|
# Snapshot the post-generation source tree: module sources + the just
|
|
# generated generated_code/ + any external libs staged into lib/.
|
|
mkdir -p "$out"
|
|
cp -a . "$out/"
|
|
|
|
${postInstall}
|
|
|
|
runHook postInstall
|
|
'';
|
|
});
|
|
}
|