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

improve documentation and code organization

+251 -89
+166 -18
README.md
··· 9 9 10 10 # `<aspect>.<class>` transposition for Dendritic Nix 11 11 12 + On [aspect oriented](https://vic.github.io/dendrix/Dendritic.html) [Dendritic](https://github.com/mightyiam/dendritic) setups, it is common to expose modules using `flake.modules.<class>.<aspect>`. 13 + However, for humans, it might be more intuitive to use a transposed attrset `<aspect>.<class>`. Because it feels more natural to nest classes inside aspects than the other way around. 14 + 15 + This project provides a [`transpose`](tree/main/default.nix) primitive, small and powerful enough to implement [cross-aspect dependencies](tree/main/aspects.nix) for *any* nix configuration class, and a [flake-parts module](./tree/main/flakeModule.nix) for turning `flake.aspects` into `flake.modules`. 16 + 12 17 <table> 13 18 <tr> 14 19 <td> ··· 16 21 17 22 ```nix 18 23 { 19 - foo = { 24 + vim-btw = { 20 25 nixos = ...; 26 + darwin = ...; 27 + homeManager = ...; 28 + nixvim = ...; 21 29 }; 22 - bar = { 30 + tiling-desktop = { 23 31 nixos = ...; 24 32 darwin = ...; 25 33 }; 26 - baz = { 34 + macos-develop = { 27 35 darwin = ...; 36 + hjem = ...; 28 37 }; 29 38 } 30 39 ``` ··· 39 48 ```nix 40 49 { 41 50 nixos = { 42 - foo = ...; 43 - bar = ...; 51 + vim-btw = ...; 52 + tiling-desktop = ...; 44 53 }; 45 54 darwin = { 46 - bar = ...; 47 - baz = ...; 55 + vim-btw = ...; 56 + tiling-desktop = ...; 57 + macos-develop = ...; 58 + }; 59 + homeManager = { 60 + vim-btw = ...; 61 + }; 62 + hjem = { 63 + macos-develop = ...; 64 + }; 65 + nixvim = { 66 + vim-btw = ...; 48 67 }; 49 68 } 50 69 ``` ··· 53 72 </tr> 54 73 </table> 55 74 56 - ## Motivation 75 + ## Usage 57 76 58 - On [Dendritic](https://github.com/mightyiam/dendritic) setups it is common to expose modules using `flake.modules.<class>.<aspect>` - see [aspect-oriented nix configurations](https://vic.github.io/dendrix/Dendritic.html). 77 + ### As a deps-free library from `./default.nix`: 59 78 60 - However, for humans, it might be more intuitive to use a transposed attrset `<aspect>.<class>`. Because it feels more natural to nest classes on aspects than the other way around. 61 - 62 - ## Usage 63 - 64 - As a deps-free library from `./default.nix`: 79 + Our [`transpose`](tree/main/default.nix) library takes an optional `emit` function that 80 + can be used to ignore some items, modify them or produce many other items on its place. 65 81 66 82 ```nix 67 83 let transpose = import ./default.nix { lib = pkgs.lib; }; in 68 84 transpose { a.b.c = 1; } # => { b.a.c = 1; } 69 85 ``` 70 86 71 - As a *Dendritic* flake-parts module that provides the `flake.aspects` option: 87 + This `emit` function is used by our [`aspects`](tree/main/aspects.nix) library 88 + (both libs are flakes-independent) to provide cross-aspects same-class module dependencies. 89 + 90 + ### As a *Dendritic* flake-parts module that provides the `flake.aspects` option: 72 91 73 92 > `flake.aspects` transposes into `flake.modules`. 74 93 75 94 ```nix 95 + # code in this example can (and should) be split into different dendritic modules. 76 96 { inputs, ... }: { 77 97 imports = [ inputs.flake-aspects.flakeModule ]; 78 - flake.aspects.sliding-desktop = { 79 - nixos = { ... }; # configure Niri 80 - darwin = { ... }; # configure Paneru 98 + flake.aspects = { 99 + 100 + sliding-desktop = { 101 + description = "nextgen tiling windowing"; 102 + nixos = { }; # configure Niri on Linux 103 + darwin = { }; # configure Paneru on MacOS 104 + }; 105 + 106 + 107 + awesome-cli = { 108 + description = "enhances environment with best of cli an tui"; 109 + nixos = { }; # os services 110 + darwin = { }; # apps like ghostty, iterm2 111 + homeManager = { }; # fish aliases, tuis, etc. 112 + nixvim = { }; # plugins 113 + }; 114 + 115 + work-network = { 116 + description = "work vpn and ssh access."; 117 + nixos = {}; # enable openssh 118 + darwin = {}; # enable MacOS ssh server 119 + terranix = {}; # provision vpn 120 + hjem = {}; # home link .ssh keys and configs. 121 + } 122 + 81 123 }; 124 + } 125 + ``` 126 + 127 + #### Declaring cross-aspect dependencies 128 + 129 + `flake.aspects` also allow to dependencies between aspects. 130 + 131 + Of course each module can have its own `imports`, however aspect requirements 132 + are aspect-level instead of module-level. Dependencies will ultimately resolve to 133 + modules and get imported only when they exist. 134 + 135 + In the following example, our `development-server` aspect can be applied into 136 + linux and macos hosts. 137 + Note that `alice` prefers to use `nixos`+`homeManager`, while `bob` likes `darwin`+`hjem`. 138 + 139 + The `development-server` is a "usability concern", that configures the exact same 140 + development tools on two different OS. 141 + When it is applied to a NixOS machine, the `alice.nixos` module will likely 142 + configure the alice user, but there is no nixos user for `bob`. 143 + 144 + ```nix 145 + { 146 + flake.aspects = {config, ...}: { 147 + development-server = { 148 + requires = with config; [ alice bob ]; 149 + 150 + # without flake-aspects, you'd normally do: 151 + # nixos.imports = [ inputs.self.modules.nixos.alice ]; 152 + # darwin.imports = [ inputs.self.modules.darwin.bob ]; 153 + }; 154 + 155 + alice = { 156 + nixos = {}; 157 + }; 158 + 159 + bob = { 160 + darwin = {}; 161 + }; 162 + }; 163 + } 164 + ``` 165 + 166 + It is out of scope for this library to create OS configurations. 167 + As you might have guessed, exposing configurations would look like this: 168 + 169 + ```nix 170 + { inputs, ... }: 171 + { 172 + flake.nixosConfigurations.fooHost = inputs.nixpkgs.lib.nixosSystem { 173 + system = "x86_64-linux"; 174 + modules = [ inputs.self.modules.nixos.development-server ]; 175 + }; 176 + 177 + flake.darwinConfigurations.fooHost = inputs.darwin.lib.darwinSystem { 178 + system = "aarm64-darwin"; 179 + modules = [ inputs.self.modules.darwin.development-server ]; 180 + }; 181 + } 182 + ``` 183 + 184 + #### Advanced aspect dependencies. 185 + 186 + You have already seen that an `aspect` can have a `requires` list: 187 + 188 + ```nix 189 + # A foo aspect that depends on aspects bar and baz. 190 + flake.aspects = { config, ... }: { 191 + foo.requires = [ config.bar config.baz ]; 192 + } 193 + ``` 194 + 195 + cross-aspect requirements work like this: 196 + 197 + When a module `flake.modules.nixos.foo` is requested (eg, included in a nixosConfiguration), 198 + a corresponding module will be computed from `flake.aspects.foo.nixos`. 199 + 200 + `flake.aspects.foo.requires` is a list of functions (named **providers**) 201 + that will be called with `{name = "foo"; class = "nixos"}` to obtain another aspect 202 + providing a module having the same `class` (`nixos` in our example). 203 + 204 + _providers_ are a way of asking: if I have a (`foo`, `nixos`) module what other 205 + aspects can you provide that have `nixos` modules to be imported in `foo`. 206 + 207 + > This way, it is aspects *being included* who decide what configuration must 208 + > be used by its caller aspect. 209 + 210 + by default, all aspects have a `<aspect>.provides.itself` function that ignores its argument 211 + and always returns the `<aspect>` itself. 212 + This is why you can use the `with config; [ bar baz ]` syntax. 213 + They are actually `[ config.bar.provides.itself config.baz.provides.itself ]`. 214 + 215 + but you can also define custom providers that can inspect the argument's `name` and `class` 216 + and return some another aspect accordingly. 217 + 218 + ```nix 219 + flake.aspects.alice.provides.os-user = { name, class, ... }: { 220 + # perhaps regexp matching on name or class. eg, match all "hosts" aspects. 221 + nixos = { }; 222 + } 223 + ``` 224 + 225 + the `os-user` provider can be now included in a `requires` list: 226 + 227 + ```nix 228 + flake.aspects = {config, ...}: { 229 + home-server.requires = [ config.alice.provides.os-user ]; 82 230 } 83 231 ``` 84 232
+24
aspects.nix
··· 1 + lib: aspects: 2 + let 3 + transpose = import ./. { inherit lib emit; }; 4 + emit = transposed: [ 5 + { 6 + inherit (transposed) parent child; 7 + value = aspectModule aspects.${transposed.child} transposed.parent; 8 + } 9 + ]; 10 + 11 + aspectModule = 12 + aspect: class: 13 + let 14 + require = f: aspectModule (f (aspect // { inherit class; })) class; 15 + module.imports = lib.flatten [ 16 + (aspect.${class} or { }) 17 + (lib.map require aspect.requires) 18 + ]; 19 + in 20 + module; 21 + in 22 + { 23 + transposed = transpose aspects; 24 + }
+4 -71
flakeModule.nix
··· 4 4 ... 5 5 }: 6 6 let 7 - aspects = config.flake.aspects; 8 - 9 - transpose = import ./. { inherit lib emit; }; 10 - emit = transposed: [ 11 - { 12 - inherit (transposed) parent child; 13 - value = aspectModule aspects.${transposed.child} transposed.parent; 14 - } 15 - ]; 16 - 17 - aspectModule = 18 - aspect: class: 19 - let 20 - require = f: aspectModule (f (aspect // { inherit class; })) class; 21 - module.imports = lib.flatten [ 22 - (aspect.${class} or { }) 23 - (lib.map require aspect.requires) 24 - ]; 25 - in 26 - module; 27 - 28 - providerType = lib.types.functionTo aspectSubmoduleType; 29 - 30 - aspectSubmoduleType = lib.types.submodule ( 31 - { name, config, ... }: 32 - { 33 - freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 34 - options.name = lib.mkOption { 35 - readOnly = true; 36 - description = "Aspect name"; 37 - default = name; 38 - type = lib.types.str; 39 - }; 40 - options.description = lib.mkOption { 41 - description = "Aspect description"; 42 - default = "Aspect ${name}"; 43 - type = lib.types.str; 44 - }; 45 - options.requires = lib.mkOption { 46 - description = "Providers to ask aspects from"; 47 - type = lib.types.listOf providerType; 48 - default = [ ]; 49 - }; 50 - options.provides = lib.mkOption { 51 - description = "Providers of aspect for other aspects"; 52 - default = { }; 53 - type = lib.types.submodule { 54 - freeformType = lib.types.lazyAttrsOf providerType; 55 - options.itself = lib.mkOption { 56 - readOnly = true; 57 - description = "Provides itself"; 58 - type = providerType; 59 - default = _: config; 60 - }; 61 - }; 62 - }; 63 - options.__functor = lib.mkOption { 64 - internal = true; 65 - readOnly = true; 66 - visible = false; 67 - description = "Functor to default provider"; 68 - type = lib.types.unspecified; 69 - default = _: config.provides.itself; 70 - }; 71 - } 72 - ); 73 - 7 + aspects = import ./aspects.nix lib config.flake.aspects; 8 + types = import ./types.nix lib; 74 9 in 75 10 { 76 11 options.flake.aspects = lib.mkOption { ··· 80 15 81 16 Convenience transposition of `flake.modules.<class>.<aspect>`. 82 17 ''; 83 - type = lib.types.submodule { 84 - freeformType = lib.types.lazyAttrsOf aspectSubmoduleType; 85 - }; 18 + type = types.aspectsType; 86 19 }; 87 - config.flake.modules = transpose aspects; 20 + config.flake.modules = aspects.transposed; 88 21 }
+57
types.nix
··· 1 + lib: 2 + let 3 + 4 + aspectsType = lib.types.submodule { 5 + freeformType = lib.types.lazyAttrsOf aspectSubmoduleType; 6 + }; 7 + 8 + providerType = lib.types.functionTo aspectSubmoduleType; 9 + 10 + aspectSubmoduleType = lib.types.submodule ( 11 + { name, config, ... }: 12 + { 13 + freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 14 + options.name = lib.mkOption { 15 + readOnly = true; 16 + description = "Aspect name"; 17 + default = name; 18 + type = lib.types.str; 19 + }; 20 + options.description = lib.mkOption { 21 + description = "Aspect description"; 22 + default = "Aspect ${name}"; 23 + type = lib.types.str; 24 + }; 25 + options.requires = lib.mkOption { 26 + description = "Providers to ask aspects from"; 27 + type = lib.types.listOf providerType; 28 + default = [ ]; 29 + }; 30 + options.provides = lib.mkOption { 31 + description = "Providers of aspect for other aspects"; 32 + default = { }; 33 + type = lib.types.submodule { 34 + freeformType = lib.types.lazyAttrsOf providerType; 35 + options.itself = lib.mkOption { 36 + readOnly = true; 37 + description = "Provides itself"; 38 + type = providerType; 39 + default = _: config; 40 + }; 41 + }; 42 + }; 43 + options.__functor = lib.mkOption { 44 + internal = true; 45 + readOnly = true; 46 + visible = false; 47 + description = "Functor to default provider"; 48 + type = lib.types.unspecified; 49 + default = _: config.provides.itself; 50 + }; 51 + } 52 + ); 53 + 54 + in 55 + { 56 + inherit aspectsType; 57 + }