fake.modules transposition for aspect-oriented Dendritic Nix. with cross-aspect dependencies. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ dendrix.oeiuwq.com/Dendritic.html
dendritic nix aspect oriented

readme (#32)

authored by oeiuwq.com and committed by

GitHub cb78001b f338a506

+124 -125
+124 -125
README.md
··· 79 79 80 80 Unlike `flake.modules.<class>.<aspect>` which is _flat_, aspects can be nested forming a _tree_ by using the `provides` (short alias: `_`) attribute. Each aspect can also specify a list of `includes` of other aspects, forming a _graph_ of dependencies. 81 81 82 - ```nix 83 - { 84 - flake.aspects = { 85 - gaming = { 86 - nixos = {}; 87 - darwin = {}; 82 + --- 88 83 89 - _.emulation = { aspect, ... }: { 90 - nixos = {}; 84 + ## Usage 91 85 92 - _.nes.nixos = {}; 93 - _.gba.nixos = {}; 86 + ### As a `flake-parts` Module 94 87 95 - includes = with aspect._; [ nes gba ]; 96 - }; 88 + ```nix 89 + { inputs, ... }: { 90 + imports = [ inputs.flake-aspects.flakeModule ]; 91 + flake.aspects = { 92 + sliding-desktop = { 93 + nixos = { }; # Niri on Linux 94 + darwin = { }; # Paneru on macOS 97 95 }; 96 + awesome-cli = { 97 + nixos = { }; darwin = { }; homeManager = { }; nixvim = { }; 98 + }; 99 + }; 100 + flake.nixosConfigurations.my-host = inputs.nixpkgs.lib.nixosSystem { 101 + modules = [ 102 + inputs.self.modules.nixos.sliding-desktop # read resolved module 103 + ]; 98 104 }; 99 105 } 100 106 ``` 101 107 102 - ## Usage 108 + ### Without Flakes ([test](checkmate/modules/tests/without_flakes.nix)) 103 109 104 - The library can be used in two ways: as a flakes-independent dependency-free utility or as a `flake-parts` module. 110 + ```nix 111 + let 105 112 106 - ### As a Dependency-Free Library (`./nix/default.nix`) 113 + myModules = (lib.evalModules { 114 + modules = [ 115 + (new-scope "my") # creates my.aspects and my.modules. 116 + { my.aspects.laptop.nixos = ...; } 117 + ]; 118 + }).config.my.modules; 107 119 108 - The core of this project is the [`transpose`](nix/default.nix) function, which is powerful enough to implement cross-aspect dependencies for any Nix configuration class. It accepts an optional `emit` function that can be used to ignore items, modify them, or generate multiple items from a single input. 120 + in lib.nixosSystem { modules = [ myModules.nixos.laptop ]; }; 121 + ``` 109 122 110 - ```nix 111 - let transpose = import ./nix/default.nix { lib = pkgs.lib; }; in 112 - transpose { a.b.c = 1; } # => { b.a.c = 1; } 113 - ``` 123 + Useful for libraries that want isolated aspect scopes or flake-parts independence (see [`den`'s scope](https://github.com/vic/den/blob/main/nix/scope.nix)). 114 124 115 - This `emit` function is utilized by the [`aspects`](nix/aspects.nix) library to manage module dependencies between different aspects of the same class. Both `transpose` and `aspects` are independent of flakes. 125 + --- 116 126 117 - #### Use aspects without flakes. 127 + ## API ([nix/lib.nix](nix/lib.nix)) 118 128 119 - It is possible to use the aspects system as a library, [without flakes](https://github.com/vic/flake-aspects/blob/b94d806/checkmate.nix#L76). This can be used, for example, to avoid poluting flake-parts' `flake.modules` or by libraries that want to create own isolated aspects scope. For examples of this, see our own [flake-parts integration](nix/flakeModule.nix), and how [`den`](https://github.com/vic/den) creates its own [`den.aspects` scope](https://github.com/vic/den/blob/main/nix/scope.nix) independent of `flakes.aspects`/`flake.modules`. 129 + | Export | Description | 130 + | --------------------- | ----------------------------------------------------------- | 131 + | `transpose { emit? }` | Generic 2-level transposition | 132 + | `types` | Nix type system for aspects and providers | 133 + | `aspects` | Aspect-aware transposition with resolution | 134 + | `new` | Low-level scope factory (callback-based) | 135 + | `new-scope` | Named scope factory (`${name}.aspects` / `${name}.modules`) | 136 + | `forward` | Cross-class module forwarding | 120 137 121 - ### As a Dendritic Flake-Parts Module (`flake.aspects` option) 138 + ### Core: `transpose` ([nix/default.nix](nix/default.nix)) 122 139 123 - When used as a `flake-parts` module, the `flake.aspects` option is automatically transposed into `flake.modules`, making the modules available to consumers of your flake. 140 + Generic 2-level attribute set transposition parameterized by an `emit` function. 124 141 125 142 ```nix 126 - # The code in this example can (and should) be split into different Dendritic modules. 127 - { inputs, ... }: { 128 - imports = [ inputs.flake-aspects.flakeModule ]; 129 - flake.aspects = { 143 + transpose { a.b.c = 1; } # ⇒ { b.a.c = 1; } 144 + ``` 130 145 131 - sliding-desktop = { 132 - description = "Next-generation tiling windowing"; 133 - nixos = { }; # Configure Niri on Linux 134 - darwin = { }; # Configure Paneru on macOS 135 - }; 146 + `emit` receives `{ child, parent, value }` and returns a list of `{ parent, child, value }` items. Default: `lib.singleton` (identity). This allows users to filter, modify or multiply items being transposed. This is exploited by [nix/aspects.nix](nix/aspects.nix) to intercept each transposition and inject [resolution](nix/resolve.nix). 136 147 137 - awesome-cli = { 138 - description = "Enhances the environment with the best of CLI and TUI"; 139 - nixos = { }; # OS services 140 - darwin = { }; # Apps like ghostty, iTerm2 141 - homeManager = { }; # Fish aliases, TUIs, etc. 142 - nixvim = { }; # Plugins 143 - }; 148 + Tests: [transpose_swap](checkmate/modules/tests/transpose_swap.nix), [transpose_common](checkmate/modules/tests/transpose_common.nix), [tranpose_flake_modules](checkmate/modules/tests/tranpose_flake_modules.nix). 144 149 145 - work-network = { 146 - description = "Work VPN and SSH access"; 147 - nixos = {}; # Enable OpenSSH 148 - darwin = {}; # Enable macOS SSH server 149 - terranix = {}; # Provision VPN 150 - hjem = {}; # Home: link .ssh keys and configs 151 - }; 150 + ### Resolution: `resolve` ([nix/resolve.nix](nix/resolve.nix)) 152 151 153 - }; 154 - } 155 - ``` 152 + Recursive dependency resolver. Given a `class` and an `aspect-chain` (the call stack of aspects that led here -- most recent first), it extracts the class-specific config and recursively resolves all `includes`. 156 153 157 - #### Declaring Cross-Aspect Dependencies 154 + The `aspect-chain` lets providers know who is including them and make decisions based on call context. Tests: [aspect_chain](checkmate/modules/tests/aspect_chain.nix), [aspect_modules_resolved](checkmate/modules/tests/aspect_modules_resolved.nix). 158 155 159 - Aspects can declare dependencies on other aspects using the `includes` attribute. This allows you to compose configurations in a modular way. 156 + ### Scope Factories ([nix/new.nix](nix/new.nix), [nix/new-scope.nix](nix/new-scope.nix)) 160 157 161 - Dependencies are defined at the aspect level, not within individual modules. When a module from an aspect is evaluated (e.g., `flake.modules.nixos.development-server`), the library resolves all dependencies for the `nixos` class and imports the corresponding modules if they exist. 158 + `new` is a callback-based factory: `new (option: transposed: moduleDefinition) aspectsConfig`. The [flakeModule](nix/flakeModule.nix) uses it to wire `flake.aspects → flake.modules`. 162 159 163 - In the example below, the `development-server` aspect includes the `alice` and `bob` aspects. This demonstrates how to create a consistent development environment across different operating systems and user configurations. 160 + `new-scope` wraps `new` to create named scopes: `new-scope "foo"` produces `foo.aspects` (input) and `foo.modules` (output). Multiple independent namespaces can coexist. Tests: [without_flakes](checkmate/modules/tests/without_flakes.nix), [aspect_assignment](checkmate/modules/tests/aspect_assignment.nix). 164 161 165 - ```nix 166 - { 167 - flake.aspects = { aspects, ... }: { 168 - development-server = { 169 - # This aspect now includes modules from 'alice' and 'bob'. 170 - includes = with aspects; [ alice bob ]; 162 + ### Forward ([nix/forward.nix](nix/forward.nix)) 171 163 172 - # Without flake-aspects, you would have to do this manually for each class. 173 - # nixos.imports = [ inputs.self.modules.nixos.alice ]; 174 - # darwin.imports = [ inputs.self.modules.darwin.bob ]; 175 - }; 164 + Cross-class configuration forwarding. Routes resolved modules from one class into a submodule path of another class. Used by [`den`](https://github.com/vic/den) to forward `homeManager` modules into `nixos.home-manager.users.<name>`. Test: [forward](checkmate/modules/tests/forward.nix). 176 165 177 - alice = { 178 - nixos = {}; 179 - homeManager = {}; 180 - }; 166 + --- 181 167 182 - bob = { 183 - darwin = {}; 184 - hjem = {}; 185 - }; 186 - }; 187 - } 188 - ``` 168 + ## Dependency Resolution 189 169 190 - Creating the final OS configurations is outside the scope of this library—for that, see [`vic/den`](https://github.com/vic/den). However, exposing them would look like this: 170 + ### `includes` — Cross-Aspect Dependencies ([test](checkmate/modules/tests/aspect_dependencies.nix)) 191 171 192 172 ```nix 193 - { inputs, ... }: 194 - { 195 - flake.nixosConfigurations.fooHost = inputs.nixpkgs.lib.nixosSystem { 196 - system = "x86_64-linux"; 197 - modules = [ inputs.self.modules.nixos.development-server ]; 173 + flake.aspects = { aspects, ... }: { 174 + server = { 175 + includes = with aspects; [ networking monitoring ]; 176 + nixos = { }; 198 177 }; 199 - 200 - flake.darwinConfigurations.fooHost = inputs.darwin.lib.darwinSystem { 201 - system = "aarch64-darwin"; 202 - modules = [ inputs.self.modules.darwin.development-server ]; 203 - }; 204 - } 178 + networking.nixos = { }; 179 + monitoring.nixos = { }; 180 + }; 205 181 ``` 206 182 207 - ### Advanced Aspect Dependencies: Providers 208 - 209 - Dependencies are managed through a powerful abstraction called **providers**. A provider is a value that returns an aspect object, which can then supply modules to the aspect that includes it. 210 - 211 - A provider can be either a static aspect object or a function that dynamically returns one. This mechanism enables sophisticated dependency chains, conditional logic, and parameterization. 183 + When `flake.modules.nixos.server` is evaluated, it resolves to `{ imports = [ server.nixos, networking.nixos, monitoring.nixos ] }`. Only classes that exist on the included aspect are imported. 212 184 213 - #### Default Provider (`__functor`) 185 + ### Providers — `provides` / `_` ([test](checkmate/modules/tests/aspect_provides.nix)) 214 186 215 - Each aspect is itself a provider via its hidden option `__functor` (see `nix/types.nix`). You can include aspects directly. 187 + Aspects can expose sub-aspects as providers. `_` is an alias for `provides`. 216 188 217 189 ```nix 218 - # A 'foo' aspect that depends on 'bar' and 'baz' aspects. 219 190 flake.aspects = { aspects, ... }: { 220 - foo.includes = with aspects; [ bar baz ]; 221 - } 191 + gaming = { 192 + nixos = { }; 193 + _.emulation = { 194 + nixos = { }; 195 + _.nes.nixos = { }; 196 + }; 197 + }; 198 + my-host.includes = [ aspects.gaming._.emulation._.nes ]; 199 + }; 222 200 ``` 223 201 224 - #### Custom Providers 202 + Providers receive `{ class, aspect-chain }` and can use them for conditional logic or context-aware configuration. The `aspect-chain` tracks the full inclusion path. 225 203 226 - You can define custom providers to implement more complex logic. A provider function receives the current `class` (e.g., `"nixos"`) and the `aspect-chain` (the list of aspects that led to the call). This allows a provider to act as a conditional proxy or router for dependencies. 204 + ### Fixpoint Semantics ([test](checkmate/modules/tests/aspect_fixpoint.nix)) 227 205 228 - In this example, the `kde-desktop` aspect defines a custom `karousel` provider that only returns a module if certain conditions are met: 206 + The top-level `aspects` argument is a fixpoint: providers at any depth can reference siblings or top-level aspects. 229 207 230 208 ```nix 231 - flake.aspects.kde-desktop._.karousel = { aspect-chain, class }: 232 - if someCondition aspect-chain && class == "nixos" then { nixos = { ... }; } else { }; 209 + flake.aspects = { aspects, ... }: { 210 + two.provides = { aspects, ... }: { 211 + sub = { includes = [ aspects.sibling ]; classOne = { }; }; 212 + sibling.classOne = { }; 213 + }; 214 + one.includes = [ aspects.two._.sub ]; 215 + }; 233 216 ``` 234 217 235 - The `karousel` provider can then be included in another aspect: 218 + ### Parametric Providers ([test](checkmate/modules/tests/aspect_parametric.nix)) 219 + 220 + Curried functions act as parametric providers: 236 221 237 222 ```nix 238 223 flake.aspects = { aspects, ... }: { 239 - home-server.includes = [ aspects.kde-desktop._.karousel ]; 240 - } 224 + base._.user = userName: { 225 + nixos.users.${userName}.isNormalUser = true; 226 + }; 227 + server.includes = [ (aspects.base._.user "bob") ]; 228 + }; 241 229 ``` 242 230 243 - This pattern allows an included aspect to determine which configuration its caller should use, enabling a tree of dependencies where each node can be either static or parametric. 231 + ### Top-Level Parametric Aspects ([test](checkmate/modules/tests/aspect_toplevel_parametric.nix)) 232 + 233 + Top-level aspects can also be curried providers: 244 234 245 - #### Parameterized Providers 235 + ```nix 236 + flake.aspects = { aspects, ... }: { 237 + greeter = { message }: { nixos.greeting = message; }; 238 + host.includes = [ (aspects.greeter { message = "hello"; }) ]; 239 + }; 240 + ``` 246 241 247 - Providers can be implemented as curried functions, allowing you to create parameterized modules. This is useful for creating reusable configurations that can be customized at the inclusion site. 242 + ### `__functor` Override ([test](checkmate/modules/tests/aspect_default_provider_functor.nix), [test](checkmate/modules/tests/aspect_default_provider_override.nix)) 248 243 249 - For real-world examples, see how `vic/den` defines [auto-imports](https://github.com/vic/den/blob/main/modules/aspects/batteries/import-tree.nix) and [home-managed](https://github.com/vic/den/blob/main/modules/aspects/batteries/home-managed.nix) parametric aspects. 244 + The default `__functor` just returns the aspect itself. However, you can override the `__functor` to allow an aspect to intercept when it is being included and provide different config depending on who is including it. 250 245 251 246 ```nix 252 247 flake.aspects = { aspects, ... }: { 253 - system = { 254 - nixos.system.stateVersion = "25.11"; 255 - _.user = userName: { 256 - darwin.system.primaryUser = userName; 257 - nixos.users.${userName}.isNormalUser = true; 258 - }; 248 + foo = { 249 + nixos = { ... }; 250 + __functor = self: 251 + { class, aspect-chain }: 252 + if class == "nixos" then self else { darwin = ...; includes = [ ... ]; }; 259 253 }; 254 + }; 255 + ``` 260 256 261 - home-server.includes = [ 262 - aspects.system 263 - (aspects.system._.user "bob") 264 - ]; 257 + ### Forward ([nix/forward.nix](nix/forward.nix)) ([test](checkmate/modules/tests/forward.nix)) 258 + 259 + Route modules from one class into a submodule path of another: 260 + 261 + ```nix 262 + forward { 263 + each = host.users; 264 + fromClass = _user: "homeManager"; 265 + intoClass = _user: "nixos"; 266 + intoPath = user: [ "home-manager" "users" user.name ]; 267 + fromAspect = user: den.aspects.${user.name}; 265 268 } 266 269 ``` 267 270 268 - See the `aspects."test provides"` and `aspects."test provides using fixpoints"` sections in the [checkmate tests](checkmate.nix) for more examples of chained providers. 269 - 270 - #### The `_` Alias for `provides` 271 - 272 - For convenience, `_` is an alias for `provides`. This allows for more concise chaining of providers. For example, `foo.provides.bar.provides.baz` can be written as `foo._.bar._.baz`. 271 + --- 273 272 274 273 ## Testing 275 274