mirror of
https://github.com/logos-co/logos-module-builder.git
synced 2026-08-30 19:51:07 +00:00
* fix(cross): fail loudly when a dep publishes nothing for the target system
Two fixes, both found while chasing why chat_ui's Windows build died on
`fatal error: chat_module_api.h: No such file or directory`.
**The silent fallback.** Resolving a transitional header-copy dependency
did `input.packages.${system} or null`, and on null fell back to `input`
itself. For a flake input that is the dependency's SOURCE TREE, so the
plugin build gets a header root with no generated headers in it and fails
far away inside a generated TU — or, worse, succeeds against whatever
stale headers happen to be checked in. chat_module v0.2.2 publishes only
the four native systems, so every x86_64-windows consumer of it resolved
its headers to the chat_module checkout.
A flake that publishes `packages` but nothing for this system is now an
error naming the dep, the system, and what it does publish. The fallback
survives only for a genuinely bare-derivation input (no `packages` attr
at all), which is the pre-refactor shape it exists for.
**One resolver, not two.** That logic was copy-pasted into
mkLogosModule.nix (core modules) and buildCppPlugin.nix (ui_qml view
modules). It now lives once in common.nix and both call it. This is not
tidying: chat_ui is a view module, so a fix applied only to
mkLogosModule.nix left the case that motivated it untouched — which is
exactly what happened on the first attempt here.
Also single-sources the Windows build platform from
logos-nix's `windowsBuildSystems` instead of repeating "x86_64-linux" as
a literal in two places. Pinning it (rather than using the evaluating
system) is deliberate: it keeps packages.x86_64-windows.* one
well-defined derivation whoever evaluates it, so a Darwin and a Linux
checkout agree and share a cache. Widening it is now a change in
logos-nix, not here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cross): fail loudly in collectAllModuleDeps too
Same silent-fallback shape as the header-copy resolver, one level out:
when a dependency published no usable package for the target system,
`collectAllModuleDeps` fell through to `input` -- putting the dep's
SOURCE TREE where an LGX package belongs. mkStandaloneApp then ships a
directory of .cpp files in place of a module, and the failure only shows
up at runtime as a module that never loads.
The two autoBundleLgx throws right above it already guard the adjacent
case ("a silent fallback would cause mkStandaloneApp to silently omit
the dependency at runtime"), so this closes the remaining hole in the
same function.
A bare-derivation input (no `packages` attr at all) still takes the
fallback -- that is the shape it exists for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
271 lines
13 KiB
Nix
271 lines
13 KiB
Nix
# Common utilities shared across all builder functions (backend-agnostic)
|
|
# Qt-specific build deps and cmake flags now live in the plugin backend.
|
|
{ lib, nix-bundle-lgx ? null, nixpkgs ? null, logos-nix ? null }:
|
|
|
|
let
|
|
# Recursively collect all module dependencies (direct + transitive) from flake
|
|
# inputs, using each module's exported config.dependencies to walk the tree.
|
|
# Returns a flat attrset: { moduleName = lgxDerivation; ... }
|
|
# Uses the LGX package output (packages.lgx) which bundles the plugin plus
|
|
# any external libraries it depends on. When a dependency lacks packages.lgx,
|
|
# it is automatically bundled into an LGX package using nix-bundle-lgx.
|
|
#
|
|
# system: target system string (e.g. "x86_64-linux")
|
|
# inputs: flake inputs attrset to search for dependency modules
|
|
# depNames: list of dependency name strings to resolve
|
|
collectAllModuleDeps = system: inputs: depNames:
|
|
let
|
|
depInputs = lib.filterAttrs (n: _: builtins.elem n depNames) inputs;
|
|
|
|
# Bundle a derivation into LGX on the fly using nix-bundle-lgx.
|
|
# Fails fast if nix-bundle-lgx is unavailable — a silent fallback would
|
|
# cause mkStandaloneApp to silently omit the dependency at runtime.
|
|
autoBundleLgx = drv:
|
|
if nix-bundle-lgx == null then
|
|
builtins.throw "collectAllModuleDeps: dependency lacks packages.${system}.lgx and nix-bundle-lgx is not available to auto-bundle it. Either add an lgx output to the dependency or ensure nix-bundle-lgx is passed to common.nix."
|
|
else if !(nix-bundle-lgx ? bundlers.${system}.default) then
|
|
builtins.throw "collectAllModuleDeps: nix-bundle-lgx does not provide a bundler for system ${system}."
|
|
else
|
|
nix-bundle-lgx.bundlers.${system}.default drv;
|
|
|
|
direct = lib.mapAttrs (depName: input:
|
|
if input ? packages.${system}.lgx
|
|
then input.packages.${system}.lgx
|
|
else if input ? packages.${system}.lib
|
|
then autoBundleLgx input.packages.${system}.lib
|
|
else if input ? packages.${system}.default
|
|
then autoBundleLgx input.packages.${system}.default
|
|
# A flake that publishes `packages` but nothing usable for THIS system is
|
|
# an error, not a fallback -- the same hazard the autoBundleLgx throws
|
|
# above already guard against, one level out. Falling through to `input`
|
|
# puts the dependency's SOURCE TREE into the app bundle where an LGX
|
|
# package belongs: mkStandaloneApp then ships a directory of .cpp files
|
|
# in place of a module and the failure only shows up at runtime, as a
|
|
# module that never loads.
|
|
#
|
|
# Only a genuinely bare-derivation input (no `packages` attr at all) may
|
|
# take the fallback below.
|
|
else if input ? packages then
|
|
builtins.throw ''
|
|
collectAllModuleDeps: dependency '${depName}' publishes no usable package for ${system}.
|
|
|
|
It exposes systems: ${lib.concatStringsSep ", " (builtins.attrNames input.packages)}
|
|
${lib.optionalString (input.packages ? ${system})
|
|
"and for ${system}: ${lib.concatStringsSep ", " (builtins.attrNames input.packages.${system})} (none of lgx / lib / default)"}
|
|
|
|
Fix: give '${depName}' a ${system} target and re-pin it, or publish an
|
|
`lgx`/`lib`/`default` output for it. For a cross target that is usually
|
|
a one-line change to the systems list its flake folds `packages` over.
|
|
''
|
|
else input
|
|
) depInputs;
|
|
|
|
transitive = builtins.foldl' (acc: name:
|
|
let
|
|
input = depInputs.${name};
|
|
tdeps = (input.config or {}).dependencies or [];
|
|
tinputs = input.inputs or {};
|
|
in
|
|
if tdeps == [] then acc
|
|
else acc // (collectAllModuleDeps system tinputs tdeps)
|
|
) {} (builtins.attrNames depInputs);
|
|
in
|
|
# direct overrides transitive so the closest (most specific) dep wins
|
|
transitive // direct;
|
|
|
|
# Supported target systems. "x86_64-windows" is a PSEUDO-system: a cross
|
|
# derivation's `system` attribute is its BUILD platform, so this evaluates
|
|
# anywhere but only realises on x86_64-linux.
|
|
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]
|
|
++ lib.optional (logos-nix != null) "x86_64-windows";
|
|
|
|
# THE package-set constructor. Every module's pkgs comes from here, which is
|
|
# what lets ~40 modules target Windows without each re-deriving the cross
|
|
# plumbing.
|
|
#
|
|
# x86_64-windows cannot be produced by `import nixpkgs { system = ...; }` --
|
|
# it needs localSystem/crossSystem plus logos-nix's mingw overlays, which is
|
|
# exactly what logos-nix.lib.mkWindowsPkgs wraps.
|
|
mkPkgsWith = extraOverlays: system:
|
|
if system != "x86_64-windows" then
|
|
import nixpkgs { inherit system; overlays = extraOverlays; }
|
|
else if logos-nix == null then
|
|
throw ("logos-module-builder: targeting x86_64-windows requires the "
|
|
+ "logos-nix input to be threaded into the builder lib.")
|
|
else if extraOverlays != [ ] then
|
|
# Rather than silently drop them and hand back a package set that is not
|
|
# what the caller asked for. mkWindowsPkgs owns its overlay list (the
|
|
# mingw cross fixes); teach it to merge before removing this.
|
|
throw ("logos-module-builder: overlays are not yet supported for the "
|
|
+ "x86_64-windows target (requested "
|
|
+ toString (builtins.length extraOverlays) + ").")
|
|
else
|
|
logos-nix.lib.mkWindowsPkgs { buildSystem = windowsBuildSystem; };
|
|
|
|
mkPkgs = mkPkgsWith [ ];
|
|
|
|
# The build platform Windows artifacts are produced FROM.
|
|
#
|
|
# Single-sourced from logos-nix, which owns the decision and the reasoning
|
|
# for it (`windowsBuildSystems`: wine does not exist for aarch64-darwin, and
|
|
# upstream only exercises mingw cross from x86_64-linux). It was previously
|
|
# a bare "x86_64-linux" literal repeated here in two places -- a second copy
|
|
# of someone else's constant, free to drift from it.
|
|
#
|
|
# Deliberately NOT the evaluating system. Pinning it keeps
|
|
# `packages.x86_64-windows.*` one well-defined derivation no matter who
|
|
# evaluates it, so a Darwin and a Linux checkout agree and share a cache; a
|
|
# Darwin dev realises it through a Linux remote builder. Widening this is a
|
|
# change to `windowsBuildSystems` in logos-nix, not an edit here.
|
|
windowsBuildSystem =
|
|
if logos-nix == null then "x86_64-linux"
|
|
else lib.head logos-nix.lib.windowsBuildSystems;
|
|
|
|
# The system a build for `target` actually RUNS on. Host TOOLS -- the code
|
|
# generators, moc, repc -- must come from here, never from the target set:
|
|
# under cross, `packages.x86_64-windows.logos-qt-generator` is a PE that the
|
|
# Linux builder cannot execute ("logos-cpp-generator: command not found").
|
|
# Identity for every native system, so callers need no isWindows test.
|
|
buildSystemFor = target:
|
|
if target == "x86_64-windows" then windowsBuildSystem else target;
|
|
|
|
# Resolve the TRANSITIONAL header-copy dependencies (deps publishing no `lidl`
|
|
# contract) from flake inputs, as a struct exposing the dep's plugin (.lib)
|
|
# plus the header variant matching the consumer's --api-style.
|
|
#
|
|
# ONE implementation on purpose. This logic used to be copy-pasted into
|
|
# mkLogosModule.nix (core modules) and buildCppPlugin.nix (ui_qml view
|
|
# modules), and a fix applied to one silently missed the other -- which is
|
|
# exactly how the missing-system case below went unnoticed for view modules.
|
|
# Remove the whole thing once every module publishes a `lidl` output.
|
|
resolveLegacyHeaderDeps = { system, flakeInputs, depNames }:
|
|
lib.mapAttrs (depName: input:
|
|
let
|
|
# An lp (Qt-free) consumer must NOT silently fall back to a Qt-typed
|
|
# header set. The wrappers would declare QString/QVariantMap while the
|
|
# consumer's own codegen ran with `--api-style lp`, so the build dies
|
|
# deep inside a generated TU with a wall of unrelated-looking Qt type
|
|
# errors. Lazy: only fires if an lp consumer really reads `headers-lp`.
|
|
staleLpDep = reason: throw ''
|
|
logos-module-builder: dependency '${depName}' cannot be consumed by an lp (Qt-free) module.
|
|
|
|
'${depName}' is taking the transitional header-copy path (it publishes
|
|
no `lidl` output), and
|
|
${reason}.
|
|
So the only headers it offers are Qt-typed. Copying those into a
|
|
Qt-free translation unit fails deep inside a generated source file
|
|
with a wall of unrelated-looking Qt type errors, so this build stops
|
|
here instead.
|
|
|
|
Fix: rebuild / re-pin '${depName}' against a current logos-module-builder.
|
|
Any module built by one publishes a `lidl` contract (preferred — it
|
|
skips the header copy entirely) as well as a `headers-lp` output.
|
|
'';
|
|
|
|
# A dep flake that publishes `packages` but nothing for THIS system is an
|
|
# error, not a fallback. Degrading to `input` hands the plugin build the
|
|
# dependency's SOURCE TREE as its header root, and the failure surfaces
|
|
# far away as `fatal error: <dep>_api.h: No such file or directory` in a
|
|
# generated TU -- or, worse, silently succeeds against whatever stale
|
|
# headers happen to be checked in.
|
|
#
|
|
# This is how chat_ui's Windows build failed: chat_module v0.2.2
|
|
# publishes only the four native systems, so an x86_64-windows consumer
|
|
# resolved its headers to the chat_module checkout.
|
|
#
|
|
# Only a genuinely bare-derivation input (no `packages` attr at all) may
|
|
# take the fallback path -- the pre-refactor shape it exists for.
|
|
ps =
|
|
if input ? packages && !(input.packages ? ${system}) then
|
|
throw ''
|
|
logos-module-builder: dependency '${depName}' publishes no packages for ${system}.
|
|
|
|
It exposes: ${lib.concatStringsSep ", " (builtins.attrNames input.packages)}
|
|
|
|
'${depName}' is taking the transitional header-copy path (it
|
|
publishes no `lidl` output), so this build needs its compiled
|
|
headers for ${system} and there are none.
|
|
|
|
Fix: give '${depName}' a ${system} target and re-pin it. For a
|
|
cross target that means adding ${system} to the systems list its
|
|
flake folds `packages` over -- mkLogosModule already understands
|
|
the target, so it is usually a one-line change in that flake.
|
|
''
|
|
else input.packages.${system} or null;
|
|
|
|
# Pre-version of this refactor: input was the raw flake-output derivation
|
|
# (not a packages set). Preserve that path so an external flake-input dep
|
|
# still works.
|
|
fallback = if input ? packages.${system}.default
|
|
then input.packages.${system}.default else input;
|
|
in
|
|
if ps != null then {
|
|
default = ps.default;
|
|
lib = ps.lib or ps.default;
|
|
headers-qt = ps.headers-qt or ps.include or ps.default;
|
|
headers-lp = ps.headers-lp or (staleLpDep "its packages.${system} exposes no `headers-lp`");
|
|
} else {
|
|
default = fallback;
|
|
lib = fallback;
|
|
headers-qt = fallback;
|
|
headers-lp = staleLpDep "the flake input is a bare derivation with no packages.${system} attrset";
|
|
}
|
|
) (lib.filterAttrs (n: _: builtins.elem n depNames) flakeInputs);
|
|
|
|
# Helper to run a function for all systems
|
|
forAllSystems = _nixpkgs: f:
|
|
lib.genAttrs systems (system: f {
|
|
inherit system;
|
|
pkgs = mkPkgs system;
|
|
});
|
|
|
|
in {
|
|
inherit systems mkPkgs mkPkgsWith forAllSystems buildSystemFor resolveLegacyHeaderDeps;
|
|
|
|
inherit collectAllModuleDeps;
|
|
|
|
|
|
|
|
# Determine library extension based on platform
|
|
getLibExtension = pkgs:
|
|
if pkgs.stdenv.hostPlatform.isDarwin then "dylib"
|
|
else if pkgs.stdenv.hostPlatform.isWindows then "dll"
|
|
else "so";
|
|
|
|
# Get the library filename for a module
|
|
getPluginFilename = pkgs: name:
|
|
"${name}_plugin.${if pkgs.stdenv.hostPlatform.isDarwin then "dylib" else "so"}";
|
|
|
|
# Convert module name to various formats
|
|
nameFormats = name: {
|
|
# my_module -> my_module
|
|
snake = name;
|
|
# my_module -> MyModule
|
|
pascal = lib.concatMapStrings (s: lib.toUpper (lib.substring 0 1 s) + lib.substring 1 (-1) s)
|
|
(lib.splitString "_" name);
|
|
# my_module -> myModule
|
|
camel = let
|
|
parts = lib.splitString "_" name;
|
|
first = lib.head parts;
|
|
rest = lib.tail parts;
|
|
in first + lib.concatMapStrings (s: lib.toUpper (lib.substring 0 1 s) + lib.substring 1 (-1) s) rest;
|
|
# my_module -> MY_MODULE
|
|
upper = lib.toUpper (lib.replaceStrings ["-"] ["_"] name);
|
|
};
|
|
|
|
# Merge two attribute sets recursively
|
|
recursiveMerge = attrList:
|
|
let
|
|
f = attrPath:
|
|
lib.zipAttrsWith (n: values:
|
|
if lib.tail values == []
|
|
then lib.head values
|
|
else if lib.all lib.isList values
|
|
then lib.unique (lib.concatLists values)
|
|
else if lib.all lib.isAttrs values
|
|
then f (attrPath ++ [n]) values
|
|
else lib.last values
|
|
);
|
|
in f [] attrList;
|
|
}
|