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
65 changes: 62 additions & 3 deletions nix/lib/aspects/fx/handlers/compile.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,88 @@
# Shape router — dispatches to compile-* based on aspect shape.
{
den,
lib,
...
}:
let
inherit (den.lib) fx;
inherit (den.lib.aspects.fx) argClass;
inherit (den.lib.aspects.fx.contentUtil) unwrapContentValuesList;
inherit (den.lib.schemaUtil) schemaEntityKindsSet;
schema = den.schema or { };
classReg = den.classes or { };

# Entity kinds NAMED as function args by a static aspect's class-content
# modules, unioned across every class key. Mirrors emit-classes.nix
# `namedEntityArgs` (bare-fn modules only) but does NOT gate on ctx — the
# router decides fan-out eligibility via the schema DAG (see below).
namedClassEntityKinds =
aspect:
let
classKeys = builtins.filter (k: classReg ? ${k}) (builtins.attrNames aspect);
kindsIn =
module:
if builtins.isFunction module then
builtins.filter (a: schemaEntityKindsSet ? ${a}) (builtins.attrNames (builtins.functionArgs module))
else
[ ];
in
lib.unique (
builtins.concatMap (k: builtins.concatMap kindsIn (unwrapContentValuesList aspect.${k})) classKeys
);
in
{
compileHandler = {
"compile" =
{ param, state }:
let
meta = param.aspect.meta or { };
aspect = param.aspect;
meta = aspect.meta or { };
isStatic =
!(meta ? __forward) && !(meta ? guard) && !(aspect ? __fn) && (aspect.__args or { }) == { };

# A class-content module naming a strict DESCENDANT of the emitting scope's
# kind (host-scope aspect with `nixos = { user, ... }:`) has no such binding
# in scope, so wrapClassModule would drop it silently. Promote the aspect to
# be parametric on those kinds: the bind handler then fans it per descendant
# instance via the SAME machinery the aspect-level `{ user, ... }:` form uses,
# making the two forms equivalent. Descendant classification (not mere
# ctx-absence) keeps scope-kind-self and ancestor args on the static path —
# bind would only satisfy or inert those. `__parametricResolvedArgs` excludes
# kinds already bound by an earlier fan, so the re-resolved body (still naming
# the kind) does not re-promote into a loop.
scopeKind = ((state.scopeEntityKind or (_: { })) null).${state.currentScope or ""} or null;
resolvedArgs = aspect.__parametricResolvedArgs or [ ];
promoteKinds =
if isStatic && scopeKind != null then
builtins.filter (
k: (argClass.isDescendantOf schema scopeKind k) && !(builtins.elem k resolvedArgs)
) (namedClassEntityKinds aspect)
else
[ ];
promoted =
if promoteKinds != [ ] then
aspect
// {
__fn = _: aspect;
__args = lib.genAttrs promoteKinds (_: false);
__functor = self: self.__fn;
}
else
aspect;

effect =
if meta ? __forward then
"compile-forward"
else if meta ? guard then
"compile-conditional"
else if param.aspect ? __fn || (param.aspect.__args or { }) != { } then
else if promoted ? __fn || (promoted.__args or { }) != { } then
"compile-parametric"
else
"compile-static";
in
{
resume = fx.send effect param;
resume = fx.send effect (param // { aspect = promoted; });
inherit state;
};
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Issue #629: requesting `user` inside an aspect's `nixos` class module silently
# drops the module when the aspect is included at HOST scope.
#
# Reporter includes an aspect at host scope whose `nixos` class module names
# `user`. At host scope the emit ctx has `host` but no `user`, so wrapClassModule
# marks it `unsatisfied` and wrap-classes drops it (silently — the lib.warn is
# attached to the discarded module and never forced). The aspect-level parametric
# form `{ user, ... }: { nixos = ...; }` works because the bind handler fans the
# aspect over host.users; class-module args never reach that fan-out.
{ denTest, ... }:
{
flake.tests.deadbugs.issue-629-class-module-user-arg = {

# FAILING: class module names `user`, aspect included at host scope.
# Host has one user; the module should fan per-user like the aspect form.
test-class-module-user-arg-at-host-scope = denTest (
{ den, igloo, ... }:
{
den.hosts.x86_64-linux.igloo.users.tux = { };

den.aspects.desktop.cosmic.nixos =
{ user, ... }:
{
environment.etc."cosmic-autologin".text = user.userName;
};

den.aspects.igloo.includes = [ den.aspects.desktop.cosmic ];

expr = igloo.environment.etc."cosmic-autologin".text or "<skipped>";
expected = "tux";
}
);

# CONTROL: aspect-level parametric form of the same thing — already works.
test-aspect-level-parametric-at-host-scope = denTest (
{ den, igloo, ... }:
{
den.hosts.x86_64-linux.igloo.users.tux = { };

den.aspects.desktop.cosmic =
{ user, ... }:
{
nixos.environment.etc."cosmic-autologin".text = user.userName;
};

den.aspects.igloo.includes = [ den.aspects.desktop.cosmic ];

expr = igloo.environment.etc."cosmic-autologin".text or "<skipped>";
expected = "tux";
}
);

};
}
Loading