···3434 _module.args.ci-os = "${{matrix.os}}";
3535 }
3636 EOF
3737- nix run .#write-flake
3737+ nix run .#write-flake --override-input den "github:$GITHUB_REPOSITORY/$GITHUB_SHA"
3838 nix flake update den
3939 nix run .#write-flake
4040 nix flake metadata
+21-19
README.md
···13131414<img width="400" height="400" alt="den" src="https://github.com/user-attachments/assets/af9c9bca-ab8b-4682-8678-31a70d510bbb" />
15151616-- focused on host/home definitions.
1717-- host/home configs via aspects.
1616+- focused on host/home [definitions](#basic-usage).
1717+- host/home configs via [aspects](#advanced-aspect-patterns).
1818- multi-platform, multi-tenant hosts.
1919- shareable-hm in os and standalone.
2020- extensible for new host/home classes.
2121-- stable/unstable input channels.
2121+- stable/unstable input [channels](#custom-factories-instantiate).
2222- customizable os/home factories.
2323+- [batteries](modules/aspects/batteries) included and replaceable.
2424+- features [tested](https://github.com/vic/den/actions) with [examples](templates/default/modules/_example).
23252426**❄️ Try it now! Launch our template VM:**
2527···163165164166This library also provides `default` aspects to apply global configurations to all hosts, users, or homes of a certain class.
165167166166-- `den.aspects.default.host`: Applied to all hosts.
167167-- `den.aspects.default.user`: Applied to all users within hosts.
168168-- `den.aspects.default.home`: Applied to all standalone homes.
168168+- `den.default.host`: Applied to all hosts.
169169+- `den.default.user`: Applied to all users within hosts.
170170+- `den.default.home`: Applied to all standalone homes.
169171170172## Advanced Customization
171173···262264263265You can define default settings that apply to all hosts, users, or homes. This is a powerful way to enforce global standards and reduce duplication.
264266265265-##### Class-Based Defaults (`den.aspects.default.<host|user|home>`)
267267+##### Class-Based Defaults (`den.default.<host|user|home>`)
266268267269You can apply settings to all systems of a specific *class* (e.g., `nixos`, `darwin`, `homeManager`) by adding them directly to the default aspect.
268270269271```nix
270272# modules/aspects.nix
271273{
272272- den.aspects.default = {
274274+ den.default = {
273275 host.nixos.system.stateVersion = "25.11";
274276 host.darwin.system.stateVersion = 6;
275277 user.homeManager.home.stateVersion = "25.11";
···278280}
279281```
280282281281-##### Parametric Defaults (`den.aspects.default.<host|user|home>.includes`)
283283+##### Parametric Defaults (`den.default.<host|user|home>.includes`)
282284283285For more dynamic configurations, you can add *functions* to the `includes` list of a default aspect. These functions are called for every host, user, or home, and receive the corresponding object (`host`, `user`, or `home`) as an argument. This allows you to generate configuration that is parameterized by the system's properties.
284286···290292{
291293 # 1. Define a parametric aspect (a function) that takes a host and returns
292294 # a configuration snippet.
295295+ # re-usable aspects use `den.aspects` and private ones let bindings.
293296 den.aspects.example.provides.hostName = { host }: { class, ... }: {
294297 ${class}.networking.hostName = host.hostName;
295298 };
296299297300 # 2. Include this function in the default host includes.
298301 # This function will now be called for every host defined in `den.hosts`.
299299- den.aspects.default.host.includes = [
302302+ den.default.host.includes = [
300303 den.aspects.example.provides.hostName
301304 ];
302305}
···304307305308###### How Parametric Defaults Work
306309307307-Under the hood, `aspects.default.host`, `aspects.default.user`, and `aspects.default.home` are not static aspects but **functors**. When `den` evaluates a system, it invokes the corresponding default functor, which in turn iterates over the functions in its `includes` list. It calls each function with a context-specific object and merges the resulting configuration snippets.
310310+Under the hood, `den.default.host`, `den.default.user`, and `den.default.home` are not static aspects but **functors**. When `den` evaluates a system, it invokes the corresponding default functor, which in turn iterates over the functions in its `includes` list. It calls each function with a context-specific object and merges the resulting configuration snippets.
308311309312The parameters passed to the functions in each `includes` list are as follows:
310313311311-- `den.aspects.default.host.includes`: Each function receives the `host` object (`{ host }`).
312312-- `den.aspects.default.user.includes`: Each function receives the `host` and `user` objects (`{ host, user }`). This applies to users defined within a host.
313313-- `den.aspects.default.home.includes`: Each function receives the `home` object (`{ home }`). This applies to standalone home-manager configurations.
314314+- `den.default.host.includes`: Each function receives the `host` object (`{ host }`).
315315+- `den.default.user.includes`: Each function receives the `host` and `user` objects (`{ host, user }`). This applies to users defined within a host.
316316+- `den.default.home.includes`: Each function receives the `home` object (`{ home }`). This applies to standalone home-manager configurations.
314317315318This mechanism allows you to create highly reusable and context-aware default configurations that adapt to each system's specific attributes.
316319···323326{
324327 den.aspects.example.provides.user = { user, host }:
325328 let
326326- # Default configuration for a user
327327- defaultConfig = {
329329+ aspect = {
328330 nixos.users.users.${user.userName}.isNormalUser = true;
329331 darwin.system.primaryUser = user.userName;
330332 };
331333332332- # Special configuration for NixOS-on-WSL
333333- hostSpecificConfig.adelie = {
334334+ # Special aspect for NixOS-on-WSL
335335+ per-host.adelie = {
334336 nixos.defaultUser = user.userName;
335337 };
336338 in
337339 # Use the host-specific config if it exists, otherwise use the default.
338338- hostSpecificConfig.${host.name} or defaultConfig;
340340+ per-host.${host.name} or aspect;
339341}
340342```
341343
···11+{
22+ inputs,
33+ lib,
44+ den,
55+ ...
66+}:
77+let
88+ home-manager.description = ''
99+ integrates home-manager into nixos/darwin OS classes.
1010+1111+ usage:
1212+1313+ for using home-manager in just a particular host:
1414+1515+ den.aspects.my-host.includes = [ (den.home-manager { host = den.hosts.<system>.my-host; }) ];
1616+1717+ for enabling home-manager by default on all hosts:
1818+1919+ den.default.host.includes = [ den.home-manager ];
2020+2121+ Does nothing for hosts that have no users with `homeManager` class.
2222+ Expects `inputs.home-manager` to exist. If `<host>.hm-input` exists
2323+ it is the name of the input to use instead of `home-manager`.
2424+2525+ For each user resolves den.aspects.''${user.aspect} and imports its homeManager class module.
2626+ '';
2727+2828+ home-manager.__functor =
2929+ _:
3030+ { host }:
3131+ { class, aspect-chain }:
3232+ let
3333+ hmUsers = builtins.filter (u: u.class == "homeManager") (lib.attrValues host.users);
3434+3535+ hmUserModule =
3636+ user:
3737+ den.aspects.${user.aspect}.resolve {
3838+ inherit aspect-chain;
3939+ class = "homeManager";
4040+ };
4141+4242+ users = map (user: {
4343+ name = user.userName;
4444+ value.imports = [ (hmUserModule user) ];
4545+ }) hmUsers;
4646+4747+ hmModule = inputs.${host.hm-input or "home-manager"}."${class}Modules".home-manager;
4848+ osPerUser =
4949+ user:
5050+ let
5151+ homeDir = if lib.hasSuffix "darwin" host.system then "/Users" else "/home";
5252+ in
5353+ {
5454+ users.users.${user.userName} = {
5555+ name = lib.mkDefault user.userName;
5656+ home = lib.mkDefault "${homeDir}/${user.userName}";
5757+ };
5858+ };
5959+6060+ aspect.${class} = {
6161+ imports = [ hmModule ] ++ (map osPerUser hmUsers);
6262+ home-manager.users = lib.listToAttrs users;
6363+ };
6464+6565+ supportedHmOS = builtins.elem class [
6666+ "nixos"
6767+ "darwin"
6868+ ];
6969+ enabled = supportedHmOS && builtins.length hmUsers > 0;
7070+ in
7171+ if enabled then aspect else { };
7272+7373+ aspect-option = import ../_aspect_option.nix { inherit inputs lib; };
7474+in
7575+{
7676+ config.den = { inherit home-manager; };
7777+ options.den.home-manager = aspect-option "home-managed OS";
7878+}
+62
modules/aspects/batteries/import-tree.nix
···11+{
22+ inputs,
33+ lib,
44+ den,
55+ ...
66+}:
77+let
88+ import-tree.description = ''
99+ an aspect that recursively imports non-dendritic .nix files from a `_''${class}` directory.
1010+1111+ this can be used to help migrating from huge existing setups,
1212+ by having files: path/_nixos/*.nix, path/_darwin/*.nix, etc.
1313+1414+ requirements:
1515+ - inputs.import-tree
1616+1717+ usage:
1818+1919+ this aspect can be included explicitly on any aspect:
2020+2121+ # example: my-host will import _nixos or _darwin nix files automatically.
2222+ den.aspects.my_host.includes = [ (den.import-tree ./.) ];
2323+2424+ or it can be default imported per host/user/home:
2525+2626+2727+ # each host will import-tree from ./hosts/''${host.name}/_{nixos,darwin}/*.nix
2828+ den.default.host.includes = [ (den.import-tree._.host ./hosts) ];
2929+3030+ # each user will import-tree from ./users/''${user.name}@''${host.name}/_homeManager/*.nix
3131+ den.default.user.includes = [ (den.import-tree._.user ./users) ];
3232+3333+ # each home will import-tree from ./homes/''${home.name}/_homeManager/*.nix
3434+ den.default.home.includes = [ (den.import-tree._.home ./homes) ];
3535+3636+ you are also free to create your own auto-imports layout following the implementation of these.
3737+ '';
3838+3939+ import-tree.__functor =
4040+ _: root:
4141+ { class, ... }:
4242+ let
4343+ path = "${toString root}/_${class}";
4444+ aspect.${class}.imports = [
4545+ (inputs.import-tree path)
4646+ ];
4747+ in
4848+ if builtins.pathExists path then aspect else { };
4949+5050+ import-tree.provides = {
5151+ host = root: { host }: import-tree "${toString root}/${host.name}";
5252+ user = root: { host, user }: import-tree "${toString root}/${user.name}@${host.name}";
5353+ home = root: { home }: import-tree "${toString root}/${home.name}";
5454+ };
5555+5656+ aspect-option = import ../_aspect_option.nix { inherit inputs lib; };
5757+5858+in
5959+{
6060+ config.den = { inherit import-tree; };
6161+ options.den.import-tree = aspect-option "import-tree aspects";
6262+}
+96
modules/aspects/defaults.nix
···11+{ inputs, lib, ... }:
22+let
33+44+ # set host static default values directly by class:
55+ #
66+ # den.aspects.default.host = {
77+ # nixos = ...;
88+ # darwin = ...;
99+ # }
1010+ #
1111+ # or register a function that takes the { host } param:
1212+ #
1313+ # den.aspects.default.host.includes = [ aspectByHost ];
1414+ # aspectByHost = { host }: { class, aspect-chain }: {
1515+ # nixos = ...;
1616+ # darwin = ...;
1717+ # }
1818+ default.host =
1919+ { aspect, ... }:
2020+ {
2121+ __functor =
2222+ _:
2323+ { host }:
2424+ { class, ... }:
2525+ {
2626+ name = "(default.host ${host.name})";
2727+ includes = map (f: f { inherit host; }) aspect.includes;
2828+ ${class} = aspect.${class} or { };
2929+ };
3030+ };
3131+3232+ # set user static values directly by class:
3333+ #
3434+ # den.aspects.default.user = {
3535+ # nixos = ...;
3636+ # darwin = ...;
3737+ # homeManager = ...;
3838+ # }
3939+ #
4040+ # or register a function that takes the { host, user } param:
4141+ #
4242+ # den.aspects.default.user.includes = [ aspectByUser ];
4343+ # aspectByUser = { host, user }: { class, aspect-chain }: {
4444+ # nixos = ...;
4545+ # darwin = ...;
4646+ # homeManager = ...;
4747+ # }
4848+ default.user =
4949+ { aspect, ... }:
5050+ {
5151+ __functor =
5252+ _:
5353+ { host, user }:
5454+ { class, ... }:
5555+ {
5656+ name = "(default.user ${host.name} ${user.name})";
5757+ includes = map (f: f { inherit host user; }) aspect.includes;
5858+ ${class} = aspect.${class} or { };
5959+ };
6060+ };
6161+6262+ # set home static values directly by class:
6363+ #
6464+ # den.aspects.default.home = {
6565+ # homeManager = ...;
6666+ # }
6767+ #
6868+ # or register a function that takes the { home } param:
6969+ #
7070+ # den.aspects.default.home.includes = [ aspectByHome ];
7171+ # aspectByHome = { home }: { class, aspect-chain }: {
7272+ # homeManager = ...;
7373+ # }
7474+ default.home =
7575+ { aspect, ... }:
7676+ {
7777+ __functor =
7878+ _:
7979+ { home }:
8080+ { class, ... }:
8181+ {
8282+ name = "(default.home ${home.name})";
8383+ includes = map (f: f { inherit home; }) aspect.includes;
8484+ ${class} = aspect.${class} or { };
8585+ };
8686+ };
8787+8888+ aspect-option = import ./_aspect_option.nix { inherit inputs lib; };
8989+9090+in
9191+{
9292+ config.den = { inherit default; };
9393+ options.den.default.host = aspect-option "host defaults";
9494+ options.den.default.user = aspect-option "host user defaults";
9595+ options.den.default.home = aspect-option "standalone home defaults";
9696+}
···11+# this is a non-dendritic darwin class module file.
22+# automatically discovered by `den.import-tree` as enabled in auto-imports.nix
33+{ ... }:
44+{
55+ # see nix-darwin options.
66+}
···11+# this is a non-dendritic nix class module file.
22+# automatically discovered by `den.import-tree` as enabled in auto-imports.nix
33+#
44+# suppose this file was auto-generated by nixos-generate-config or some other hardware tooling.
55+{
66+77+}
+86-78
templates/default/modules/_example/aspects.nix
···11# example aspect dependencies for our hosts
22# Feel free to remove it, adapt or split into modules.
33-{ inputs, lib, ... }:
33+# see also: defaults.nix, compat-imports.nix, home-managed.nix
44+{
55+ inputs,
66+ den,
77+ lib,
88+ ...
99+}:
410{
1111+ # see also defaults.nix where static settings are set.
1212+ den.default = {
1313+ # parametric defaults for host/user/home. see aspects/dependencies.nix
1414+ # `_` is shorthand alias for `provides`.
1515+ host.includes = [ den.aspects.example._.host ];
1616+ user.includes = [ den.aspects.example._.user ];
1717+ home.includes = [ den.aspects.example._.home ];
1818+ };
51966- den.aspects =
77- { aspects, ... }:
88- {
99- # rockhopper.nixos = { }; # config for rockhopper host
1010- # alice.homeManager = { }; # config for alice
1111- developer = {
1212- description = "aspect for bob's standalone home-manager";
1313- homeManager = { };
1414- };
2020+ # aspects for our example host/user/home definitions.
2121+ # on a real setup you will split these over into multiple dendritic files.
2222+ den.aspects = {
2323+ rockhopper.nixos = { }; # config for rockhopper host
2424+ # alice.homeManager = { }; # config for alice
15251616- # aspect for adelie host using github:nix-community/NixOS-WSL
1717- wsl.nixos = {
1818- imports = [ inputs.nixos-wsl.nixosModules.default ];
1919- wsl.enable = true;
2020- };
2121-2222- # default.{host,user,home} can be used for global settings.
2323- default.host.darwin.system.stateVersion = lib.mkDefault 6;
2424- default.host.nixos.system.stateVersion = "25.11";
2525- default.home.homeManager.home.stateVersion = lib.mkDefault "25.11";
2626+ developer = {
2727+ description = "aspect for bob's standalone home-manager";
2828+ homeManager = { };
2929+ };
26302727- # parametric host and user default configs. see aspects-config.nix
2828- default.host.includes = [ aspects.example.provides.host ];
2929- default.user.includes = [ aspects.example.provides.user ];
3030- default.home.includes = [ aspects.example.provides.home ];
3131+ # aspect for adelie host using github:nix-community/NixOS-WSL
3232+ wsl.nixos = {
3333+ imports = [ inputs.nixos-wsl.nixosModules.default ];
3434+ wsl.enable = true;
3535+ };
31363232- # aspect for each host that includes the user alice.
3333- alice.provides.hostUser =
3434- { user, ... }:
3535- {
3636- # administrator in all nixos hosts
3737- nixos.users.users.${user.userName} = {
3838- isNormalUser = true;
3939- extraGroups = [ "wheel" ];
4040- };
3737+ # aspect for each host that includes the user alice.
3838+ alice.provides.hostUser =
3939+ { user, ... }:
4040+ {
4141+ # administrator in all nixos hosts
4242+ nixos.users.users.${user.userName} = {
4343+ isNormalUser = true;
4444+ extraGroups = [ "wheel" ];
4145 };
4242-4343- # subtree of aspects for demo purposes.
4444- example.provides = {
4646+ };
45474646- # in our example, we allow all nixos hosts to be vm-bootable.
4747- vm-bootable = {
4848- nixos =
4949- { modulesPath, ... }:
5050- {
5151- imports = [ (modulesPath + "/installer/cd-dvd/installation-cd-minimal.nix") ];
5252- };
5353- };
4848+ # subtree of aspects for demo purposes.
4949+ example.provides = {
54505555- # parametric providers.
5656- host =
5757- { host }:
5858- { class, ... }:
5151+ # in our example, we allow all nixos hosts to be vm-bootable.
5252+ vm-bootable = {
5353+ nixos =
5454+ { modulesPath, ... }:
5955 {
6060- includes = [ aspects.example.provides.vm-bootable ];
6161- ${class}.networking.hostName = host.hostName;
5656+ imports = [ (modulesPath + "/installer/cd-dvd/installation-cd-minimal.nix") ];
6257 };
5858+ };
63596464- user =
6565- { user, host }:
6666- let
6767- aspect = {
6868- name = "(example.user ${host.name} ${user.name})";
6969- description = "user setup on different OS";
7070- darwin.system.primaryUser = user.userName;
7171- nixos.users.users.${user.userName}.isNormalUser = true;
7272- };
7373-7474- # adelie is nixos-on-wsl, has special user setup
7575- by-host.adelie = {
7676- nixos.defaultUser = user.userName;
7777- };
7878- in
7979- by-host.${host.name} or aspect;
6060+ # parametric providers.
6161+ host =
6262+ { host }:
6363+ { class, ... }:
6464+ {
6565+ # `_` is a shorthand alias for `provides`
6666+ includes = [ den.aspects.example._.vm-bootable ];
6767+ ${class}.networking.hostName = host.hostName;
6868+ };
80698181- home =
8282- { home }:
8383- { class, ... }:
8484- let
8585- path = if lib.hasSuffix "darwin" home.system then "/Users" else "/home";
8686- in
8787- {
8888- ${class}.home = {
8989- username = home.userName;
9090- homeDirectory = "${path}/${home.userName}";
9191- };
7070+ user =
7171+ { user, host }:
7272+ let
7373+ by-class.nixos.users.users.${user.userName}.isNormalUser = true;
7474+ by-class.darwin = {
7575+ system.primaryUser = user.userName;
7676+ users.users.${user.userName}.isNormalUser = true;
9277 };
93789494- };
7979+ # adelie is nixos-on-wsl, has special additional user setup
8080+ by-host.adelie.nixos.defaultUser = user.userName;
8181+ in
8282+ {
8383+ includes = [
8484+ by-class
8585+ (by-host.${host.name} or { })
8686+ ];
8787+ };
8888+8989+ home =
9090+ { home }:
9191+ { class, ... }:
9292+ let
9393+ homeDir = if lib.hasSuffix "darwin" home.system then "/Users" else "/home";
9494+ in
9595+ {
9696+ ${class}.home = {
9797+ username = lib.mkDefault home.userName;
9898+ homeDirectory = lib.mkDefault "${homeDir}/${home.userName}";
9999+ };
100100+ };
9510196102 };
103103+104104+ };
97105}