Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/

New Intent-oriented and Incremental contexts. (#57)

( Closes #56, #55 ) -- (Pending #58)

This PR introduces a fundamental refactoring of the aspect dependency and context management system. The primary goal is to provide a more robust, explicit, and powerful way to handle how different parts of the configuration (hosts, users, home-manager) interact with each other. This leads to clearer and more predictable evaluations, but also introduces significant breaking changes from previous unified context `userToHost` and `hostToUser`.

> [!IMPORTANT]
> The previous context objects `userToHost` and `hostToUser` prove to be harder to use and have been **completely removed**.

Now contexts are incremental and context now are used for **configuration intent**. For example, a NixOS configuration starts with context `{ OS }` but later expands to `{ OS, user, host }` as soon as the OS has information of which users and hosts it is being applied to. See the examples on aspects.nix for how this replaces `userToHost` and `hostToUser`.

> [!IMPORTANT]
> Since contexts are now _incremental_, they are also no longer _exact_ by default.

What this means: In previous releases, a function `{ host }: { }` context had to match exactly. For example if applied to `{ host = ...; foo = ... }` it would not apply since `foo` is unexpected. This was as to avoid `function called with unexpectd "foo"` errors. However now contexts can be incremental.

A function `{ host, ... }: { }` provides anytime a `host` is in context.
A function `{ host, user, ... }: { }` provides when at least both are in context.

For example, you can use `{ user, ... }: ...` to provide configurations based on user data anytime it is in context.

**OS/HM Intent Contexts**

OS and HM configurations now start with a single `{ OS }` and `{ HM }` contexts. They are higher-level contexts used to describe the **Intent** of the configuration being produced. This, in hand with incremental contexts, allows to get rid of `hostToUser` and `userToHost`.

For example a context `{ OS, user, host }` is explicit about it being a configuration depending on host/user data but also that it is **being used** for providing NixOS/Darwin configuration specifically.

`OS`/`HM` are not used in our provided batteries at all. Instead the batteries rely only on `host`/`user`/`home` data being in context, fixing the complexity that was introduced by previous `hostToUser` and `userToHost`.

`OS`/`HM` are useful for opt-in bi-directional dependencies, this is how we avoid duplicating settings now. Since a function `{ user, host, ...}: { }` can be installed in `den.default`, and `den.default` is included for any User and Host. That function would produce duplicate configurations - this is correct since the function is **explicitly set in den.default** which is included twice: once for the user and once for the host. To avoid this, either install the function on a specific `den.aspect.<host/user>.includes` or use a higher **intent context** like `OS`/`HM`. See how our aspects.nix examples were updated for this.


## refactor: Dependency Resolution Engine

The core dependency resolution logic in `modules/aspects/dependencies.nix` has been rewritten to be more explicit and prevent recursion.

- It now uses `take.exactly` with the new `OS` and `HM` contexts to ensure that dependency-gathering functions run only once at the appropriate level, preventing evaluation loops.
- The logic for how users contribute to a host's configuration (`osIncludesFromUsers`) and how a host contributes to a user's home-manager configuration (`hmIncludesFromHost`) is now clearly defined and separated.
- The new engine correctly passes the intent `OS` and `HM` contexts down to the aspects being resolved.

## refactor: `provides` Modules Updated for New Context

All built-in `provides` modules have been updated to use the new intent-based context system.

- `provides/define-user`: Now uses generic data-specific `{ host, user, ... }` and `{ home, ... }` contexts directly, removing the need for the intermediate `userToHostContext`.
- `provides/home-manager`: Now passes the `{ HM }` **intent** context when resolving a user's `homeManager` aspect.
- `provides/primary-user`: Logic simplified to expect `{ user, host, ... }` instead of `userToHost`.
- `provides/user-shell`: The logic has been unified. A single `userShell` function now correctly applies shell configuration to `nixos`, `darwin`, and `homeManager` attributes based on the context it receives (`{ user, ... }` or `{ home, ... }`).

## test: Enhanced `canTake` Tests

The test suite for `function_can_take.nix` has been expanded to include assertions for the new `takes.exactly` functionality, ensuring that it correctly identifies functions that take an exact set of arguments.

## feat: Granular Context Control with `take` and `parametric`

A new set of library functions, `den.lib.take` and a redesigned `den.lib.parametric`, have been introduced to give aspect authors fine-grained control over how and when their functions are applied.

- **`den.lib.take`**: This is a new functor that allows a function to declare precisely what context it expects.
- `take.atLeast`: The function is applied if the context provides *at least* the required arguments. This is the default behavior for most parametric functions.
- `take.exactly`: The function is applied only if the context provides the *exact* set of required arguments, with no extras. This is crucial for preventing infinite recursion in dependency resolution.
- `take.unused`: A utility to signal which arguments from the context are "consumed" by the function, allowing for clearer composition.

- **`den.lib.parametric`**: The `parametric` functor has been completely overhauled to leverage `take`.
- It now offers `atLeast` and `exactly` variants to create aspects that apply their includes based on these context matching rules.
- The syntax `parametric true` is now an alias for `parametric.atLeast`.

This new system replaces the older, less precise `canTake` logic, providing a more declarative and safer way to write parametric aspects.

## BREAKING CHANGE: Intent Context (`OS` and `HM`)

The context passed to parametric aspects is no longer a flat attribute set. It has been restructured into namespaced contexts to clearly separate concerns between operating system configuration and home-manager configuration.

- **`OS` Context**: When evaluating for a host's operating system configuration (NixOS or `nix-darwin`), aspects now receive an `OS` attribute in their context. The original `host` object is available as `OS.host`.
- **`HM` Context**: When evaluating for a user's `home-manager` configuration, aspects receive an `HM` attribute. This context contains both the `user` and the `host` they belong to (`HM.user` and `HM.host`).

The previous context objects `userToHost` and `hostToUser` proven to be harder to use and have been **completely removed**. And now contexts are incremental. For example, a NixOS configuration starts with context `{ OS }` but later expands to `{ OS, user, host }` as soon as the OS has information of which users and hosts it is being applied to. See the examples on aspects.nix for how this replaces `userToHost` and `hostToUser`.

### Migration Guide

All parametric aspects must be updated to expect the new context structure.

**Before:**
```nix
my-aspect = { userToHost, ... }: {
${userToHost.host.class}.services.foo.enable = true;
};

my-other-aspect = { hostToUser, ... }: {
homeManager.programs.bar.enable = true;
};
```

**After:**
```nix
# OS context is needed only if my-aspect is installed at den.default
my-aspect = { OS, user, host, ... }: {
${host.class}.services.foo.enable = true;
};

# HM intent context is needed only if installed into den.default.
my-other-aspect = { HM, user, host, ... }: {
homeManager.programs.bar.enable = true;
};
```

authored by oeiuwq.com and committed by

GitHub 721c34b1 4f5653b0

+264 -186
+30 -13
checkmate/tests/aspect-functor.nix
··· 5 ... 6 }: 7 let 8 - __functor = (inputs.target.lib { inherit lib inputs config; }).parametric true; 9 10 aspect-example = { 11 - inherit __functor; 12 nixos.foo = 99; 13 includes = [ 14 { nixos.static = 100; } ··· 28 } 29 ) 30 ( 31 - { userToHost, ... }: 32 { 33 - nixos.user-to-host = [ 34 - userToHost.host 35 - userToHost.user 36 ]; 37 } 38 ) ··· 44 ) 45 ( 46 { user, ... }@ctx: 47 - if builtins.length (builtins.attrNames ctx) == 1 then 48 { 49 nixos.user-only = user; 50 } ··· 153 }; 154 }; 155 156 - flake.tests."test functor applied with userToHost" = { 157 expr = ( 158 aspect-example { 159 - userToHost = { 160 - user = 2; 161 - host = 1; 162 - }; 163 } 164 ); 165 expected = { 166 includes = [ 167 { 168 - nixos.user-to-host = [ 169 1 170 2 171 ]; 172 } 173 { nixos.any = 10; } 174 ]; 175 };
··· 5 ... 6 }: 7 let 8 + den.lib = inputs.target.lib { inherit lib inputs config; }; 9 + 10 + inherit (den.lib) parametric canTake; 11 12 aspect-example = { 13 + __functor = parametric true; 14 nixos.foo = 99; 15 includes = [ 16 { nixos.static = 100; } ··· 30 } 31 ) 32 ( 33 { 34 + OS, 35 + user, 36 + host, 37 + ... 38 + }: 39 + { 40 + nixos.os-user-host = [ 41 + OS 42 + user 43 + host 44 ]; 45 } 46 ) ··· 52 ) 53 ( 54 { user, ... }@ctx: 55 + if canTake.exactly ctx ({ user }: user) then 56 { 57 nixos.user-only = user; 58 } ··· 161 }; 162 }; 163 164 + flake.tests."test functor applied with host/user/OS" = { 165 expr = ( 166 aspect-example { 167 + OS = 0; 168 + user = 2; 169 + host = 1; 170 } 171 ); 172 expected = { 173 includes = [ 174 + { nixos.host = 1; } 175 { 176 + nixos.host-user = [ 177 1 178 2 179 ]; 180 } 181 + { 182 + nixos.os-user-host = [ 183 + 0 184 + 2 185 + 1 186 + ]; 187 + } 188 + { nixos.user = 2; } 189 + { nixos.user-only = false; } 190 { nixos.any = 10; } 191 ]; 192 };
+15 -1
checkmate/tests/function_can_take.nix
··· 5 ... 6 }: 7 let 8 - takes = (inputs.target.lib { inherit inputs lib config; }).canTake; 9 10 flake.tests."test function with no named arguments can take anything" = { 11 expr = takes { } (x: x);
··· 5 ... 6 }: 7 let 8 + den.lib = inputs.target.lib { inherit inputs lib config; }; 9 + takes = den.lib.canTake; 10 + 11 + flake.tests."test exactly fails" = { 12 + expr = takes.exactly { 13 + a = 1; 14 + b = 2; 15 + } ({ a }: a); 16 + expected = false; 17 + }; 18 + 19 + flake.tests."test exactly succeeds" = { 20 + expr = takes.exactly { a = 1; } ({ a }: a); 21 + expected = true; 22 + }; 23 24 flake.tests."test function with no named arguments can take anything" = { 25 expr = takes { } (x: x);
+1 -1
modules/aspects/definition.nix
··· 19 ${host.aspect} = { 20 ${host.class} = { }; 21 includes = [ den.default ]; 22 - __functor = den.lib.parametric { inherit host; }; 23 }; 24 }; 25
··· 19 ${host.aspect} = { 20 ${host.class} = { }; 21 includes = [ den.default ]; 22 + __functor = den.lib.parametric { OS = { inherit host; }; }; 23 }; 24 }; 25
+56 -47
modules/aspects/dependencies.nix
··· 1 { 2 den, 3 ... 4 }: 5 let 6 - inherit (den.lib) owned statics; 7 8 dependencies = [ 9 - # owned attributes: <aspect>.<class> 10 - ({ home, ... }: owned home.class den.aspects.${home.aspect}) 11 - ({ host, ... }: owned host.class den.aspects.${host.aspect}) 12 - ({ user, ... }: owned user.class den.aspects.${user.aspect}) 13 14 - # defaults: owned from den.default.<class> 15 - ({ home, ... }: owned home.class den.default) 16 - ({ host, ... }: owned host.class den.default) 17 - ({ user, ... }: owned user.class den.default) 18 19 - # static (non-parametric) from <aspect>.includes 20 - ({ home, ... }: statics den.aspects.${home.aspect}) 21 - ({ host, ... }: statics den.aspects.${host.aspect}) 22 - ({ user, ... }: statics den.aspects.${user.aspect}) 23 24 - # user-to-host context 25 - ({ userToHost, ... }: owned userToHost.host.class den.aspects.${userToHost.user.aspect}) 26 - # host-to-user context 27 - ({ hostToUser, ... }: owned hostToUser.user.class den.aspects.${hostToUser.host.aspect}) 28 - 29 - # { host } => [ { userToHost } ] 30 - (hostIncludesFromUsers) 31 - 32 - # { user, host } => { hostToUser } 33 - (userIncludesFromHost) 34 - ]; 35 - 36 - hostIncludesFromUsers = 37 - { host, ... }: 38 { 39 - includes = 40 - let 41 - users = builtins.attrValues host.users; 42 - context = user: { 43 - userToHost = { 44 - inherit user host; 45 - }; 46 - }; 47 - contrib = user: den.aspects.${user.aspect} (context user); 48 - in 49 - map contrib users; 50 }; 51 52 - userIncludesFromHost = 53 - { user, host }: 54 { 55 - includes = den.aspects.${host.aspect}.includes; 56 - __functor = den.lib.parametric { 57 - hostToUser = { 58 - inherit host user; 59 - }; 60 - }; 61 }; 62 - 63 in 64 { 65 den.default.includes = dependencies;
··· 1 { 2 den, 3 + lib, 4 ... 5 }: 6 let 7 + inherit (den.lib) 8 + owned 9 + statics 10 + parametric 11 + take 12 + ; 13 14 dependencies = [ 15 + ({ home, ... }: baseDeps home) 16 + ({ host, ... }: baseDeps host) 17 + ({ user, ... }: baseDeps user) 18 + (os osIncludesFromUsers) 19 + (hm hmIncludesFromHost) 20 + ]; 21 22 + # deadnix: skip # exact { OS } to avoid recursion 23 + os = fn: { OS, ... }@ctx: take.exactly ctx fn; 24 + # deadnix: skip # exact { HM } to avoid recursion 25 + hm = fn: { HM, ... }@ctx: take.exactly ctx fn; 26 27 + baseDeps = 28 + from: 29 + let 30 + exists = from ? aspect && builtins.hasAttr from.aspect den.aspects; 31 + aspect = den.aspects.${from.aspect}; 32 + in 33 + { 34 + includes = lib.optionals exists [ 35 + (statics den.default) 36 + (statics aspect) 37 + (owned den.default) 38 + (owned aspect) 39 + ]; 40 + }; 41 42 + osIncludesFromUsers = 43 + { OS }: 44 + let 45 + inherit (OS) host; 46 + users = builtins.attrValues host.users; 47 + userDeps = user: parametric { inherit OS user host; } den.aspects.${user.aspect}; 48 + userContribs.includes = map userDeps users; 49 + hostDeps = user: parametric { inherit user host; } den.aspects.${host.aspect}; 50 + hostContribs.includes = map hostDeps users; 51 + in 52 { 53 + includes = [ 54 + hostContribs 55 + userContribs 56 + ]; 57 }; 58 59 + hmIncludesFromHost = 60 + { HM }: 61 + let 62 + inherit (HM) user host; 63 + userDeps = parametric { inherit HM user host; } den.aspects.${user.aspect}; 64 + hostDeps = parametric { inherit HM user host; } den.aspects.${host.aspect}; 65 + in 66 { 67 + includes = [ 68 + userDeps 69 + hostDeps 70 + ]; 71 }; 72 in 73 { 74 den.default.includes = dependencies;
+3 -12
modules/aspects/provides/define-user.nix
··· 22 host: user: 23 if lib.hasSuffix "darwin" host.system then "/Users/${user.userName}" else "/home/${user.userName}"; 24 25 - userToHostContext = 26 - { userToHost, ... }: 27 - let 28 - inherit (userToHost) host user; 29 - in 30 { 31 nixos.users.users.${user.userName}.isNormalUser = true; 32 darwin.users.users.${user.userName} = { 33 name = user.userName; 34 home = homeDir host user; 35 }; 36 - }; 37 - 38 - userContext = 39 - { host, user }: 40 - { 41 homeManager = { 42 home.username = user.userName; 43 home.homeDirectory = homeDir host user; ··· 45 }; 46 47 hmContext = 48 - { home }: 49 userContext { 50 host.system = home.system; 51 user.userName = home.userName; ··· 55 den.provides.define-user = { 56 inherit description; 57 includes = [ 58 - userToHostContext 59 userContext 60 hmContext 61 ];
··· 22 host: user: 23 if lib.hasSuffix "darwin" host.system then "/Users/${user.userName}" else "/home/${user.userName}"; 24 25 + userContext = 26 + { host, user, ... }: 27 { 28 nixos.users.users.${user.userName}.isNormalUser = true; 29 darwin.users.users.${user.userName} = { 30 name = user.userName; 31 home = homeDir host user; 32 }; 33 homeManager = { 34 home.username = user.userName; 35 home.homeDirectory = homeDir host user; ··· 37 }; 38 39 hmContext = 40 + { home, ... }: 41 userContext { 42 host.system = home.system; 43 user.userName = home.userName; ··· 47 den.provides.define-user = { 48 inherit description; 49 includes = [ 50 userContext 51 hmContext 52 ];
+23 -17
modules/aspects/provides/home-manager.nix
··· 4 den, 5 ... 6 }: 7 - { 8 - den.provides.home-manager = { 9 - description = '' 10 - integrates home-manager into nixos/darwin OS classes. 11 12 - usage: 13 14 - for using home-manager in just a particular host: 15 16 - den.aspects.my-laptop.includes = [ den._.home-manager ]; 17 18 - for enabling home-manager by default on all hosts: 19 20 - den.default.includes = [ den._.home-manager ]; 21 22 - Does nothing for hosts that have no users with `homeManager` class. 23 - Expects `inputs.home-manager` to exist. If `<host>.hm-module` exists 24 - it is the home-manager.{nixos/darwin}Modules.home-manager. 25 26 - For each user resolves den.aspects.''${user.aspect} and imports its homeManager class module. 27 - ''; 28 29 __functor = 30 _: 31 { host, ... }: 32 { class, aspect-chain }: 33 let 34 - hmUsers = builtins.filter (u: u.class == "homeManager") (lib.attrValues host.users); 35 36 hmUserModule = 37 user: 38 let 39 - aspect = den.aspects.${user.aspect} { inherit user host; }; 40 ctx = { 41 inherit aspect-chain; 42 - class = user.class; 43 }; 44 in 45 aspect.resolve ctx;
··· 4 den, 5 ... 6 }: 7 + let 8 + description = '' 9 + integrates home-manager into nixos/darwin OS classes. 10 11 + usage: 12 13 + for using home-manager in just a particular host: 14 15 + den.aspects.my-laptop.includes = [ den._.home-manager ]; 16 17 + for enabling home-manager by default on all hosts: 18 19 + den.default.includes = [ den._.home-manager ]; 20 21 + Does nothing for hosts that have no users with `homeManager` class. 22 + Expects `inputs.home-manager` to exist. If `<host>.hm-module` exists 23 + it is the home-manager.{nixos/darwin}Modules.home-manager. 24 25 + For each user resolves den.aspects.''${user.aspect} and imports its homeManager class module. 26 + ''; 27 + in 28 + { 29 + den.provides.home-manager = { 30 + inherit description; 31 32 __functor = 33 _: 34 { host, ... }: 35 { class, aspect-chain }: 36 let 37 + hmClass = "homeManager"; 38 + hmUsers = builtins.filter (u: u.class == hmClass) (lib.attrValues host.users); 39 40 hmUserModule = 41 user: 42 let 43 ctx = { 44 inherit aspect-chain; 45 + class = hmClass; 46 + }; 47 + aspect = den.aspects.${user.aspect} { 48 + HM = { inherit host user; }; 49 }; 50 in 51 aspect.resolve ctx;
+2 -2
modules/aspects/provides/import-tree.nix
··· 50 # load from ./hosts/<host>/_nixos 51 den.default.includes = [ (den._.import-tree._.host ./hosts) ]; 52 53 - # load from ./users/<user>@<host>/{_homeManager, _nixos} 54 den.default.includes = [ (den._.import-tree._.user ./users) ]; 55 56 # load from ./homes/<home>/_homeManager ··· 72 den._.import-tree.provides = { 73 host = root: { host, ... }: den._.import-tree "${toString root}/${host.name}"; 74 home = root: { home, ... }: den._.import-tree "${toString root}/${home.name}"; 75 - user = root: { host, user, ... }: den._.import-tree "${toString root}/${user.name}@${host.name}"; 76 }; 77 }
··· 50 # load from ./hosts/<host>/_nixos 51 den.default.includes = [ (den._.import-tree._.host ./hosts) ]; 52 53 + # load from ./users/<user>/{_homeManager, _nixos} 54 den.default.includes = [ (den._.import-tree._.user ./users) ]; 55 56 # load from ./homes/<home>/_homeManager ··· 72 den._.import-tree.provides = { 73 host = root: { host, ... }: den._.import-tree "${toString root}/${host.name}"; 74 home = root: { home, ... }: den._.import-tree "${toString root}/${home.name}"; 75 + user = root: { user, ... }: den._.import-tree "${toString root}/${user.name}"; 76 }; 77 }
+1 -2
modules/aspects/provides/primary-user.nix
··· 14 ''; 15 16 userToHostContext = 17 - { userToHost, ... }: 18 let 19 - inherit (userToHost) host user; 20 on-wsl.nixos.wsl.defaultUser = user.userName; 21 in 22 {
··· 14 ''; 15 16 userToHostContext = 17 + { user, host, ... }: 18 let 19 on-wsl.nixos.wsl.defaultUser = user.userName; 20 in 21 {
+7 -15
modules/aspects/provides/user-shell.nix
··· 12 ]; 13 ''; 14 15 - userContext = 16 - { shell }: 17 - { host }: 18 - { 19 - homeManager.programs.${shell}.enable = true; 20 - }; 21 - 22 - userToHostContext = 23 - { shell }: 24 - { userToHost, ... }: 25 let 26 - inherit (userToHost) user; 27 nixos = 28 { pkgs, ... }: 29 { ··· 31 users.users.${user.userName}.shell = pkgs.${shell}; 32 }; 33 darwin = nixos; 34 in 35 { 36 - inherit nixos darwin; 37 }; 38 39 in ··· 41 den.provides.user-shell = shell: { 42 inherit description; 43 __functor = den.lib.parametric true; 44 - includes = map (f: f { inherit shell; }) [ 45 - userContext 46 - userToHostContext 47 ]; 48 }; 49 }
··· 12 ]; 13 ''; 14 15 + userShell = 16 + shell: user: 17 let 18 nixos = 19 { pkgs, ... }: 20 { ··· 22 users.users.${user.userName}.shell = pkgs.${shell}; 23 }; 24 darwin = nixos; 25 + homeManager.programs.${shell}.enable = true; 26 in 27 { 28 + inherit nixos darwin homeManager; 29 }; 30 31 in ··· 33 den.provides.user-shell = shell: { 34 inherit description; 35 __functor = den.lib.parametric true; 36 + includes = [ 37 + ({ user, ... }: userShell shell user) 38 + ({ home, ... }: userShell shell home) 39 ]; 40 }; 41 }
+22 -11
nix/fn-can-take.nix
··· 1 - lib: param: f: 2 let 3 - args = lib.mapAttrsToList (name: optional: { inherit name optional; }) (lib.functionArgs f); 4 - 5 - givenAttrs = (builtins.isAttrs param) && !param ? __functor; 6 - 7 - required = map (x: x.name) (lib.filter (x: !x.optional) args); 8 - provided = lib.attrNames param; 9 - 10 - intersection = lib.intersectLists required provided; 11 - satisfied = lib.length required == lib.length intersection; 12 in 13 - givenAttrs && satisfied
··· 1 + lib: 2 let 3 + check = 4 + params: func: 5 + let 6 + givenArgs = builtins.isAttrs params; 7 + fargs = lib.functionArgs func; 8 + provided = builtins.attrNames params; 9 + args = lib.mapAttrsToList (name: optional: { inherit name optional; }) fargs; 10 + required = map (x: x.name) (lib.filter (x: !x.optional) args); 11 + intersection = lib.intersectLists required provided; 12 + satisfied = givenArgs && lib.length required == lib.length intersection; 13 + noExtras = lib.length required == lib.length provided; 14 + exactly = satisfied && noExtras; 15 + in 16 + { 17 + inherit satisfied exactly; 18 + }; 19 in 20 + { 21 + __functor = self: self.atLeast; 22 + atLeast = params: func: (check params func).satisfied; 23 + exactly = params: func: (check params func).exactly; 24 + }
+64 -40
nix/lib.nix
··· 5 ... 6 }: 7 let 8 isFn = f: (builtins.isFunction f) || (f ? __functor); 9 canTake = import ./fn-can-take.nix lib; 10 - inCtx = ctx: f: isFn f && canTake ctx f; 11 - 12 - isAspect = canTake { 13 - class = ""; 14 - aspect-chain = [ ]; 15 - }; 16 17 # creates an aspect that inherits class from fromAspect. 18 - owned = class: fromAspect: { ${class} = fromAspect.${class} or { }; }; 19 20 # only static includes from an aspect. 21 statics = 22 aspect: 23 - # deadnix: skip 24 - { class, aspect-chain }: 25 - { 26 - includes = 27 - let 28 - include = 29 - f: 30 - if !isFn f then 31 - f 32 - else if isAspect f then 33 - f { inherit class aspect-chain; } 34 - else 35 - { }; 36 - in 37 - map include aspect.includes; 38 }; 39 40 - # "Just Give 'Em One of These" - Moe Szyslak 41 - # a __functor that **only** considers parametric includes 42 - # that **exactly** match the given context. 43 - funk = 44 - aspect: ctx: 45 - let 46 - fns = builtins.filter (inCtx ctx) aspect.includes; 47 - includes = map (f: f ctx) fns; 48 - in 49 - { 50 - inherit includes; 51 - }; 52 53 - parametric = 54 - param: aspect: 55 - if param == true then 56 - funk aspect 57 else 58 - # deadnix: skip 59 - { class, aspect-chain }: funk aspect param; 60 61 aspects = inputs.flake-aspects.lib lib; 62 ··· 71 owned 72 isFn 73 canTake 74 ; 75 }; 76 in
··· 5 ... 6 }: 7 let 8 + 9 + # "Just Give 'Em One of These" - Moe Szyslak 10 + # A __functor that applies context to parametric includes (functions) 11 + funk = 12 + apply: aspect: 13 + aspect 14 + // { 15 + __functor = self: ctx: { 16 + includes = builtins.filter (x: x != { }) (map (apply ctx) (builtins.filter isFn self.includes)); 17 + }; 18 + }; 19 + 20 isFn = f: (builtins.isFunction f) || (f ? __functor); 21 canTake = import ./fn-can-take.nix lib; 22 23 # creates an aspect that inherits class from fromAspect. 24 + owned = 25 + aspect: 26 + aspect 27 + // { 28 + includes = [ ]; 29 + __functor = 30 + self: 31 + # deadnix: skip 32 + { class, aspect-chain }: 33 + self; 34 + }; 35 36 # only static includes from an aspect. 37 statics = 38 aspect: 39 + aspect 40 + // { 41 + __functor = 42 + self: 43 + # deadnix: skip 44 + { class, aspect-chain }@ctx: 45 + funk applyStatics self ctx; 46 }; 47 48 + applyStatics = 49 + ctx: f: 50 + if isStatic f then 51 + f ctx 52 + else if !isFn f then 53 + f 54 + else 55 + { }; 56 57 + isStatic = canTake { 58 + class = ""; 59 + aspect-chain = [ ]; 60 + }; 61 + 62 + take.unused = _unused: used: used; 63 + take.exactly = take canTake.exactly; 64 + take.atLeast = take canTake.atLeast; 65 + take.__functor = 66 + _: takes: ctx: fn: 67 + if takes ctx fn then fn ctx else { }; 68 + 69 + parametric.atLeast = funk take.atLeast; 70 + parametric.exactly = funk take.exactly; 71 + parametric.context = lib.flip parametric.atLeast; 72 + parametric.expands = attrs: funk (ctx: take.atLeast (attrs // ctx)); 73 + parametric.__functor = 74 + self: ctx: 75 + if ctx == true then 76 + self.atLeast 77 + else if ctx == false then 78 + self.exactly 79 + else if isFn ctx then 80 + funk ctx 81 else 82 + self.context ctx; 83 84 aspects = inputs.flake-aspects.lib lib; 85 ··· 94 owned 95 isFn 96 canTake 97 + take 98 ; 99 }; 100 in
+6 -3
templates/bogus/modules/bug.nix
··· 16 tux = inputs.self.nixosConfigurations.igloo.config.users.users.tux; 17 18 expr.len = lib.length tux.packages; 19 - expr.name = lib.getName (lib.head tux.packages); 20 21 - expected.len = 1; 22 - expected.name = "hello"; 23 in 24 { 25 inherit expr expected;
··· 16 tux = inputs.self.nixosConfigurations.igloo.config.users.users.tux; 17 18 expr.len = lib.length tux.packages; 19 + expr.names = map lib.getName tux.packages; 20 21 + expected.len = 2; 22 + expected.names = [ 23 + "hello" 24 + "hello" 25 + ]; 26 in 27 { 28 inherit expr expected;
+34 -22
templates/default/modules/_example/aspects.nix
··· 31 ${host.class}.networking.hostName = host.name; 32 }; 33 34 - # Example: installed on den.defaults for each user contribute into host. 35 - one-hello-package-for-each-user = 36 - { userToHost, ... }: 37 - { 38 - ${userToHost.host.class} = 39 - { pkgs, ... }: 40 - { 41 - users.users.${userToHost.user.userName}.packages = [ pkgs.hello ]; 42 - }; 43 - }; 44 - 45 - # Example: configuration that depends on both host and user. provides to the host. 46 user-to-host-conditional = 47 - { userToHost, ... }: 48 - if userToHost.user.userName == "alice" && !lib.hasSuffix "darwin" userToHost.host.system then 49 { 50 nixos.programs.tmux.enable = true; 51 } 52 else 53 { }; 54 55 - # Example: configuration that depends on both host and user. provides to the host. 56 host-to-user-conditional = 57 - { hostToUser, ... }: 58 - if hostToUser.user.userName == "alice" && !lib.hasSuffix "darwin" hostToUser.host.system then 59 - { 60 - homeManager.programs.git.enable = true; 61 - } 62 - else 63 - { }; 64 65 # Example: luke standalone home-manager has access to rockhopper osConfig specialArg. 66 os-conditional-hm =
··· 31 ${host.class}.networking.hostName = host.name; 32 }; 33 34 + # Example: configuration that depends on both host and user. provides anytime { user, host } is in context. 35 user-to-host-conditional = 36 + { user, host, ... }: 37 + if user.userName == "alice" && !lib.hasSuffix "darwin" host.system then 38 { 39 nixos.programs.tmux.enable = true; 40 } 41 else 42 { }; 43 44 + # Example: adds hello into each user. provides only to OS. 45 + one-hello-package-for-each-user = 46 + { 47 + OS, 48 + user, 49 + host, 50 + ... 51 + }: 52 + den.lib.take.unused [ OS ] { 53 + ${host.class} = 54 + { pkgs, ... }: 55 + { 56 + users.users.${user.userName}.packages = [ pkgs.hello ]; 57 + }; 58 + }; 59 + 60 + # Example: configuration that depends on both host and user. provides only to HM. 61 host-to-user-conditional = 62 + { 63 + HM, 64 + user, 65 + host, 66 + ... 67 + }: 68 + den.lib.take.unused [ HM ] ( 69 + if user.userName == "alice" && !lib.hasSuffix "darwin" host.system then 70 + { 71 + homeManager.programs.git.enable = true; 72 + } 73 + else 74 + { } 75 + ); 76 77 # Example: luke standalone home-manager has access to rockhopper osConfig specialArg. 78 os-conditional-hm =