Files
logos-plugin-qt/lib/buildPlugin.nix
T
Dario LipicarandClaude Opus 4.8 3aefbc55b5 feat(universal): route core universal modules through the cdylib mechanism (#12)
apiStyle for `interface: "universal"` core modules becomes "lp" (was "std"):
they now build as header-first cdylibs — Qt-free impl + the common
module-impl C ABI export seam + uniform Qt glue, with outbound
modules().<dep> calls hitting logos-protocol's lp_* C ABI directly. An
unchanged universal module recompiles Qt-free in its impl/wrapper TUs and
still loads via the same Qt plugin (load + call ABI preserved).

UI universal backends (type: ui_qml) derive a Qt SimpleSource and cannot be
Qt-free, so they stay on the std surface. Everything else (legacy) stays qt.

Pairs with logos-module-builder modulePreConfigure.universalCodegen, which
now derives the LIDL from the impl header and runs the cdylib backend.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:55:13 -03:00

305 lines
13 KiB
Nix

# Builder for the module library (Qt plugin .so/.dylib)
# This is the Qt-specific implementation of the plugin build step.
{ lib, common }:
{
build = {
pkgs,
src,
config,
commonArgs,
logosSdk,
moduleDeps ? {},
interfaceDeps ? [],
staticDeps ? [],
externalLibs ? {},
preConfigure ? "",
postInstall ? "",
}:
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-std` — 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) inherently derive a Qt SimpleSource,
# so they stay on the std surface (Qt bridged inside the generated .cpp);
# everything else (legacy / handcrafted Qt) stays Qt-typed.
apiStyle = if config.interface == "cdylib" then "lp"
else if config.interface == "universal" && (config.type or "core") != "ui_qml" then "lp"
else if config.interface == "universal" then "std"
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;
in pkgs.stdenv.mkDerivation (commonArgs // {
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
# 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}
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
'';
});
}