diff --git a/flake.lock b/flake.lock index 84da8b0..04723de 100644 --- a/flake.lock +++ b/flake.lock @@ -177,11 +177,11 @@ ] }, "locked": { - "lastModified": 1780703083, - "narHash": "sha256-Z0nOivZhmNWprngIAxx1ZwpZPKH3EvZRTPzr3nTrMgs=", + "lastModified": 1780929790, + "narHash": "sha256-lYhDm9E47IUNJwtSFVrdoOkLu/3bnaFzGx3agLPuXlQ=", "owner": "logos-co", "repo": "logos-cpp-sdk", - "rev": "eb71a1aa90e05a98814138779680de2ea60a9ff2", + "rev": "42a8b9ed5c5deb29b984f86d7736c3c25a380655", "type": "github" }, "original": { @@ -3940,11 +3940,11 @@ ] }, "locked": { - "lastModified": 1780703355, - "narHash": "sha256-QwLWoeYn0yMrldG63dWaH2lqyYFn7qgTi5XFlrgCJSc=", + "lastModified": 1780929868, + "narHash": "sha256-yDxv9Cv5VEJDVqHz9qYCU3UXv+kCIjRoD90A7uGYuGU=", "owner": "logos-co", "repo": "logos-plugin-qt", - "rev": "f58fbaa25acbc547ae2671e2cddcc1517d6643f0", + "rev": "68090d0a976224ca1c3c606c486a9ac3dd7a4559", "type": "github" }, "original": { @@ -4019,11 +4019,11 @@ ] }, "locked": { - "lastModified": 1780703355, - "narHash": "sha256-QwLWoeYn0yMrldG63dWaH2lqyYFn7qgTi5XFlrgCJSc=", + "lastModified": 1780929868, + "narHash": "sha256-yDxv9Cv5VEJDVqHz9qYCU3UXv+kCIjRoD90A7uGYuGU=", "owner": "logos-co", "repo": "logos-plugin-qt", - "rev": "f58fbaa25acbc547ae2671e2cddcc1517d6643f0", + "rev": "68090d0a976224ca1c3c606c486a9ac3dd7a4559", "type": "github" }, "original": { diff --git a/lib/buildCppPlugin.nix b/lib/buildCppPlugin.nix index 5dbf7af..50e2331 100644 --- a/lib/buildCppPlugin.nix +++ b/lib/buildCppPlugin.nix @@ -41,12 +41,42 @@ let let pkgs = import nixpkgs { inherit system; }; - # Resolve module dependencies from inputs. Each entry is a struct - # exposing the dep's plugin (.lib) plus both header variants - # (.headers-qt / .headers-std) so the plugin builder can pick - # the one matching its own --api-style. See the matching block - # in mkLogosModule.nix for the full rationale + fallback chain. - moduleInputs = lib.filterAttrs (n: _: builtins.elem n config.dependencies) flakeInputs; + # ── Concrete dependency classification (mirrors mkLogosModule.nix) ────── + # LIDL-based deps → bindings generated from the dep's published `lidl` + # output (no dep plugin build). Deps without a `lidl` output take the + # TRANSITIONAL header-copy fallback below (which builds them). + # Guard every level so a non-flake / raw-derivation dep input returns + # null (→ TRANSITIONAL header-copy fallback) rather than throwing. + depLidlOf = name: + let i = flakeInputs.${name} or null; + in if i != null && i ? packages && i.packages ? ${system} + then (i.packages.${system}.lidl or null) + else null; + depIsLidl = name: (config.dependency_overrides ? ${name}) || (depLidlOf name != null); + staticDeps = map (name: + let ov = config.dependency_overrides.${name} or null; + in if ov != null then { + inherit name; + impl_class = ov.impl_class; + path = if ov.input != null + then (if flakeInputs ? ${ov.input} + then "${flakeInputs.${ov.input}}/${ov.file}" + else throw "dependency_overrides.${name}: flake input '${ov.input}' was not passed to mkLogosQmlModule.") + else "${src}/${ov.file}"; + } else { + inherit name; + impl_class = null; + path = "${depLidlOf name}/${name}.lidl"; + } + ) (lib.filter depIsLidl config.dependencies); + legacyHeaderDepNames = lib.filter (name: !(depIsLidl name)) config.dependencies; + + # Resolve the TRANSITIONAL header-copy deps from inputs. Each entry is a + # struct exposing the dep's plugin (.lib) plus both header variants + # (.headers-qt / .headers-std) so the plugin builder can pick the one + # matching its own --api-style. See the matching block in mkLogosModule.nix + # for the full rationale + fallback chain. Remove once all deps publish LIDL. + moduleInputs = lib.filterAttrs (n: _: builtins.elem n legacyHeaderDepNames) flakeInputs; resolvedModuleDeps = lib.mapAttrs (_: input: let ps = input.packages.${system} or null; @@ -132,7 +162,7 @@ let fixDarwin = false; copyExternals = false; }; - in selectedBackend.buildPlugin { + in selectedBackend.buildPlugin ({ inherit pkgs src config postInstall logosModule; preConfigure = preConfigureStr; moduleDeps = resolvedModuleDeps; @@ -145,7 +175,11 @@ let } // lib.optionalAttrs hasBuilderCmake { LOGOS_MODULE_BUILDER_ROOT = "${src}"; }; - }; + } + # LIDL-based concrete deps → `--dep` flags (no dep plugin build). + // lib.optionalAttrs (staticDeps != []) { + inherit staticDeps; + }); moduleLib = buildVariant "default"; moduleLibPortable = if hasVariants then buildVariant "portable" else null; diff --git a/lib/mkLogosModule.nix b/lib/mkLogosModule.nix index 0739a6a..1778a38 100644 --- a/lib/mkLogosModule.nix +++ b/lib/mkLogosModule.nix @@ -84,13 +84,57 @@ let let pkgs = import nixpkgs { inherit system; }; - # Resolve module dependencies from inputs. Each entry is exposed + # ── Concrete dependency classification ───────────────────────────────── + # A dependency's typed `modules().` wrapper is generated from its + # published LIDL contract (`packages..lidl`) WITHOUT building the + # dep's plugin. Deps that don't expose a `lidl` output yet take the + # TRANSITIONAL header-copy fallback (`legacyHeaderDepNames`), which DOES + # build them — identical to today's behavior. + # Returns the dep's published LIDL output, or null if the input isn't a + # flake exposing packages..lidl (e.g. a raw-derivation dep, or a + # module built by a builder that predates this feature) — those fall + # through to the TRANSITIONAL header-copy path. Guard every level so a + # non-flake input never throws. + depLidlOf = name: + let i = flakeInputs.${name} or null; + in if i != null && i ? packages && i.packages ? ${system} + then (i.packages.${system}.lidl or null) + else null; + depIsLidl = name: (config.dependency_overrides ? ${name}) || (depLidlOf name != null); + + # LIDL-based deps → `--dep =` for the generator. An override + # forces a specific definition (.lidl, or .h + impl_class); otherwise we + # use the dep's published `lidl` output. + staticDeps = map (name: + let ov = config.dependency_overrides.${name} or null; + in if ov != null then { + inherit name; + impl_class = ov.impl_class; + path = if ov.input != null + then (if flakeInputs ? ${ov.input} + then "${flakeInputs.${ov.input}}/${ov.file}" + else throw "dependency_overrides.${name}: flake input '${ov.input}' was not passed to mkLogosModule.") + else "${src}/${ov.file}"; + } else { + inherit name; + impl_class = null; + path = "${depLidlOf name}/${name}.lidl"; + } + ) (lib.filter depIsLidl config.dependencies); + + # TRANSITIONAL: header-copy fallback for deps that predate the `lidl` + # output. These deps ARE built (their headers come from introspecting the + # compiled plugin). Remove this block — and the `moduleDepIncludes` use in + # the plugin backends — once every module exposes packages..lidl. + legacyHeaderDepNames = lib.filter (name: !(depIsLidl name)) config.dependencies; + + # Resolve the fallback deps from inputs. Each entry is exposed # as a struct so the plugin builder can pick BOTH the dep's # plugin .dylib AND the right header variant for its own # --api-style without re-running the codegen at consume time. # Backward-compatible fallbacks let older deps (which only # expose `default`) still work — they get treated as Qt-typed. - moduleInputs = lib.filterAttrs (n: _: builtins.elem n config.dependencies) flakeInputs; + moduleInputs = lib.filterAttrs (n: _: builtins.elem n legacyHeaderDepNames) flakeInputs; resolvedModuleDeps = lib.mapAttrs (_: input: let ps = input.packages.${system} or null; @@ -226,6 +270,12 @@ let # interface-dependencies feature (graceful degradation). // lib.optionalAttrs (config.interface_dependencies != []) { interfaceDeps = resolvedInterfaceDeps; + } + # LIDL-based concrete deps → `--dep` flags (generate from the dep's + # published LIDL, no dep plugin build). Gated so a backend that predates + # this feature still builds (such deps then fall through unresolved). + // lib.optionalAttrs (staticDeps != []) { + inherit staticDeps; }); moduleLib = buildVariant "default"; @@ -248,6 +298,28 @@ let apiStyle = "std"; }; + # Publish this module's interface as LIDL — the language-neutral contract + # a consumer turns into typed `modules().` bindings WITHOUT building + # this module's plugin (source → LIDL → C++). Cheap: runs only the C++ + # frontend (`--header-to-lidl`) over the impl header; no Qt/plugin compile. + # Produced for universal modules; the impl header + class come from the + # same convention `universalCodegen` uses (`codegen.impl_*` or defaults). + lidlImplClass = config.codegen.impl_class or (modulePreConfigure.defaultImplClassFromName config.name); + lidlIhRaw = config.codegen.impl_header or "${config.name}_impl.h"; + lidlImplHeaderRel = if lib.hasInfix "/" lidlIhRaw then lidlIhRaw else "src/${lidlIhRaw}"; + moduleLidl = + if config.interface == "universal" + then pkgs.runCommand "logos-${config.name}-lidl" { + nativeBuildInputs = [ logosSdk ]; + } '' + mkdir -p $out + logos-cpp-generator --header-to-lidl "${src}/${lidlImplHeaderRel}" \ + --impl-class "${lidlImplClass}" \ + --metadata "${configFile}" \ + -o "$out/${config.name}.lidl" + '' + else null; + # Combined package — copies the Qt-typed headers (backward # compat). The `//` merge exposes src + version on the derivation # so downstream bundlers (nix-bundle-lgx) can locate metadata.json. @@ -283,6 +355,12 @@ let } // lib.optionalAttrs (moduleLibPortable != null) { "${config.name}-lib-portable" = moduleLibPortable; lib-portable = moduleLibPortable; + } // lib.optionalAttrs (moduleLidl != null) { + # Published LIDL contract — consumers generate bindings from this without + # building the plugin. Cheap (frontend only). Absent for non-universal + # modules, so consumers fall back to the header-copy path for those. + "${config.name}-lidl" = moduleLidl; + lidl = moduleLidl; } ); diff --git a/lib/parseMetadata.nix b/lib/parseMetadata.nix index 221b703..f07f5cf 100644 --- a/lib/parseMetadata.nix +++ b/lib/parseMetadata.nix @@ -20,7 +20,14 @@ main = raw.main or null; icon = raw.icon or null; view = raw.view or null; - dependencies = safeList (raw.dependencies or []); + # Concrete module dependencies, as a list of NAME strings. Entries may be + # bare strings (the common form) or objects `{ name, ... }`; either way we + # keep just the name here so every existing consumer of `config.dependencies` + # (the umbrella, collectAllModuleDeps, the header-copy fallback) is unchanged. + dependencies = map (e: + if builtins.isString e then e + else (e.name or (throw "dependencies entry must be a string name or { name, ... }, got: ${builtins.toJSON e}")) + ) (safeList (raw.dependencies or [])); include = safeList (raw.include or []); # Interface dependencies — method/event contracts decoupled from any @@ -57,6 +64,26 @@ } ) (safeList (raw.interface_dependencies or [])); + # Optional per-dependency LIDL-source overrides. Normally a dependency's + # interface LIDL is auto-resolved from its `packages..lidl` flake + # output (no plugin build); an override forces a specific definition — + # e.g. a committed `.lidl`, a header in another input, or pinning to the + # old header-copy path. Keyed by dependency name → { file, input?, impl_class? }: + # file — path to the .lidl/.h. Relative to this repo (no `input`) + # or to the named flake input. + # input — (optional) flake-input attr name hosting the file. + # impl_class — (required for a .h file) the class whose API defines the dep. + dependency_overrides = lib.mapAttrs (name: ov: + let + file = ov.file or (throw "dependency_overrides.${name} must specify 'file'"); + implClass = ov.impl_class or null; + isHeader = lib.hasSuffix ".h" file || lib.hasSuffix ".hpp" file; + in + if isHeader && implClass == null + then throw "dependency_overrides.${name} is a C++ header (${file}) and must specify 'impl_class'" + else { inherit file; input = ov.input or null; impl_class = implClass; } + ) (if builtins.isAttrs (raw.dependency_overrides or {}) then (raw.dependency_overrides or {}) else {}); + # Nix/build-only fields (nested under "nix" in metadata.json) nix_packages = { build = safeList ((nix.packages or {}).build or []); diff --git a/tests/test-parse-metadata.nix b/tests/test-parse-metadata.nix index b375c90..7f93268 100644 --- a/tests/test-parse-metadata.nix +++ b/tests/test-parse-metadata.nix @@ -189,4 +189,48 @@ in [ ]; }); in assertEq "go_static_lib_names picks go_build entries" goNames.go_static_lib_names [ "go1" ]) + + # --- dependencies: object entries normalized to name strings --- + (assertEq "dependency object/string entries normalized to names" + (parse (builtins.toJSON { + name = "x"; + dependencies = [ "a" { name = "b"; } { name = "c"; } ]; + })).dependencies + [ "a" "b" "c" ]) + + # --- dependency_overrides: defaults to empty attrset --- + (assertEq "dependency_overrides defaults to {}" + (parse ''{ "name": "x" }'').dependency_overrides {}) + + # --- dependency_overrides: .lidl entry (no impl_class needed) --- + (assertEq "dependency_overrides .lidl entry parsed" + (parse (builtins.toJSON { + name = "x"; + dependency_overrides = { dep_a = { file = "iface/dep_a.lidl"; }; }; + })).dependency_overrides + { dep_a = { file = "iface/dep_a.lidl"; input = null; impl_class = null; }; }) + + # --- dependency_overrides: .h entry with impl_class + input --- + (assertEq "dependency_overrides .h entry parsed" + (parse (builtins.toJSON { + name = "x"; + dependency_overrides = { + dep_b = { file = "src/dep_b_impl.h"; impl_class = "DepBImpl"; input = "dep_b_src"; }; + }; + })).dependency_overrides + { dep_b = { file = "src/dep_b_impl.h"; impl_class = "DepBImpl"; input = "dep_b_src"; }; }) + + # --- dependency_overrides: .h without impl_class throws --- + (assertThrows "dependency_overrides .h without impl_class throws" + (parse (builtins.toJSON { + name = "x"; + dependency_overrides = { d = { file = "d.h"; }; }; + }))) + + # --- dependency_overrides: entry without file throws --- + (assertThrows "dependency_overrides without file throws" + (parse (builtins.toJSON { + name = "x"; + dependency_overrides = { d = { input = "z"; }; }; + }))) ]