Custom Nix Classes
What is a Custom Class
Section titled “What is a Custom Class”Den’s built-in classes (nixos, darwin, homeManager) map to well-known
NixOS module systems. But you can define custom classes that forward their
contents into a target submodule path on another class.
This is how Den implements:
- The
userclass (forwards tousers.users.<name>on the OS) - Home Manager integration (forwards
homeManagertohome-manager.users.<name>) - hjem integration (forwards
hjemtohjem.users.<name>) - nix-maid integration (forwards
maidtousers.users.<name>.maid)
The forward Battery
Section titled “The forward Battery”den.provides.forward creates a new class by forwarding its module contents
into a target path on an existing class:
{ host }:den.provides.forward { each = lib.attrValues host.users; fromClass = user: "user"; intoClass = user: host.class; intoPath = user: [ "users" "users" user.userName ]; fromAspect = user: den.aspects.${user.aspect};}| Parameter | Description |
|---|---|
each = items | List of items to forward (typically [ user ] or [ true ]) |
fromClass = item: class | The custom class name to read from |
intoClass = item: class | The target class to write into |
intoPath = item: path or args: path | Target attribute path in the target class |
fromAspect = item: aspect | The aspect to read the custom class from |
Example: The Built-in user Class
Section titled “Example: The Built-in user Class”The user class (provides/os-user.nix)
forwards OS-level user settings to NixOS/nix-Darwin lightweight user-environment.
# Instead of:den.aspects.alice.nixos = { pkgs, ... } { users.users.alice = { packages = [ pkgs.hello ]; extraGroups = [ "wheel" ]; };};
# You write:den.aspects.alice.user = { pkgs, ... }: { packages = [ pkgs.hello ]; extraGroups = [ "wheel" ];};The user class is automatically forwarded to users.users.<userName> on
whatever OS class the host uses (NixOS or Darwin).
Creating Your Own Class
Section titled “Creating Your Own Class”Suppose you want a container class that forwards into
virtualisation.oci-containers.containers.<name>:
{ den, lib, ... }:let fwd = { host, user }: den.provides.forward { each = lib.singleton user; fromClass = _: "container"; intoClass = _: host.class; intoPath = _: [ "virtualisation" "oci-containers" "containers" user.userName ]; fromAspect = _: den.aspects.${user.aspect}; };in { den.ctx.user.includes = [ fwd ];}Now any user aspect can use the container class:
den.aspects.alice.container = { image = "nginx:latest"; ports = [ "8080:80" ];};Advanced: Guards and Adapters
Section titled “Advanced: Guards and Adapters”forward supports optional parameters for complex scenarios:
den.provides.forward { each = lib.singleton true; # <- true is the single item, mostly ignored. fromClass = _item: "wsl"; intoClass = _item: host.class; intoPath = _item: [ "wsl" ]; fromAspect = _item: lib.head aspect-chain;
# Only forward if target has wsl options guard = { options, ... }: options ? wsl;
# Modify module args for the forwarded module adaptArgs = args: { osConfig = args.config; };
# Custom module type for the forwarded submodule adapterModule = { config._module.freeformType = lib.types.anything; };}| Parameter | Description |
|---|---|
guard = args: bool or item: mkIf | Only forward when this predicate returns true |
adaptArgs = args: attrs | Transform module arguments before forwarding |
adapterModule = deferredModule | Custom module for the forwarded submodule type |
User contributed examples
Section titled “User contributed examples”Example: Alias a Class into the Target Root
Section titled “Example: Alias a Class into the Target Root”This pattern is useful when you want a class to behave like an alias for another class while keeping a separate name in your aspects.
hmAlias = { class, aspect-chain }: den._.forward { each = lib.singleton class; fromClass = _: "hm"; intoClass = _: "homeManager"; intoPath = _: [ ]; fromAspect = _: lib.head aspect-chain; adaptArgs = { config, ... }: { osConfig = config; }; };
den.aspects.tux = { includes = [ hmAlias ]; hm = { osConfig, ... }: { programs.fish.enable = true; home.keyboard.model = osConfig.networking.hostName; };};This forwards hm.* directly into homeManager.*. A more interesting use case is the following:
Example: Platform specific hm classes
Section titled “Example: Platform specific hm classes”This pattern is useful when you need HM to distinguish between different OS Platforms, because some packages only build in Darwin and not NixOS.
hmPlatforms = { class, aspect-chain }: den._.forward { each = [ "Linux" "Darwin" "Aarch64" "64bit" ]; fromClass = platform: "hm${platform}"; intoClass = _: "homeManager"; intoPath = _: [ ]; fromAspect = _: lib.head aspect-chain; guard = { pkgs, ... }: platform: lib.mkIf pkgs.stdenv."is${platform}"; adaptArgs = { config, ... }: { osConfig = config; }; };
den.aspects.tux = { includes = [ hmPlatforms ];
hmLinux = { pkgs, ... }: { home.packages = [ pkgs.wl-clipboard-rs ]; };
hmDarwin = { pkgs, ... }: { home.packages = [ pkgs.iterm2 ]; };};Example: Config across nixos and darwin classes.
Section titled “Example: Config across nixos and darwin classes.”The os forward class (provided by Den) can be useful for settings that must be forwarded to both on NixOS and MacOS.
Requested by @Risa-G at #222
# Note: this is already provided by Den at provides/os-class.nixos-class = { class, aspect-chain }: den._.forward { each = [ "nixos" "darwin" ]; fromClass = _: "os"; intoClass = lib.id; intoPath = _: [ ]; # top-level fromAspect = _: lib.head aspect-chain;};
# Note: already enabled by Den# den.ctx.host.includes = [ os-class ];
den.aspects.my-laptop = { os.networking.hostName = "Yavanna"; # on both NixOS and MacOS};Example: Role based configuration between users and hosts
Section titled “Example: Role based configuration between users and hosts”A dynamic class for matching roles between users and hosts.
roleClass = { host, user }: { class, aspect-chain }: den._.forward { each = lib.intersectLists (host.roles or []) (user.roles or []); fromClass = lib.id; intoClass = _: host.class; intoPath = _: [ ]; fromAspect = _: lib.head aspect-chain; };
den.ctx.user.includes = [ roleClass ];
den.hosts.x86_64-linux.igloo = { roles = [ "devops" "gaming" ]; users = { alice.roles = [ "gaming" ]; bob.roles = [ "devops" ]; };};
den.aspects.alice = { # enabled when host supports gaming role gaming = { pkgs, ... }: { programs.steam.enable = true; };
# enabled when host supports devops role devops = { pkgs, ... }: { virtualisation.podman.enable = true; };};Example: A git class that checks enable.
Section titled “Example: A git class that checks enable.”gitClass = { class, aspect-chain }: den._.forward { each = lib.singleton true; fromClass = _: "git"; intoClass = _: "homeManager"; intoPath = _: [ "programs" "git" ]; fromAspect = _: lib.head aspect-chain; guard = { config, ... }: _: lib.mkIf config.programs.git.enable; };
den.aspects.tux = { includes = [ gitClass ]; git.userEmail = "root@linux.com";};This will set at host: home-manager.users.tux.programs.git.userEmail
Example: A nix class that propagates settings to NixOS and HomeManager
Section titled “Example: A nix class that propagates settings to NixOS and HomeManager”This can be used when you don’t want NixOS and HomeManager to share the same pkgs but still configure both at the same time.
Contributed by @musjj
nixClass = { class, aspect-chain }: den._.forward { each = [ "nixos" "homeManager" ]; fromClass = _: "nix"; intoClass = lib.id; intoPath = _: [ "nix" "settings" ]; fromAspect = _: lib.head aspect-chain; adaptArgs = lib.id; };
# enable class for all users:den.ctx.user.includes = [ nixClass ];
# custom aspect that uses the `nix` class.nix-allowed = { user, ... }: { nix.allowed-users = [ user.userName ]; };
# included at users who can fix things with nix.den.aspects.tux.includes = [ nix-allowed ];Example: An impermanence class
Section titled “Example: An impermanence class”Suggested by @Doc-Steve
The following example, creates a forwarding class that is propagated only
when environment.persistance option is available in the host (the impermanence module was imported in host)
One cool thing about these custom classes is that aspects can simply define settings at the new class, without having to worry if the options they depend or some capability is enabled.
The froward-guard itself is reponsible checking in only one place, instead of having mkIf in a lot of places.
# Custom `persys` forwards config into nixos.environment.persistance."/nix/persist/system"# only if environment.persistance option is present.persys = { class, aspect-chain }: den._.forward { each = lib.singleton true; fromClass = _item: "persys"; intoClass = _item: class; intoPath = _item: { config, ... }: # access intoClass module arguments: [ "environment" "persistance" config.impermanence.persistence-dir ]; fromAspect = _item: lib.head aspect-chain; guard = { options, ... }@osArgs: options ? environment.persistance;};
den.hosts.my-laptop.includes = [ persys ];
# becomes nixos.environment.persistance."/nix/persist/system".hideMounts = true;den.aspects.my-laptop.persys.hideMounts = true;