Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 42 additions & 8 deletions lib/buildCppPlugin.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
82 changes: 80 additions & 2 deletions lib/mkLogosModule.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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().<dep>` wrapper is generated from its
# published LIDL contract (`packages.<sys>.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.<system>.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 <name>=<lidl>` 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.<sys>.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;
Expand Down Expand Up @@ -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";
Expand All @@ -248,6 +298,28 @@ let
apiStyle = "std";
};

# Publish this module's interface as LIDL — the language-neutral contract
# a consumer turns into typed `modules().<name>` 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.
Expand Down Expand Up @@ -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;
}
);

Expand Down
29 changes: 28 additions & 1 deletion lib/parseMetadata.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 []));
Comment thread
dlipicar marked this conversation as resolved.
include = safeList (raw.include or []);

# Interface dependencies — method/event contracts decoupled from any
Expand Down Expand Up @@ -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.<sys>.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 []);
Expand Down
44 changes: 44 additions & 0 deletions tests/test-parse-metadata.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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"; }; };
})))
]
Loading