diff --git a/nix/lib/aspects/fx/edges/materialize-unified.nix b/nix/lib/aspects/fx/edges/materialize-unified.nix index b196c1ec3..68153615e 100644 --- a/nix/lib/aspects/fx/edges/materialize-unified.nix +++ b/nix/lib/aspects/fx/edges/materialize-unified.nix @@ -110,6 +110,7 @@ let scopeContexts scopeParent scopeIsolated + scopeEntityKind ; # Stable scope name for the trace-edge construction (ordering only). name = den.lib.aspects.fx.edges.edge.scopeName { @@ -176,6 +177,7 @@ let rootScopeId scopeContexts scopeParent + scopeEntityKind spawnNode buildForwardAspect ; diff --git a/nix/lib/aspects/fx/edges/route.nix b/nix/lib/aspects/fx/edges/route.nix index 3abced184..1dedeb1e8 100644 --- a/nix/lib/aspects/fx/edges/route.nix +++ b/nix/lib/aspects/fx/edges/route.nix @@ -31,6 +31,7 @@ let synthesize rootTarget ; + inherit (import ./spawn-context.nix { inherit lib; }) mkSourceScopeBindings; inherit (import ../scope-walk.nix { inherit lib; }) subtreeScopes; # ===== materialization mechanics (ported from route/wrap.nix) ========== @@ -579,7 +580,7 @@ let # edges here would double-count the same delivery. Only the drain-fold spawn # (resolve.nix mkDrained) surfaces edges into unifiedEdges. resolveSourceFallback = - spec: spawnNode: scopeParent: + spec: spawnNode: scopeParent: scopeContexts: scopeEntityKind: if !(spec ? sourceAspect) || spawnNode == null || !(spec ? sourceScopeId) then [ ] else @@ -587,7 +588,10 @@ let from = scopeParent.${spec.sourceScopeId} or spec.sourceScopeId; class = spec.fromClass; aspect = den.lib.aspects.normalizeRoot spec.sourceAspect; - bindings = { }; + bindings = mkSourceScopeBindings { + inherit scopeContexts scopeEntityKind; + sourceScopeId = spec.sourceScopeId; + }; }).imports; # Append synthesized modules to a class bucket at a scope (flat + perScope). @@ -615,6 +619,7 @@ let rootScopeId, scopeContexts, scopeParent, + scopeEntityKind, spawnNode, buildForwardAspect, }: @@ -625,7 +630,7 @@ let if collectedSource != [ ] then collectedSource else - resolveSourceFallback spec spawnNode scopeParent; + resolveSourceFallback spec spawnNode scopeParent scopeContexts scopeEntityKind; sourceModule = spec.mapModule { imports = sourceModules; }; newMods = collectClassMods spec.intoClass (buildForwardAspect spec sourceModule); in @@ -706,6 +711,7 @@ let scopeParent ? { }, scopeIsolated ? { }, scopeContexts ? { }, + scopeEntityKind, spawnNode ? null, rootScopeId ? null, buildForwardAspect ? null, @@ -723,6 +729,7 @@ let rootScopeId scopeContexts scopeParent + scopeEntityKind spawnNode buildForwardAspect ; diff --git a/nix/lib/aspects/fx/edges/spawn-context.nix b/nix/lib/aspects/fx/edges/spawn-context.nix new file mode 100644 index 000000000..4090afb0e --- /dev/null +++ b/nix/lib/aspects/fx/edges/spawn-context.nix @@ -0,0 +1,17 @@ +{ ... }: +{ + # Fallback rewalk starts from the parent scope for fleet visibility, so restore + # the source scope's own entity binding (for example, `user`) explicitly. + mkSourceScopeBindings = + { + scopeContexts, + scopeEntityKind, + sourceScopeId, + }: + let + sourceCtx = scopeContexts.${sourceScopeId} or { }; + ownKind = scopeEntityKind.${sourceScopeId} or null; + ownRecord = if ownKind == null then null else sourceCtx.${ownKind} or null; + in + if ownRecord == null then { } else { ${ownKind} = ownRecord; }; +} diff --git a/nix/lib/aspects/fx/resolve.nix b/nix/lib/aspects/fx/resolve.nix index 3b9212502..d6e1801d3 100644 --- a/nix/lib/aspects/fx/resolve.nix +++ b/nix/lib/aspects/fx/resolve.nix @@ -74,13 +74,14 @@ let # complex-route forward SOURCE with full fleet visibility (replaces the old # isolated fxResolve fallback). applyRoutes = - spawnNode: ctx: scopeContexts: rootScopeId: scopeParent: scopeIsolated: scopedRoutes: acc: + spawnNode: ctx: scopeContexts: rootScopeId: scopeParent: scopeIsolated: scopeEntityKind: scopedRoutes: acc: routeEdges.applyRoutes { inherit scopedRoutes scopeContexts scopeParent scopeIsolated + scopeEntityKind rootScopeId spawnNode ; @@ -983,6 +984,7 @@ let oraclePhase2 = applyProvidesEdges ctx scopedProvides phase1; oraclePhase3 = applyRoutes spawnNode ctx augmentedScopeContexts result.state.rootScopeId scopeParent scopeIsolated + scopeEntityKind scopedRoutes oraclePhase2; in diff --git a/templates/ci/modules/deadbugs/hm-platform-forward.nix b/templates/ci/modules/deadbugs/hm-platform-forward.nix new file mode 100644 index 000000000..ad8bbea50 --- /dev/null +++ b/templates/ci/modules/deadbugs/hm-platform-forward.nix @@ -0,0 +1,90 @@ +# Regression for discussion #620: +# `docs/src/content/docs/guides/custom-classes.mdx`, +# "Example: Platform specific `hm` classes". +# +# The documented `hmPlatforms` forward failed for a host user with +# "spawnNode spawn root equals its parent scope". +{ denTest, ... }: +{ + flake.tests.deadbugs.hm-platform-forward = { + + # Docs example: forward `hmLinux`, guard out `hmDarwin`. + test-doc-example-forwards-linux-only = denTest ( + { + den, + lib, + tuxHm, + ... + }: + let + hmPlatforms = + { class, aspect-chain }: + den.batteries.forward { + each = [ + "Linux" + "Darwin" + "Aarch64" + "64bit" + ]; + fromClass = platform: "hm${platform}"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + guard = { pkgs, ... }: platform: lib.mkIf pkgs.stdenv."is${platform}"; + adaptArgs = + { config, ... }: + { + osConfig = config; + }; + }; + in + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + den.aspects.tux = { + includes = [ hmPlatforms ]; + + hmLinux.home.sessionVariables.HM_PLATFORM = "linux"; + + hmDarwin.home.sessionVariables.HM_PLATFORM_DARWIN = "darwin"; + }; + + expr = { + linux = tuxHm.home.sessionVariables.HM_PLATFORM or null; + darwin = tuxHm.home.sessionVariables.HM_PLATFORM_DARWIN or null; + }; + expected = { + linux = "linux"; + darwin = null; + }; + } + ); + + # Unknown source class: forwarding an undeclared, unused class into an + # evaluated class should be inert, not crash during fallback rewalk. + test-unknown-source-class-to-home-manager-is-inert = denTest ( + { + den, + lib, + igloo, + ... + }: + let + forwardUnknownSource = + { aspect-chain, ... }: + den.batteries.forward { + each = [ true ]; + fromClass = _: "unusedSource"; + intoClass = _: "homeManager"; + fromAspect = _: lib.head aspect-chain; + }; + in + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + den.aspects.tux.includes = [ forwardUnknownSource ]; + + expr = igloo.home-manager.users.tux.home.stateVersion; + expected = "25.11"; + } + ); + }; +}