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

Compare changes

Choose any two refs to compare.

+164 -273
+1 -1
.github/workflows/test.yml
··· 44 - run: nix flake update den 45 - run: nix run .#write-flake 46 - run: nix flake metadata 47 - - run: nix flake check -L
··· 44 - run: nix flake update den 45 - run: nix run .#write-flake 46 - run: nix flake metadata 47 + - run: nix flake check
+1 -1
README.md
··· 68 **Real-world examples for inspiration** 69 70 - [`vic/vix`](https://github.com/vic/vix/tree/den) 71 - - [`belsanti.xyz/nixconfig`](https://tangled.org/belsanti.xyz/nixconfig/tree/den) 72 - [GitHub Search](https://github.com/search?q=vic%2Fden+language%3ANix&type=code). 73 74 **Available templates**
··· 68 **Real-world examples for inspiration** 69 70 - [`vic/vix`](https://github.com/vic/vix/tree/den) 71 + - [`belsanti.xyz/nixconfig`](https://tangled.org/belsanti.xyz/nixconfig) 72 - [GitHub Search](https://github.com/search?q=vic%2Fden+language%3ANix&type=code). 73 74 **Available templates**
+16 -10
modules/aspects/provides/inputs.nix
··· 1 { den, withSystem, ... }: 2 { 3 - den.provides.inputs' = den.lib.parametric.exactly { 4 description = '' 5 Provides the `flake-parts` `inputs'` (the flake's `inputs` with system pre-selected) 6 as a top-level module argument. ··· 30 ( 31 { OS, host }: 32 let 33 - unused = den.lib.take.unused OS; 34 in 35 withSystem host.system ( 36 { inputs', ... }: 37 - { 38 - ${host.class}._module.args.inputs' = unused inputs'; 39 } 40 ) 41 ) ··· 47 host, 48 }: 49 let 50 - unused = den.lib.take.unused [ 51 OS 52 HM 53 ]; 54 in 55 withSystem host.system ( 56 { inputs', ... }: 57 - { 58 - ${user.class}._module.args.inputs' = unused inputs'; 59 } 60 ) 61 ) 62 ( 63 { HM, home }: 64 let 65 - unused = den.lib.take.unused HM; 66 in 67 withSystem home.system ( 68 { inputs', ... }: 69 - { 70 - ${home.class}._module.args.inputs' = unused inputs'; 71 } 72 ) 73 )
··· 1 { den, withSystem, ... }: 2 + let 3 + inherit (den.lib) 4 + parametric 5 + take 6 + ; 7 + in 8 { 9 + den.provides.inputs' = parametric.exactly { 10 description = '' 11 Provides the `flake-parts` `inputs'` (the flake's `inputs` with system pre-selected) 12 as a top-level module argument. ··· 36 ( 37 { OS, host }: 38 let 39 + unused = take.unused OS; 40 in 41 withSystem host.system ( 42 { inputs', ... }: 43 + unused { 44 + ${host.class}._module.args.inputs' = inputs'; 45 } 46 ) 47 ) ··· 53 host, 54 }: 55 let 56 + unused = take.unused [ 57 OS 58 HM 59 ]; 60 in 61 withSystem host.system ( 62 { inputs', ... }: 63 + unused { 64 + ${user.class}._module.args.inputs' = inputs'; 65 } 66 ) 67 ) 68 ( 69 { HM, home }: 70 let 71 + unused = take.unused HM; 72 in 73 withSystem home.system ( 74 { inputs', ... }: 75 + unused { 76 + ${home.class}._module.args.inputs' = inputs'; 77 } 78 ) 79 )
+91
modules/aspects/provides/unfree/unfree-predicate-builder.nix
···
··· 1 + { den, lib, ... }: 2 + let 3 + inherit (den.lib) 4 + parametric 5 + take 6 + ; 7 + 8 + description = '' 9 + This is a private aspect always included in den.default. 10 + 11 + It adds a module option that gathers all packages defined 12 + in den._.unfree usages and declares a 13 + nixpkgs.config.allowUnfreePredicate for each class. 14 + 15 + ''; 16 + 17 + unfreeComposableModule.options.unfree = { 18 + packages = lib.mkOption { 19 + type = lib.types.listOf lib.types.str; 20 + default = [ ]; 21 + }; 22 + }; 23 + 24 + nixosAspect = 25 + { config, ... }: 26 + { 27 + nixpkgs.config.allowUnfreePredicate = (pkg: builtins.elem (lib.getName pkg) config.unfree.packages); 28 + }; 29 + 30 + homeManagerAspect = 31 + { config, osConfig, ... }: 32 + { 33 + nixpkgs = lib.mkIf (!osConfig.home-manager.useGlobalPkgs) { 34 + config.allowUnfreePredicate = (pkg: builtins.elem (lib.getName pkg) config.unfree.packages); 35 + }; 36 + }; 37 + 38 + aspect = parametric.exactly { 39 + inherit description; 40 + includes = [ 41 + ( 42 + { OS, host }: 43 + let 44 + unused = take.unused OS; 45 + in 46 + { 47 + ${host.class}.imports = unused [ 48 + unfreeComposableModule 49 + nixosAspect 50 + ]; 51 + } 52 + ) 53 + ( 54 + { 55 + OS, 56 + HM, 57 + user, 58 + host, 59 + }: 60 + let 61 + unused = take.unused [ 62 + OS 63 + HM 64 + host 65 + ]; 66 + in 67 + { 68 + ${user.class}.imports = unused [ 69 + unfreeComposableModule 70 + homeManagerAspect 71 + ]; 72 + } 73 + ) 74 + ( 75 + { HM, home }: 76 + let 77 + unused = take.unused HM; 78 + in 79 + { 80 + ${home.class}.imports = unused [ 81 + unfreeComposableModule 82 + nixosAspect 83 + ]; 84 + } 85 + ) 86 + ]; 87 + }; 88 + in 89 + { 90 + den.default.includes = [ aspect ]; 91 + }
+21
modules/aspects/provides/unfree/unfree.nix
···
··· 1 + { den, ... }: 2 + { 3 + den.provides.unfree.description = '' 4 + A class generic aspect that enables unfree packages by name. 5 + 6 + Works for any class (nixos/darwin/homeManager,etc) on any host/user/home context. 7 + 8 + ## Usage 9 + 10 + den.aspects.my-laptop.includes = [ (den._.unfree [ "code" ]) ]; 11 + 12 + It will dynamically provide a module for each class when accessed. 13 + ''; 14 + 15 + den.provides.unfree.__functor = 16 + _self: allowed-names: 17 + { class, aspect-chain }: 18 + den.lib.take.unused aspect-chain { 19 + ${class}.unfree.packages = allowed-names; 20 + }; 21 + }
-21
modules/aspects/provides/unfree.nix
··· 1 - { lib, den, ... }: 2 - { 3 - den.provides.unfree.description = '' 4 - A class generic aspect that enables unfree packages by name. 5 - 6 - Works for any class (nixos/darwin/homeManager,etc) on any host/user/home context. 7 - 8 - ## Usage 9 - 10 - den.aspects.my-laptop.includes = [ (den._.unfree [ "code" ]) ]; 11 - 12 - It will dynamically provide a module for each class when accessed. 13 - ''; 14 - 15 - den.provides.unfree.__functor = 16 - _self: allowed-names: 17 - { class, aspect-chain }: 18 - den.lib.take.unused aspect-chain { 19 - ${class}.nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) allowed-names; 20 - }; 21 - }
···
+6 -50
nix/namespace.nix
··· 3 let 4 from = lib.flatten [ sources ]; 5 isOutput = builtins.any (x: x == true) from; 6 - attrs = builtins.filter builtins.isAttrs from; 7 - 8 - # Strip module system metadata to get clean raw values 9 - stripMeta = value: 10 - if builtins.isList value then 11 - map stripMeta value 12 - else if builtins.isAttrs value then 13 - let 14 - # Remove module system special attributes 15 - cleaned = builtins.removeAttrs value [ 16 - "__functor" 17 - "__functionArgs" 18 - "_module" 19 - "config" 20 - ]; 21 - in 22 - lib.mapAttrs (_: stripMeta) cleaned 23 - else 24 - value; 25 - 26 - # Deep merge that concatenates lists instead of overwriting them 27 - deepMergeWith = lhs: rhs: 28 - if builtins.isList lhs && builtins.isList rhs then 29 - lhs ++ rhs 30 - else if builtins.isAttrs lhs && builtins.isAttrs rhs then 31 - let 32 - allKeys = lib.unique (builtins.attrNames lhs ++ builtins.attrNames rhs); 33 - mergedAttrs = builtins.listToAttrs (map (name: { 34 - inherit name; 35 - value = 36 - if lhs ? ${name} && rhs ? ${name} then 37 - deepMergeWith lhs.${name} rhs.${name} 38 - else if lhs ? ${name} then 39 - lhs.${name} 40 - else 41 - rhs.${name}; 42 - }) allKeys); 43 - in 44 - mergedAttrs 45 - else 46 - rhs; 47 - 48 - # Extract denful values, strip metadata, and merge them deeply before passing to module system 49 - deepMerge = builtins.foldl' (acc: x: 50 - deepMergeWith acc (stripMeta (lib.getAttrFromPath [ "denful" name ] x)) 51 - ) { } attrs; 52 53 sourceModule = { 54 - config.den.ful.${name} = deepMerge; 55 }; 56 57 aliasModule = lib.mkAliasOptionModule [ name ] [ "den" "ful" name ]; ··· 59 outputModule = 60 if isOutput then 61 { 62 - # Use mkOptionDefault to ensure this assignment has lower priority 63 - # This prevents re-evaluation and duplication issues 64 - config.flake.denful.${name} = lib.mkOptionDefault config.den.ful.${name}; 65 } 66 else 67 { };
··· 3 let 4 from = lib.flatten [ sources ]; 5 isOutput = builtins.any (x: x == true) from; 6 + denfuls = map (lib.getAttrFromPath [ 7 + "denful" 8 + name 9 + ]) (builtins.filter builtins.isAttrs from); 10 11 sourceModule = { 12 + config.den.ful.${name} = lib.mkMerge denfuls; 13 }; 14 15 aliasModule = lib.mkAliasOptionModule [ name ] [ "den" "ful" name ]; ··· 17 outputModule = 18 if isOutput then 19 { 20 + config.flake.denful.${name} = config.den.ful.${name}; 21 } 22 else 23 { };
+4 -33
templates/examples/flake.lock
··· 22 }, 23 "den": { 24 "locked": { 25 - "lastModified": 1766081768, 26 - "narHash": "sha256-8Ea1DW3YZHifezfdEFHWEIpZBNKvEL+3iFOEcl3eFBU=", 27 "owner": "vic", 28 "repo": "den", 29 - "rev": "7271da18c60ab4d7c275ecaab480d29729f05d17", 30 "type": "github" 31 }, 32 "original": { ··· 251 "nixpkgs" 252 ], 253 "nixpkgs-stable": "nixpkgs-stable", 254 - "systems": "systems", 255 - "theirs": "theirs" 256 } 257 }, 258 "systems": { ··· 269 "repo": "default", 270 "type": "github" 271 } 272 - }, 273 - "theirs": { 274 - "inputs": { 275 - "den": [ 276 - "den" 277 - ], 278 - "flake-aspects": [ 279 - "flake-aspects" 280 - ], 281 - "flake-parts": [ 282 - "flake-parts" 283 - ], 284 - "import-tree": [ 285 - "import-tree" 286 - ], 287 - "nixpkgs": [ 288 - "nixpkgs" 289 - ] 290 - }, 291 - "locked": { 292 - "path": "./modules/_example/ci/_theirs", 293 - "type": "path" 294 - }, 295 - "original": { 296 - "path": "./modules/_example/ci/_theirs", 297 - "type": "path" 298 - }, 299 - "parent": [] 300 } 301 }, 302 "root": "root",
··· 22 }, 23 "den": { 24 "locked": { 25 + "lastModified": 1767645236, 26 + "narHash": "sha256-VMrWett3fWRb0hQh9K1JUC3zV2OlV4zOajFZHGr1GPw=", 27 "owner": "vic", 28 "repo": "den", 29 + "rev": "e7837daa0ea0f03fba62a2653ce16314a7d9d1c8", 30 "type": "github" 31 }, 32 "original": { ··· 251 "nixpkgs" 252 ], 253 "nixpkgs-stable": "nixpkgs-stable", 254 + "systems": "systems" 255 } 256 }, 257 "systems": { ··· 268 "repo": "default", 269 "type": "github" 270 } 271 } 272 }, 273 "root": "root",
-10
templates/examples/flake.nix
··· 43 nixpkgs-lib.follows = "nixpkgs"; 44 nixpkgs-stable.url = "github:nixos/nixpkgs/release-25.05"; 45 systems.url = "github:nix-systems/default"; 46 - theirs = { 47 - inputs = { 48 - den.follows = "den"; 49 - flake-aspects.follows = "flake-aspects"; 50 - flake-parts.follows = "flake-parts"; 51 - import-tree.follows = "import-tree"; 52 - nixpkgs.follows = "nixpkgs"; 53 - }; 54 - url = "path:./modules/_example/ci/_theirs"; 55 - }; 56 }; 57 58 }
··· 43 nixpkgs-lib.follows = "nixpkgs"; 44 nixpkgs-stable.url = "github:nixos/nixpkgs/release-25.05"; 45 systems.url = "github:nix-systems/default"; 46 }; 47 48 }
-96
templates/examples/modules/_example/ci/_theirs/flake.lock
··· 1 - { 2 - "nodes": { 3 - "den": { 4 - "locked": { 5 - "lastModified": 1766081768, 6 - "narHash": "sha256-8Ea1DW3YZHifezfdEFHWEIpZBNKvEL+3iFOEcl3eFBU=", 7 - "owner": "vic", 8 - "repo": "den", 9 - "rev": "7271da18c60ab4d7c275ecaab480d29729f05d17", 10 - "type": "github" 11 - }, 12 - "original": { 13 - "owner": "vic", 14 - "repo": "den", 15 - "type": "github" 16 - } 17 - }, 18 - "flake-aspects": { 19 - "locked": { 20 - "lastModified": 1766081176, 21 - "narHash": "sha256-JrsuNSIEXPS3AiIxuWZw+sJ2Td6ni1OkqbW6mO/F4Rs=", 22 - "owner": "vic", 23 - "repo": "flake-aspects", 24 - "rev": "d0a226c84be2900d307aa1896e4e2c6e451844b2", 25 - "type": "github" 26 - }, 27 - "original": { 28 - "owner": "vic", 29 - "repo": "flake-aspects", 30 - "type": "github" 31 - } 32 - }, 33 - "flake-parts": { 34 - "inputs": { 35 - "nixpkgs-lib": [ 36 - "nixpkgs" 37 - ] 38 - }, 39 - "locked": { 40 - "lastModified": 1762980239, 41 - "narHash": "sha256-8oNVE8TrD19ulHinjaqONf9QWCKK+w4url56cdStMpM=", 42 - "owner": "hercules-ci", 43 - "repo": "flake-parts", 44 - "rev": "52a2caecc898d0b46b2b905f058ccc5081f842da", 45 - "type": "github" 46 - }, 47 - "original": { 48 - "owner": "hercules-ci", 49 - "repo": "flake-parts", 50 - "type": "github" 51 - } 52 - }, 53 - "import-tree": { 54 - "locked": { 55 - "lastModified": 1763263999, 56 - "narHash": "sha256-AZ4UkBJQKfaL9sX+/mzc1xBtcJk8hDQGkhjWX0Py5hU=", 57 - "owner": "vic", 58 - "repo": "import-tree", 59 - "rev": "058bd03ac818ea349946323ae3c2837b4cab7f22", 60 - "type": "github" 61 - }, 62 - "original": { 63 - "owner": "vic", 64 - "repo": "import-tree", 65 - "type": "github" 66 - } 67 - }, 68 - "nixpkgs": { 69 - "locked": { 70 - "lastModified": 1763464769, 71 - "narHash": "sha256-AJHrsT7VoeQzErpBRlLJM1SODcaayp0joAoEA35yiwM=", 72 - "owner": "nixos", 73 - "repo": "nixpkgs", 74 - "rev": "6f374686605df381de8541c072038472a5ea2e2d", 75 - "type": "github" 76 - }, 77 - "original": { 78 - "owner": "nixos", 79 - "ref": "nixpkgs-unstable", 80 - "repo": "nixpkgs", 81 - "type": "github" 82 - } 83 - }, 84 - "root": { 85 - "inputs": { 86 - "den": "den", 87 - "flake-aspects": "flake-aspects", 88 - "flake-parts": "flake-parts", 89 - "import-tree": "import-tree", 90 - "nixpkgs": "nixpkgs" 91 - } 92 - } 93 - }, 94 - "root": "root", 95 - "version": 7 96 - }
···
-13
templates/examples/modules/_example/ci/_theirs/flake.nix
··· 1 - { 2 - outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); 3 - 4 - inputs = { 5 - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 - flake-parts.url = "github:hercules-ci/flake-parts"; 7 - flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 8 - 9 - import-tree.url = "github:vic/import-tree"; 10 - flake-aspects.url = "github:vic/flake-aspects"; 11 - den.url = "github:vic/den"; 12 - }; 13 - }
···
-21
templates/examples/modules/_example/ci/_theirs/modules/theirs.nix
··· 1 - # This flake is for testing by ci/namespace.nix 2 - { inputs, ... }: 3 - { 4 - systems = [ 5 - "x86_64-linux" 6 - "aarch64-darwin" 7 - ]; 8 - imports = [ 9 - inputs.den.flakeModule 10 - (inputs.den.namespace "sim" true) 11 - ]; 12 - 13 - sim.a._.b._.c._.d = { 14 - nixos.sims = [ "theirs abcd" ]; 15 - }; 16 - 17 - sim.ul._.a._.tion = { 18 - nixos.sims = [ "theirs simulation" ]; 19 - }; 20 - 21 - }
···
+1 -1
templates/examples/modules/_example/ci/base-conf-modules.nix
··· 9 den.base.host = 10 { host, ... }: 11 { 12 - options.capabilities.ssh-server = lib.mkEnableOption "Does host ${host.name} provides ssh?"; 13 }; 14 15 # This module is base for all user configs.
··· 9 den.base.host = 10 { host, ... }: 11 { 12 + options.capabilities.ssh-server = lib.mkEnableOption "Does host ${host.name} provide ssh?"; 13 }; 14 15 # This module is base for all user configs.
+1 -13
templates/examples/modules/_example/ci/namespace.nix
··· 17 # enable <angle/bracket> syntax for finding aspects. 18 _module.args.__findFile = den.lib.__findFile; 19 20 - # example "external" flake to import some aspects from. 21 - flake-file.inputs.theirs = { 22 - url = "path:./modules/_example/ci/_theirs"; 23 - inputs.nixpkgs.follows = "nixpkgs"; 24 - inputs.flake-parts.follows = "flake-parts"; 25 - inputs.import-tree.follows = "import-tree"; 26 - inputs.flake-aspects.follows = "flake-aspects"; 27 - inputs.den.follows = "den"; 28 - }; 29 - 30 imports = [ 31 # create a local namespace and output at flake.denful.eg 32 (inputs.den.namespace "eg" true) ··· 44 inputA 45 inputB 46 exposeToFlake 47 - inputs.theirs # from actual external flake 48 ] 49 ) 50 ]; ··· 82 "inputB simulation" 83 "local namespace" 84 "local simulation" 85 - "theirs simulation" 86 ]; 87 actual = lib.sort (a: b: a < b) rockhopper.config.sims; 88 in 89 - expected == (builtins.trace actual actual) 90 ); 91 92 };
··· 17 # enable <angle/bracket> syntax for finding aspects. 18 _module.args.__findFile = den.lib.__findFile; 19 20 imports = [ 21 # create a local namespace and output at flake.denful.eg 22 (inputs.den.namespace "eg" true) ··· 34 inputA 35 inputB 36 exposeToFlake 37 ] 38 ) 39 ]; ··· 71 "inputB simulation" 72 "local namespace" 73 "local simulation" 74 ]; 75 actual = lib.sort (a: b: a < b) rockhopper.config.sims; 76 in 77 + expected == actual 78 ); 79 80 };
+22 -3
templates/examples/modules/_example/ci/unfree.nix
··· 1 { den, ... }: 2 { 3 - # cam uses unfree vscode. 4 - den.aspects.cam.homeManager.programs.vscode.enable = true; 5 - den.aspects.cam.includes = [ (den._.unfree [ "vscode" ]) ]; 6 }
··· 1 { den, ... }: 2 + 3 + let 4 + codeAspect = { 5 + includes = [ (den._.unfree [ "vscode" ]) ]; 6 + homeManager.programs.vscode.enable = true; 7 + }; 8 + discordAspect = { 9 + includes = [ 10 + (den._.unfree [ "discord" ]) 11 + ]; 12 + homeManager = 13 + { pkgs, ... }: 14 + { 15 + home.packages = [ pkgs.discord ]; 16 + }; 17 + }; 18 + in 19 { 20 + # cam uses unfree vscode and discord loaded from different aspects. 21 + den.aspects.cam.includes = [ 22 + codeAspect 23 + discordAspect 24 + ]; 25 }