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

Improve support for namespaces. Social denful ! (#45)

Enhance README with clearer aspect descriptions

Updated README to clarify aspects and configurations.

authored by oeiuwq.com and committed by

GitHub 83e238f3 c7923378

+417 -484
+127 -346
README.md
··· 18 18 19 19 <img width="300" height="300" alt="den" src="https://github.com/user-attachments/assets/af9c9bca-ab8b-4682-8678-31a70d510bbb" /> 20 20 21 - - Dendritic: Same concern over different classes. 21 + - Dendritic: same concern, different classes and context-aware. 22 22 23 - - Small and [DRY](modules/aspects/provides/unfree.nix)<br> 24 - [`class` generic](modules/aspects/provides/primary-user.nix) Nix configurations. 23 + - Small, [DRY](modules/aspects/provides/unfree.nix) & [`class`-generic](modules/aspects/provides/primary-user.nix) modules. 25 24 26 - - Context-Aware aspects.<br>[Parametric](modules/aspects/provides/define-user.nix) over `host`/`home`/`user`. 25 + - [Parametric](modules/aspects/provides/define-user.nix) over `host`/`home`/`user`. 27 26 28 - - Stop copying and **share**(tm)<br> 29 - aspects across systems and Dendritic repos. 27 + - [Share](templates/default/modules/_profile/namespace.nix) aspects across systems & repos. 30 28 31 - - Bidirectional [configurations](modules/aspects/dependencies.nix).<br> 32 - Users can contribute to their Host configuration and the other way around. 33 - 34 - - Custom factories and output attributes.<br> 35 - Support any new `class` of Nix configurations. 29 + - Bidirectional [dependencies](modules/aspects/dependencies.nix): user/host contributions. 36 30 37 - - Use your `stable`/`unstable` input channels. 31 + - Custom factories for any Nix `class`. 38 32 39 - - _Freeform_ `host`/`user`/`home` [schemas](modules/_types.nix).<br> 40 - Avoid the need for using `specialArgs`. 33 + - Use `stable`/`unstable` channels per config. 41 34 42 - - Multi-platform, Multi-tenant hosts. 35 + - Freeform `host`/`user`/`home` [schemas](modules/_types.nix) (no `specialArgs`). 43 36 44 - - Reuse aspects across hosts, OS, homes. 37 + - Multi-platform, multi-tenant hosts. 45 38 46 - - [Batteries Included](modules/aspects/provides/)<br> 47 - Opt-in and replaceable. 39 + - [Batteries](modules/aspects/provides/): Opt-in, replaceable aspects. 48 40 49 - - [Well tested](templates/default/modules/_example/ci.nix)<br> 50 - Suite exercises all [features with examples](templates/default/modules/). 41 + - [Well-tested](templates/default/modules/_example/ci.nix) with an [example suite](templates/default/modules/). 51 42 52 43 Need more batteries? See [vic/denful](https://github.com/vic/denful). 53 44 54 - Join the community [discussion](https://github.com/vic/den/discussions). Ask questions, share how you use `den`. 45 + Join our [community discussion](https://github.com/vic/den/discussions). 55 46 56 47 </div> 57 48 </td> 58 49 <td> 59 50 60 - 🏠 Concise definitions of 61 - [Hosts, Users](templates/default/modules/_example/hosts.nix) and [Standalone-Homes](templates/default/modules/_example/homes.nix). 51 + 🏠 Define [Hosts, Users](templates/default/modules/_example/hosts.nix) & [Homes](templates/default/modules/_example/homes.nix) concisely. 62 52 63 - See [\_types.nix](modules/_types.nix) for complete schema. 53 + See schema in [`_types.nix`](modules/_types.nix). 64 54 65 55 ```nix 66 56 # modules/hosts.nix 57 + # OS & standalone homes share 'vic' aspect. 58 + # $ nixos-rebuild switch --flake .#my-laptop 59 + # $ home-manager switch --flake .#vic 67 60 { 68 - # This example defines the following aspects: 69 - # den.aspects.my-laptop and den.aspects.vic 70 - # standalone-hm and nixos-hm share vic aspect 71 - 72 - # $ nixos-rebuild switch --flake .#my-laptop 73 - den.hosts.x86-64-linux.my-laptop.users.vic = {}; 74 - # $ home-manager switch --flake .#vic 75 - den.homes.aarch64-darwin.vic = { }; 76 - 77 - # That's it! Now lets attach configs via aspects 61 + den.hosts.x86-64-linux.laptop.users.vic = {}; 62 + den.homes.aarch64-darwin.vic = {}; 78 63 } 79 64 ``` 80 65 81 - 🧩 [Aspect-oriented](https://github.com/vic/flake-aspects) configurations. ([example](templates/default/modules/_example/aspects.nix)) 66 + 🧩 [Aspect-oriented](https://github.com/vic/flake-aspects) incremental features. ([example](templates/default/modules/_example/aspects.nix)) 67 + 68 + Any module can contribute configurations to aspects. 82 69 83 70 ```nix 84 - # modules/my-laptop.nix -- Attach behaviour to host 85 - { den, ... }: 86 - { 71 + # modules/my-laptop.nix 72 + { den, ... }: { 87 73 den.aspects.my-laptop = { 88 - # dependency graph on other shared aspects 89 74 includes = [ 90 - # my parametric { host } => aspect 91 - den.aspects.vpn # setups firewall/daemons 92 - # opt-in, replaceable batteries included 93 - den._.home-manager 75 + den.aspects.vpn # { host } => config 76 + den._.home-manager # battery 94 77 ]; 95 - # provide same features at any OS/platform 96 - nixos = ...; # (see nixos options) 97 - darwin = ...; # (see nix-darwin options) 98 - # contrib hm-config to all my-laptop users 78 + nixos = { /* NixOS options */ }; 79 + darwin = { /* nix-darwin options */ }; 80 + # For all users of my-laptop 99 81 homeManager.programs.vim.enable = true; 100 82 }; 101 83 } 102 84 103 - # modules/vic.nix -- Reused in OS/standalone HM 104 - { den, ... }: 105 - { 85 + # modules/vic.nix 86 + { den, ... }: { 106 87 den.aspects.vic = { 107 - homeManager = ...; # (see home-manager options) 108 - # user can contrib to hosts where it lives 109 - nixos.users.users.vic.description = "oeiuwq"; 88 + homeManager = { /* ... */ }; 89 + # User contribs to host 90 + nixos.users.users = { 91 + vic.description = "oeiuwq"; 92 + } 110 93 includes = [ 111 94 den.aspects.tiling-wm 112 - # parametric { user, host } => aspect 113 - den._.primary-user # vic is admin 95 + den._.primary-user 114 96 ]; 115 97 }; 116 98 } 117 99 ``` 118 100 119 - For real-world examples, see [`vic/vix`](https://github.com/vic/vix/tree/den) 120 - or try this [GH search](https://github.com/search?q=vic%2Fden+language%3ANix&type=code). 101 + For real-world examples, see [`vic/vix`](https://github.com/vic/vix/tree/den) or this [GH search](https://github.com/search?q=vic%2Fden+language%3ANix&type=code). 121 102 122 - **❄️ Try it now! Launch our template VM:** 103 + **❄️ Try it now!** 123 104 105 + Launch our template VM: 124 106 ```console 125 107 nix run github:vic/den 126 108 ``` 127 109 128 - Or clone it and run the VM as you edit 110 + Or, initialize a project: 129 111 130 112 ```console 131 113 nix flake init -t github:vic/den ··· 133 115 nix run .#vm 134 116 ``` 135 117 136 - Our [default template](templates/default) provides a [layout](templates/default/modules/_profile/) for quickstart. 118 + Our [default template](templates/default) provides a [profile-based layout](templates/default/modules/_profile/) for a quick start. 137 119 138 120 </td> 139 121 </tr> 140 122 </table> 141 123 142 - You are done! You know everything there is to know about `den` for creating configurations with it. 124 + You are done! You know everything to start creating configurations with `den`. 125 + 126 + Feel free to to __explore__ the codebase, particularly our [included batteries](modules/aspects/provides) and [tests](templates/default/modules/_example). 143 127 144 - However, if you want to learn more about how it works, I have tried to document some other topics in collapsible sections to avoid distraction from the introduction. 128 + # Learn More 129 + 130 + If you want to learn how `den` works, the following sections detail its concepts and patterns. 145 131 146 132 <details> 147 133 <summary> 148 134 149 - # Basic Concepts and Patterns. 135 + ### Basic Concepts 150 136 151 - > Learn about aspects, static and parametric. Default aspects and dependencies. 137 + > Learn about static vs. parametric aspects, the default aspect, and dependencies. 152 138 153 139 </summary> 154 140 155 - There are two fundamental types of aspects in `den`, _static_ and _parametric_. 141 + `den` has two fundamental types of aspects: _static_ and _parametric_. 156 142 157 143 <table> 158 144 <tr> 159 145 <td> 160 146 161 - #### **Static** aspects are just attribute sets 147 + #### **Static** aspects are attribute sets. 148 + 149 + An aspect is a set of modules for different `classes`, configuring a single concern across them. 162 150 163 151 ```nix 164 - # An aspect is a collection of many 165 - # Nix configuration modules, each having 166 - # a different `class`. 167 152 den.aspects.my-laptop = { 168 - nixos = { }; 169 - darwin = { }; 170 - homeManager = { }; 153 + nixos = { /* ... */ }; 154 + darwin = { /* ... */ }; 171 155 172 - # aspects can be nested via `_` (`provides`) 173 - # forming a tree structure. 156 + # Nested aspects via `_` 157 + # (alias for `provides`) 174 158 _.gaming = { 175 - nixos = { }; 176 - 177 - # aspects can also `include` others 178 - # forming a graph of dependencies 179 - includes = [ 180 - # gaming.nixos module will mixin 181 - # nvidia-gpu.nixos module if any. 182 - den.aspects.nvidia-gpu 183 - ]; 159 + nixos = { /* ... */ }; 160 + # Dependency graph via `includes` 161 + includes = [ den.aspects.nvidia-gpu ]; 184 162 }; 185 - 186 163 }; 187 164 ``` 188 165 189 - > **TIP** 190 - > **`_`** is an alias for `provides`. In many examples you will see `foo._.bar._.baz` instead of `foo.provides.bar.provides.baz`. 191 - 192 166 </td> 193 167 <td> 194 168 195 - #### **Parametric** aspects are just functions. 169 + #### **Parametric** aspects are functions. 170 + 171 + They take a `context` and return a configuration. 196 172 197 173 ```nix 198 - # The convention is to always use named args. 199 - # These required args is the **context** the 200 - # aspect needs to provide configuration. 201 - hostParametric = { host }: { 174 + # A `{ host, gaming }` contextual aspect. 175 + hostFunction = { host, gaming }: { 202 176 nixos.networking.hostName = host.hostName; 203 - 204 - # A parametric aspect can also ask other 205 - # aspects for context-aware configurations. 206 - # Here, we ask two other parametric aspects 207 - # given the `{ host, gaming }` context. 208 - includes = let 209 - context.host = host; 210 - context.gaming.emulators = [ "nes" ]; 211 - in map (f: f context) [ 212 - den.default 213 - den.aspects.gaming-platform 214 - ]; 215 177 }; 216 - ``` 217 178 218 - #### Important **context** variants in `den`. 219 - 220 - Den uses the following contexts by default. 221 - 222 - The aspect system is not limited to these,<br> 223 - but these are used to describe [dependencies](modules/aspects/dependencies.nix)<br> 224 - between hosts/users, homes and default configs. 225 - 226 - - `{ host }` - For host _OS_ level configs. 227 - - `{ user, host }` - For user _Home_ level configs. 228 - - `{ home }` - For standalone _Home_ configs. 179 + # A parametric aspect can request context-aware 180 + # configurations from other aspects. 181 + hostParametric = { host }: { 182 + __functor = den.lib.parametric { 183 + inherit host; 184 + gaming.emulators = [ "nes" ]; 185 + }; 186 + includes = [ hostFunction den.default ]; 187 + } 188 + ``` 229 189 230 190 </td> 231 191 </tr> 232 192 </table> 233 193 234 - ### The Default aspect and default Dependencies 235 - 236 - Den has an special aspect at `den.default` that serves for global configuration. `den.default` is **included by default** in all *Hosts*, *Users* and *Homes*. For example a Home configuration invokes `den.default { inherit home; }`, to obtain the aggregated defaults for home contexts. 194 + `den` uses several default contexts to manage [dependencies](modules/aspects/dependencies.nix): 237 195 238 - <table> 239 - <tr> 240 - <td> 241 - 242 - #### Registering defaults values 243 - 244 - ```nix 245 - { 246 - # you can assign static values directly. 247 - den.default.nixos = { 248 - system.stateVersion = "25.11"; 249 - }; 250 - 251 - # you can also include other aspects 252 - den.default.includes = [ 253 - { darwin.system.stateVersion = 6; } 254 - ] 255 - } 256 - ``` 257 - 258 - It is possible to also register context-aware parametric aspects in `den.defaults`. This is how you can provide a default to all hosts or users that match a condition. 259 - 260 - ```nix 261 - # This example is split to aid reading. 262 - let 263 - hasOneUser = host: 264 - length (attrNames host.users) == 1; 265 - hasUser = host: user: 266 - hasAttr user.name host.users; 267 - makeAdmin = user: { 268 - nixos.users.users.${user.name} = { 269 - extraGroups = [ "wheel" ]; 270 - }; 271 - }; 272 - 273 - # IFF host has ONE user, make it admin 274 - single-user-is-admin = { host, user }: 275 - if hasOneUser host && hasUser host user 276 - then makeAdmin user else { }; 277 - in 278 - { 279 - den.default.includes = [ 280 - # will be called *ONLY* on when the 281 - # `{ host, user }` context is used. 282 - single-user-is-admin 283 - ]; 284 - } 285 - ``` 286 - 287 - #### Custom parametric providers. 288 - 289 - The following is the code for how `den.default` is 290 - defined. 291 - 292 - ```nix 293 - { den, ... }: { 294 - den.default.__functor = den.lib.parametric true; 295 - } 296 - ``` 297 - 298 - You can do the very same for other aspects of you 299 - that can have context-aware aspects in their `.includes`. 300 - 301 - ```nix 302 - { den, ...}: { 303 - den.aspects.gaming.__functor = den.lib.parametric true; 304 - 305 - # any other file can register gaming aspects 306 - den.aspects.gaming.includes = [ 307 - ({ emulation }: { 308 - nixos = ...; 309 - includes = [ den.aspects.steam ]; 310 - }) 311 - 312 - { nixos = ...; } # always included 313 - ({ gpu }: { }) # non-match on my-laptop 314 - ]; 315 - 316 - # then you can depend on the gaming aspect: 317 - den.aspects.my-laptop.includes = [ 318 - (den.aspects.gaming { emulation = "neogeo"; }) 319 - ]; 320 - 321 - } 322 - ``` 323 - 324 - For more examples on parametric aspects explore our [batteries](modules/aspects/provides/). 325 - 326 - > Use the source, Luke! 196 + - `{ host }`, `{ user, host }`, `{ home }` 197 + - `{ fromUser, toHost }`, `{ fromHost, toUser }` 327 198 328 199 </td> 329 - 330 - <td> 331 - 332 - #### Aspect dependencies 333 - 334 - Accessing an aspect module causes `flake-aspects` to resolve it 335 - by including the aspect's own class module and the same-class module 336 - of all its transitive includes. 337 - 338 - Additional to this, `den` registers some special [dependencies](modules/aspects/dependencies.nix) 339 - designed to aid on Den particular use case: Host/Users, Homes. 340 - 341 - ###### Host dependencies 342 - 343 - Host also include `den.aspects.<host> { inherit host; }` meaning 344 - all included parametric aspects have an opportunity to produce 345 - aditional configurations for the host. 346 - 347 - Also for each user, `den.aspects.<user> { inherit host user; }` 348 - is called. So `den.aspects.alice.nixos` can provide config to 349 - all hosts where alice lives. And also has the opportunity to 350 - register parametric aspects on `alice.provides` that inspect 351 - the host attribute to contionally produce other aspects. 200 + </tr> 201 + </table> 352 202 353 - ###### User dependencies 203 + ### The Default Aspect and Dependencies 354 204 355 - User modules are read from [os-home](modules/aspects/provides/home-manager.nix) configurations. 205 + `den` features a special aspect, `den.default`, which applies global configurations. It is automatically included in all hosts, users, and homes, receiving the appropriate context (e.g., `den.default { inherit home; }`). 356 206 357 - It basically invokes `den.aspects.<user> { inherit host user; }` but 358 - this time the host also contributes back generic configs to the users. 359 - If you are wondering how this is not recursive, the answer is by using 360 - our _contexts_, user-to-host dependencies start with a `{ host }` context, 361 - while host-to-user dependencies start with a `{ user, host }` context, and 362 - even when both are given to `den.aspects.<user> ctx` the results are 363 - non recursive. 207 + You can register static values or context-aware parametric aspects within `den.default`. This allows you to define defaults that apply conditionally to all hosts or users. 364 208 365 - A user also depends on `den.default { inherit host user; }`. 366 - 367 - ###### Home dependencies 368 - 369 - Home just uses its own module, its includes, and invokes `den.default { inherit home; }`. 370 - 371 - </td> 372 - 373 - </tr> 374 - </table> 209 + A key feature of `den` is its management of aspect dependencies. When an aspect is resolved, `flake-aspects` includes its class-specific module along with those of its transitive `includes`. `den` extends this by registering special [dependencies](modules/aspects/dependencies.nix) to link hosts and users. For instance, a host's configuration includes contributions from each of its users' aspects, and a user's home configuration includes contributions from its host's aspect, creating a bidirectional flow of settings. This is achieved without recursion by using distinct contexts for each direction. 375 210 376 211 </details> 377 212 378 213 <details> 379 214 <summary> 380 215 381 - # Aspect Organization Patterns 216 + ### Organization Patterns 382 217 383 - > Learn about organizing patterns for reuse. 218 + > Learn patterns for organizing and reusing aspects. 384 219 385 220 </summary> 386 221 387 - No two nix configurations are the same. We all tend to organize things as we feel better. 388 - This section will try to outline some hints on possible ways to organize aspects, none 389 - of this is mandatory, and indeed you are encouraged to explore and [share](https://github.com/vic/den/discussions) your own patterns and insights. 222 + While `den` is unopinionated about organization, these patterns can help structure your aspects for clarity and reuse. 390 223 391 - #### Having a _namespace_ of aspects. 392 - 393 - The first principle is using `.provides.` (aka [`._.`](https://github.com/vic/den/discussions/34)) to nest your aspects as they make sense for you. 394 - 395 - Unlike normal flake-parts, where modules are _flat_ and people tend to use names like `flake.modules.nixos."host/my-laptop"` or `nixos."feature/gaming"` to avoid collission, in `den` you have a proper tree structure. 396 - 397 - I (_vic_), use an aspect `vix` for all _features_ on my system, and from there I create sub-aspects. 398 - 399 - Because writing `den.aspects.vix._.gaming._.emulation` tends to be repetitive, I use the following `vix` alias as module argument. 400 - 401 - > This pattern is also shown in the default template, under [`_profile`](templates/default/modules/_profile/). 224 + #### Aspect Namespaces 402 225 403 - > **NOTE**: 404 - > `den` provides an [__angle brackets__](https://fzakaria.com/2025/08/10/angle-brackets-in-a-nix-flake-world) **experimental feature** that allows even shorter syntax for deep `.provides.` access. 405 - > See [import-non-dendritic.nix](templates/default/modules/_example/import-non-dendritic.nix) for an example usage. 226 + Group related aspects under a single top-level aspect and alias it as a module argument for easier access. The `den.namespace` function streamlines this. 406 227 407 228 <table> 408 229 <tr> 409 230 <td> 410 231 232 + #### Using local and remote namespaces 233 + 411 234 ```nix 412 235 # modules/namespace.nix 413 - { config, ... }: 236 + { inputs, ... }: 414 237 { 415 - den.aspects.vix = { }; 416 - _module.args.vix = config.den.aspects.vix._; 238 + imports = [ 239 + # create local `pro` namespace 240 + (inputs.den.namespace "pro" true) 241 + # mixin remote `pro` from input 242 + (inputs.den.namespace "pro" inputs.foo) 243 + ]; 417 244 } 418 245 ``` 419 246 420 247 </td> 421 248 <td> 422 249 250 + #### Directly read and write to namespace 251 + 423 252 ```nix 424 253 # modules/my-laptop.nix 425 - { vix, ... }: 254 + { pro, ... }: 426 255 { 427 - vix.gaming = { 428 - _.steam.includes = [ 429 - vix.gpu 430 - vix.performance-profile 431 - ]; 432 - }; 256 + pro.gaming.includes = [ pro.gpu ]; 433 257 } 434 258 ``` 435 259 436 - See [real-life example from vic/vix](https://github.com/vic/vix/blob/den/modules/hosts/nargun.nix) 437 - 438 260 </td> 439 261 </tr> 440 262 </table> 441 263 442 - #### Using parametric aspects to route configurations. 264 + #### Angle-Bracket Syntax 443 265 444 - The following example routes configurations into the `vix` namespace. 445 - This is just an example of using parametric aspects to depend on other 446 - aspects in any part of the tree. 266 + For deeply nested aspects, `den` offers an experimental feature to shorten access paths. By bringing `den.lib.__findFile` into scope, you can use angle brackets to reference aspects more concisely. 447 267 448 - <table> 449 - <tr> 450 - <td> 268 + - `<my-laptop>` resolves to `den.aspects.my-laptop` 269 + - `<my-laptop/gaming>` resolves to `den.aspects.my-laptop.provides.gaming` 270 + - `<den/import-tree/host>` resolves to `den.provides.import-tree.provides.home` 271 + - `<vix/foo/bar>` namespace aware: `den.ful.vix.foo.provides.bar` 451 272 452 - ```nix 453 - # modules/routes.nix 454 - { den, ... }: 455 - let 456 - noop = _: { }; 273 + This feature is powered by a custom [`__findFile`](nix/den-brackets.nix) implementation. See the [profile example](templates/default/modules/_profile/den-brackets.nix) to learn how to enable it. 457 274 458 - by-platform-config = { host }: 459 - vix.${host.system} or noop; 460 - 461 - user-provides-host-config = { user, host }: 462 - vix.${user.aspect}._.${host.aspect} or noop; 463 - 464 - host-provides-user-config = { user, host }: 465 - vix.${host.aspect}._.${user.aspect} or noop; 466 - 467 - route = locator: { user, host }@ctx: 468 - (locator ctx) ctx; 469 - in 470 - { 471 - den.aspects.routes.__functor = 472 - den.lib.parametric true; 473 - den.aspects.routes.includes = 474 - map route [ 475 - user-provides-host-config 476 - host-provides-user-config 477 - by-platform-config 478 - ]; 479 - } 480 - ``` 275 + #### Parametric Routing 481 276 482 - </td> 483 - <td> 277 + You can use parametric aspects to create routing logic that dynamically includes other aspects based on context. This pattern allows you to build a flexible and declarative dependency tree. 484 278 485 279 ```nix 280 + # modules/routes.nix 281 + { den, pro, ... }: 282 + let 283 + # Route to a platform-specific profile 284 + by-platform = { host }: pro.${host.system} or { }; 285 + in 486 286 { 487 - # for all darwin hardware 488 - vix.aarch64-darwin = { host }: { 489 - darwin = ...; 490 - }; 491 - 492 - # config bound to vic user 493 - # on my-laptop host. 494 - vix.vic.provides.my-laptop = 495 - { host, user }: { 496 - nixos = ...; 497 - }; 287 + # Apply routes globally 288 + den.default.includes = [ by-platform ]; 498 289 } 499 290 ``` 500 291 501 - Use your imagination, come up with 502 - an awesome Dendritic setup that suits you. 503 - 504 - </td> 505 - </tr> 506 - </table> 507 - 508 - > You made it to the end!, thanks for reading to this point. 509 - > I hope you enjoy using `den` as much as I have done writing it and have put dedication 510 - > on it for being high quality-nix for you. \<3. 511 - > I feel like den is now feature complete, and it will not likely change. 292 + > You made it to the end! Thanks for reading. I hope you enjoy using `den`. It is feature-complete and unlikely to change. 512 293 513 294 </details> 514 295 515 - ### Contributing. 296 + ### Contributing 516 297 517 - Yes, please, anything!. From fixing my bad english and typos, to sharing your ideas and experience with `den` in our discussion forums. Feel free to participate and be nice with everyone. 298 + Contributions are welcome! Feel free to fix typos, improve documentation, or share ideas in our [discussions](https://github.com/vic/den/discussions). 518 299 519 - PRs are more than welcome, the CI runs some checks that verify nothing (known) is broken. Any new feature needs a test in `_example/ci.nix`. 300 + All PRs are checked against the CI. New features should include a test in `_example/ci.nix`. 520 301 521 - If you need to run the test suite locally: 302 + To run tests locally: 522 303 523 304 ```console 524 - $ nix flake check ./checkmate --override-input target . 525 - $ cd templates/default && nix flake check --override-input den ../.. 305 + nix flake check ./checkmate --override-input target . 306 + cd templates/default && nix flake check --override-input den ../.. 526 307 ```
+7 -2
checkmate/tests/aspect-functor.nix
··· 1 - { lib, inputs, ... }: 1 + { 2 + lib, 3 + inputs, 4 + config, 5 + ... 6 + }: 2 7 let 3 - __functor = (inputs.target.lib lib inputs).parametric true; 8 + __functor = (inputs.target.lib { inherit lib inputs config; }).parametric true; 4 9 5 10 aspect-example = { 6 11 inherit __functor;
+40
checkmate/tests/den-brackets.nix
··· 1 + { ... }@top: 2 + let 3 + lib = top.inputs.nixpkgs.lib; 4 + 5 + # deadnix: skip 6 + __findFile = 7 + if true then 8 + import "${top.inputs.target}/nix/den-brackets.nix" { inherit lib config inputs; } 9 + else 10 + __findFile; 11 + 12 + inputs = { 13 + 14 + }; 15 + 16 + config.den = { 17 + default.foo = 1; 18 + 19 + provides.foo.a = 2; 20 + provides.foo.provides.bar.b = 3; 21 + provides.foo.provides.c = 4; 22 + 23 + d = 5; 24 + 25 + aspects.foo.a = 6; 26 + aspects.foo.provides.bar.b = 7; 27 + aspects.foo.provides.c = 8; 28 + 29 + }; 30 + in 31 + { 32 + flake.tests."<den.default>" = 33 + let 34 + expr = <den.default>; 35 + expected.foo = 2; 36 + in 37 + { 38 + inherit expr expected; 39 + }; 40 + }
+7 -2
checkmate/tests/function_can_take.nix
··· 1 - { lib, inputs, ... }: 1 + { 2 + lib, 3 + inputs, 4 + config, 5 + ... 6 + }: 2 7 let 3 - takes = (inputs.target.lib lib inputs).canTake; 8 + takes = (inputs.target.lib { inherit inputs lib config; }).canTake; 4 9 5 10 flake.tests."test function with no named arguments can take anything" = { 6 11 expr = takes { } (x: x);
+2 -1
flake.nix
··· 1 1 { 2 - outputs = _inputs: { 2 + outputs = _: { 3 3 flakeModule = ./nix/flakeModule.nix; 4 4 templates.default = { 5 5 path = ./templates/default; 6 6 description = "Minimal nixos configuration"; 7 7 }; 8 8 packages = import ./nix/template-packages.nix; 9 + namespace = import ./nix/namespace.nix; 9 10 lib = import ./nix/lib.nix; 10 11 }; 11 12 }
+2 -1
modules/lib.nix
··· 1 1 { 2 2 lib, 3 3 inputs, 4 + config, 4 5 ... 5 6 }: 6 7 { 7 - config.den.lib = inputs.den.lib lib inputs; 8 + config.den.lib = inputs.den.lib { inherit inputs lib config; }; 8 9 options.den.lib = lib.mkOption { 9 10 internal = true; 10 11 visible = false;
+49
nix/den-brackets.nix
··· 1 + # __findFile implementation to resolve deep aspects. 2 + # inspired by https://fzakaria.com/2025/08/10/angle-brackets-in-a-nix-flake-world 3 + # 4 + # For user facing documentation, see: 5 + # See templates/default/_profile/den-brackets.nix 6 + # See templates/default/_profile/namespace.nix 7 + { 8 + lib, 9 + config, 10 + ... 11 + }: 12 + _nixPath: name: 13 + let 14 + 15 + findAspect = 16 + path: 17 + let 18 + head = lib.head path; 19 + tail = lib.tail path; 20 + 21 + notFound = "Aspect not found: ${lib.concatStringsSep "." path}"; 22 + 23 + headIsDen = head == "den"; 24 + readFromDen = lib.getAttrFromPath tail config.den; 25 + 26 + headIsAspect = builtins.hasAttr head config.den.aspects; 27 + readFromAspects = lib.getAttrFromPath path config.den.aspects; 28 + 29 + headIsDenful = lib.hasAttrByPath [ "ful" head ] config.den; 30 + readFromDenful = lib.getAttrFromPath tail config.den.ful; 31 + 32 + found = 33 + if headIsDen then 34 + readFromDen 35 + else if headIsAspect then 36 + readFromAspects 37 + else if headIsDenful then 38 + readFromDenful 39 + else 40 + throw notFound; 41 + in 42 + found; 43 + 44 + in 45 + lib.pipe name [ 46 + (lib.strings.replaceStrings [ "/" ] [ ".provides." ]) 47 + (lib.strings.splitString ".") 48 + (findAspect) 49 + ]
+8 -13
nix/lib.nix
··· 1 - lib: inputs: 1 + { 2 + inputs, 3 + lib, 4 + config, 5 + ... 6 + }: 2 7 let 3 8 isFn = f: (builtins.isFunction f) || (f ? __functor); 4 9 canTake = import ./fn-can-take.nix lib; ··· 55 60 56 61 aspects = inputs.flake-aspects.lib lib; 57 62 58 - # EXPERIMENTAL. __findFile to resolve deep aspects. 59 - # __findFile = angleBrackets den.aspects; 60 - # <foo/bar/baz> => den.aspects.foo.provides.bar.provides.baz 61 - # inspired by https://fzakaria.com/2025/08/10/angle-brackets-in-a-nix-flake-world 62 - angleBrackets = 63 - den-ns: _nixPath: name: 64 - lib.pipe name [ 65 - (lib.replaceString "/" ".provides.") 66 - (lib.splitString ".") 67 - (path: lib.getAttrFromPath path den-ns) 68 - ]; 63 + __findFile = import ./den-brackets.nix { inherit lib config; }; 69 64 70 65 den-lib = { 71 66 inherit 72 67 parametric 73 68 aspects 74 - angleBrackets 69 + __findFile 75 70 statics 76 71 owned 77 72 isFn
+29
nix/namespace.nix
··· 1 + name: input: 2 + { config, lib, ... }: 3 + let 4 + isLocal = !builtins.isAttrs input; 5 + isOutput = isLocal && input == true; 6 + 7 + aliasModule = lib.mkAliasOptionModule [ name ] [ "den" "ful" name ]; 8 + 9 + type = lib.types.attrsOf config.den.lib.aspects.types.providerType; 10 + 11 + source = if isLocal then { } else input.denful.${name}; 12 + output = 13 + if isOutput then 14 + { 15 + config.flake.denful.${name} = config.den.ful.${name}; 16 + options.flake.denful.${name} = lib.mkOption { inherit type; }; 17 + } 18 + else 19 + { }; 20 + in 21 + { 22 + imports = [ 23 + aliasModule 24 + output 25 + ]; 26 + config._module.args.${name} = config.den.ful.${name}; 27 + config.den.ful.${name} = source; 28 + options.den.ful.${name} = lib.mkOption { inherit type; }; 29 + }
+6 -9
templates/default/flake.lock
··· 22 22 }, 23 23 "den": { 24 24 "locked": { 25 - "lastModified": 1762483837, 26 - "narHash": "sha256-4rFMMbyZdzxPduljpUB2eEB4VHNLcF2pM2K6KxWK37k=", 27 - "owner": "vic", 28 - "repo": "den", 29 - "rev": "c1a0d345c4484faba4a146d60c746b12c5fce9e6", 30 - "type": "github" 25 + "lastModified": 1762501715, 26 + "narHash": "sha256-ynnkGe7wBqQLZM+R4mwh/N7nFn5mh870wRe6tyiZfQY=", 27 + "path": "/home/vic/hk/den", 28 + "type": "path" 31 29 }, 32 30 "original": { 33 - "owner": "vic", 34 - "repo": "den", 35 - "type": "github" 31 + "path": "/home/vic/hk/den", 32 + "type": "path" 36 33 } 37 34 }, 38 35 "flake-aspects": {
+1 -1
templates/default/flake.nix
··· 14 14 url = "github:nix-darwin/nix-darwin"; 15 15 }; 16 16 den = { 17 - url = "github:vic/den"; 17 + url = "path:/home/vic/hk/den"; 18 18 }; 19 19 flake-aspects = { 20 20 url = "github:vic/flake-aspects";
+9 -12
templates/default/modules/_example/import-non-dendritic.nix
··· 1 1 # configures class-automatic module auto imports for hosts/users/homes. 2 2 # See documentation at modules/aspects/provides/import-tree.nix 3 - { den, ... }: 4 - let 5 - # EXPERIMENTAL FEATURE: __findFile enables angle brackets 6 - # <import-tree/host> resolves to: den.provides.import-tree.provides.host 7 - # See lib.nix and https://fzakaria.com/2025/08/10/angle-brackets-in-a-nix-flake-world 8 - # deadnix: skip # this weird if is for my nixf-diagnose to skip unused __findFile. 9 - __findFile = if true then den.lib.angleBrackets den.provides else __findFile; 10 - in 3 + { 4 + # deadnix: skip # see _profile/den-brackets.nix 5 + __findFile ? __findFile, 6 + ... 7 + }: 11 8 { 12 9 13 10 # alice imports non-dendritic <class> modules from _non_dendritic/alice/_<class>/*.nix 14 - den.aspects.alice.includes = [ (<import-tree> ./_non_dendritic/alice) ]; 11 + den.aspects.alice.includes = [ (<den/import-tree> ./_non_dendritic/alice) ]; 15 12 16 13 # See the documentation at batteries/import-tree.nix 17 14 den.default.includes = [ 18 - (<import-tree/host> ./_non_dendritic/hosts) 19 - (<import-tree/user> ./_non_dendritic/users) 20 - (<import-tree/home> ./_non_dendritic/homes) 15 + (<den/import-tree/host> ./_non_dendritic/hosts) 16 + (<den/import-tree/user> ./_non_dendritic/users) 17 + (<den/import-tree/home> ./_non_dendritic/homes) 21 18 ]; 22 19 23 20 }
+36
templates/default/modules/_profile/den-brackets.nix
··· 1 + # This enables den's angle brackets opt-in feature. 2 + # Remove this file to opt-out. 3 + # 4 + # When den.lib.__findFile is in scope, you can do: 5 + # 6 + # <pro/foo/bar> and it will resolve to: 7 + # den.aspects.pro.provides.foo.provides.bar 8 + # 9 + # <pro/foo.includes> resolves to: 10 + # den.aspects.pro.provides.foo.includes 11 + # 12 + # <den/import-tree/home> resolves to: 13 + # den.provides.import-tree.provides.home 14 + # 15 + # <den.default> resolves to den.default 16 + # 17 + # When the vix remote namespace is enabled 18 + # <vix/foo> resolves to: den.ful.vix.provides.foo 19 + # 20 + # Usage: 21 + # 22 + # Bring `__findFile` into scope from module args: 23 + # 24 + # { __findFile, ... }: 25 + # den.default.includes = [ <den/home-manager> ]; 26 + # } 27 + # 28 + # IF you are using nixf-diagnose, it will complain 29 + # about __findFile not being used, trick it with: 30 + # 31 + # { __findFile ? __findFile, ... } 32 + # 33 + { den, ... }: 34 + { 35 + _module.args.__findFile = den.lib.__findFile; 36 + }
+8 -7
templates/default/modules/_profile/hosts/bones/common-user-env.nix
··· 4 4 # private aspects can be let-bindings 5 5 # more re-usable ones are better defined inside the `pro` namespace. 6 6 host-contrib-to-user = 7 - # { host, user }: # uncomment if needed 8 - { 9 - homeManager.programs.vim.enable = true; 10 - }; 7 + { fromHost, toUser }: 8 + if fromHost.name == "bones" || toUser.name == "fido" then 9 + { 10 + homeManager.programs.vim.enable = true; 11 + } 12 + else 13 + { }; 11 14 in 12 15 { 13 - den.aspects.bones.provides.user.includes = [ 14 - # add other aspects of yours that use host, user 15 - # to conditionally add behaviour. 16 + den.default.includes = [ 16 17 host-contrib-to-user 17 18 ]; 18 19 }
+62 -19
templates/default/modules/_profile/namespace.nix
··· 1 - # creates a `pro` aspect namespace. 1 + # This module creates an aspect namespace. 2 + # 3 + # Just add the following import: 4 + # 5 + # # define local namespace. enable flake output. 6 + # imports = [ (inputs.den.namespace "vix" true) ]; 7 + # 8 + # # you can use remote namespaces and they will merge 9 + # imports = [ (inputs.den.namespace "vix" inputs.dendrix) ]; 10 + # 11 + # Internally, a namespace is just a `provides` branch: 12 + # 13 + # # den.ful is the social-convention for namespaces. 14 + # den.ful.<name> 15 + # 16 + # Having an aspect namespace is not required but helps a lot 17 + # with organization and conventient access to your aspects. 18 + # 19 + # The following examples use the `vix` namespace, 20 + # inspired by github:vic/vix own namespace pattern. 21 + # 22 + # By using an aspect namespace you can: 23 + # 24 + # - Directly write to aspects in your namespace. 25 + # 26 + # { 27 + # vix.gaming.nixos = ...; 28 + # 29 + # # instead of: 30 + # # den.ful.vix.gaming.nixos = ...; 31 + # } 32 + # 33 + # - Directly read aspects from your namespace. 34 + # 35 + # # Access the namespace from module args 36 + # { vix, ... }: 37 + # { 38 + # den.default.includes = [ vix.security ]; 39 + # 40 + # # instead of: 41 + # # den.default.includes = [ den.ful.vix.security ]; 42 + # } 2 43 # 3 - # this is not required but helps you avoid writing 4 - # `den.aspects.profile.provides` all the time 5 - # and use your namespace instead. 6 - # This is inspired by vic's vix namespae. 44 + # - Share and re-use aspects between Dendritic flakes 7 45 # 8 - # User TODO: rename `pro` to something else on your project. 9 - # The namespace is accesible via the module args 10 - # and also writable as an option at module root. 46 + # # Aspects opt-in exposed as flake.denful.<name> 47 + # { imports = [( inputs.den.namespace "vix" true)] } 11 48 # 12 - { config, lib, ... }: 49 + # # Many flakes can expose to the same namespace and we 50 + # # can merge them, accessing aspects in a uniform way. 51 + # { imports = [( inputs.den.namespace "vix" inputs.dendrix )] } 52 + # 53 + # - Use angle-brackets to access deeply nested trees 54 + # 55 + # # Be sure to read _profile/den-brackets.nix 56 + # { __findFile, ... }: 57 + # den.aspects.my-laptop.includes = [ <vix/gaming/retro> ]; 58 + # } 59 + # 60 + # 61 + # You can of course choose to not have any of the above. 62 + # USER TODO: Remove this file for not using a namespace. 63 + # USER TODO: Replace `pro` and update other files using it. 64 + { inputs, ... }: 13 65 { 14 - # create a sub-tree of provided aspects. 15 - # the `profile` name is generic, use your own 16 - # as deep as you like, only that it ends in a provides tree. 17 - den.aspects.profile.provides = { }; 18 - # setup for write 19 - imports = [ (lib.mkAliasOptionModule [ "pro" ] [ "den" "aspects" "profile" "provides" ]) ]; 20 - # setup for read 21 - _module.args.pro = config.den.aspects.profile.provides; 22 - # optionally expose outside your flake. 23 - # flake.pro = config.den.aspects.profile.provides; 66 + imports = [ (inputs.den.namespace "pro" true) ]; 24 67 }
+9 -52
templates/default/modules/_profile/profiles.nix
··· 1 1 # Profiles are just aspects whose only job is to include other aspects 2 2 # based on the properties (context) of the host/user they are included in. 3 - { pro, ... }: 3 + { pro, den, ... }: 4 4 { 5 5 6 6 # install profiles as parametric aspects on all hosts/users 7 7 den.default.includes = [ 8 - pro.by-host 9 - pro.by-user 8 + pro.profiles 10 9 ]; 11 10 12 - # `by-host { host }` 13 - # 14 - # den automatically includes `den.aspects.${host.name}`, besides that 15 - # this profile adds the following aspects if they exist: 16 - # 17 - # - `den.aspects.profile._.${system}` eg, an aspect per host hardware platform. 18 - # 19 - # since the `host` type is a freeform (see types.nix) you can add 20 - # custom attributes to your hosts and use them to dispatch for 21 - # common aspects. eg, by host network, etc. 22 - # 23 - # Also, remember that aspects can form a tree structure by using their 24 - # `provides` attribute, not all aspects need to exist at same level. 25 - pro.by-host = 26 - { host, ... }: 27 - { 28 - includes = [ 29 - (pro.${host.system} or { }) 30 - ]; 31 - }; 32 - 33 - # `by-user { host, user }` 34 - # 35 - # den automatically includes `den.aspects.${user.name}`. 36 - # a user can contribute modules to the host is part of, and also 37 - # define its own home-level configs. 38 - # 39 - # - `den.aspects.<host>._.user { host, user }`: included on each user of a host. 40 - # - `den.aspects.<user>._.host { host, user }`: included on each host where a user exists. 41 - # 42 - # this profile adds the following aspects if they exist: 43 - # 44 - # - `den.aspects.profile._.single-user-is-admin { host, user }` 45 - # 46 - # Since both host and user types are freeforms, you might add custom attributes 47 - # to them and your parametric aspects can use those attributes to conditionally add 48 - # features into the host or user level. 49 - pro.by-user = 50 - { host, user }: 51 - { 52 - includes = 53 - let 54 - apply = f: f { inherit host user; }; 55 - in 56 - map apply [ 57 - pro.single-user-is-admin 58 - # TODO: add 59 - ]; 60 - }; 11 + pro.profiles = { 12 + __functor = den.lib.parametric true; 13 + includes = [ 14 + ({ host }: pro.${host.system} or { }) 15 + # add other routes according to context. 16 + ]; 17 + }; 61 18 62 19 }
+5 -5
templates/default/modules/_profile/profiles/single-user-is-admin.nix
··· 3 3 4 4 # When a host includes *ONLY* one user, make that user the admin. 5 5 pro.single-user-is-admin = 6 - { host, user }: 6 + { fromUser, toHost }@context: 7 7 let 8 - one-user = 1 == builtins.length (builtins.attrValues host.users); 9 - is-admin = one-user && builtins.hasAttr user.name host.users; 10 - admin = lib.optionals is-admin [ den._.primary-user ]; 8 + single = 1 == builtins.length (builtins.attrValues toHost.users); 9 + exists = single && builtins.hasAttr fromUser.name toHost.users; 10 + admin = lib.optionals exists [ den._.primary-user ]; 11 11 define = [ den._.define-user ]; 12 12 in 13 13 { 14 - includes = map (f: f { inherit user host; }) (define ++ admin); 14 + includes = map (f: f context) (define ++ admin); 15 15 }; 16 16 }
+9 -13
templates/default/modules/_profile/users/fido/common-host-env.nix
··· 1 1 # An aspect that contributes to any operating system where fido is a user. 2 - { ... }: 2 + # hooks itself into any host. 3 + { pro, ... }: 3 4 let 4 - # private aspects can be in variables 5 - # more re-usable ones are better defined inside the `pro` namespace. 6 - user-contrib-to-host = 7 - # { user, host }: # uncomment if needed 8 - { 9 - nixos = { }; 10 - darwin = { }; 11 - }; 5 + fido-at-host = 6 + { fromUser, toHost }: if fromUser.name != "fido" then { } else pro.fido._.${toHost.name}; 12 7 in 13 8 { 14 - den.aspects.fido.provides.host.includes = [ 15 - # add other aspects of yours that use host, user 16 - # to conditionally add behaviour. 17 - user-contrib-to-host 9 + den.default.includes = [ 10 + fido-at-host 18 11 ]; 12 + 13 + # fido on bones host. 14 + pro.fido._.bones.nixos = { }; 19 15 }
+1 -1
templates/default/modules/den.nix
··· 8 8 (inputs.import-tree ./_example) 9 9 10 10 # The _profile directory contains a minimal profile-based layout. 11 - # (inputs.import-tree ./_profile) # Disabled, working on #45 11 + (inputs.import-tree ./_profile) 12 12 ]; 13 13 }