Skip to content

includes, provides and meta

This page follows the structure of drupol/infra, one of the largest public Den configurations: ten hosts, one user on most of them, and about a hundred feature files. The same shapes appear in most Den configurations on GitHub. Each section shows the yen files first. A table at the end maps each Den form to its yen form.

First the schema. Den keeps data in a free-form meta and in aspect arguments. yen declares them as typed fields of a kind.

modules/fleet.nix
# Den's schema, declared. A user's `meta` attrs become typed fields; `facter ./facter.json` becomes a host field.
{
yen,
inputs,
...
}:
let
inherit (yen.lib) opt;
strs = yen.lib.listOf yen.lib.str;
in
{
yen.kinds.fleet = {
spawns.host = _: yen.host;
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.host = {
fields = {
system = opt yen.lib.str "x86_64-linux";
shares = opt strs [ ]; # (1) parameters of a parametric aspect
};
spawns.user = yen.lib.pointing "hosts" yen.user;
sinks.nixos.out =
{ host, nixos, ... }:
{
nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; };
};
};
yen.kinds.user.fields = {
hosts = yen.lib.listOf yen.kinds.host;
fullname = yen.lib.str; # (2)
authorizedKeys = opt strs [ ];
};
yen.fleet.main = { };
yen.flake = { out, ... }: out;
}

(1) is the shares field of a host: a list of strings, empty by default. A parametric Den aspect, such as mount-cifs "//nas/media", takes its argument from this field. (2) is the fullname field of a user. A user without it is an error.

Each file under modules/base/ writes part of the base aspect. yen reads all of them. A NixOS module from each file reaches every host, because of the at at (1).

modules/base/nix.nix
# One feature, many files: each file of modules/base/ adds to the same `base` aspect.
{ yen, ... }:
{
yen.aspects.base = {
at = yen.kinds.host; # (1) den.default.includes: every host
nixos.nix.settings.experimental-features = [
"nix-command"
"flakes"
];
};
}

In Den, a host file lists the features it includes. In yen a feature has no at of its own, and each host file adds the host to the at of the features it wants. at from several files is a union, so the list stays in the host file, as in Den. The first file declares the features. The tabs are two host files.

modules/features.nix
# Features without `at`: each host file says which ones it has.
{ lib, ... }:
{
yen.aspects.desktop.nixos.services.xserver.desktopManager.gnome.enable = true;
yen.aspects.openssh.nixos.services.openssh.enable = true;
# (den.aspects.mount-cifs "//nas/media"): the parameters are the host's `shares` field
yen.aspects.shares = {
# (1)
at = s: (s.shares or [ ]) != [ ];
nixos =
{ host, ... }:
{
fileSystems = lib.listToAttrs (
map (
share:
lib.nameValuePair "/mnt/${baseNameOf share}" {
device = share;
fsType = "cifs";
options = [ "noauto" ];
}
) host.shares
);
};
};
}
modules/hosts/x280.nix
# Den: provides.to-users.includes = with den.aspects; [ desktop openssh … ]. yen: the host joins each feature's `at`.
{ yen, ... }:
let
host = yen.host.x280;
in
{
yen.host.x280.shares = [ "//nas/media" ];
yen.aspects.desktop.at = [ host ];
yen.aspects.openssh.at = [ host ]; # (1)
yen.aspects.x280 = {
at = host;
nixos = {
boot.loader.systemd-boot.enable = true;
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
};
};
# den.aspects.pol.provides.x280: pol's content, only on this host
yen.aspects.pol-on-x280 = {
at = yen.lib.all [
# (2)
yen.user.pol
(yen.lib.within host)
];
nixos.users.users.pol.extraGroups = [ "dialout" ];
};
}

In features.nix, (1) is the aspect shares. Its at is true where the shares field is not empty, so it needs no host list. desktop and openssh have no at here.

In hosts/x280.nix, (1) adds x280 to the at of openssh. hosts/rpi4.nix does the same for rpi4, and the two lists join. (2) is the provides of Den: the aspect is active at the user pol only where pol is within the host x280.

Den users often keep a free-form meta attribute set and read it back with config.meta. In yen these are fields of the user kind, declared in fleet.nix above. A missing fullname is an error at the user, and every file can read the value.

modules/users/pol.nix
# Den: den.aspects.pol = { config, ... }: { meta.fullname = …; user.description = config.meta.fullname; }.
# yen: meta is fields of the instance. Any file reads them as `yen.user.pol.fullname`, or as the `user` arg.
{ yen, ... }:
{
yen.user.pol = {
hosts = [
yen.host.x280
yen.host.rpi4
];
fullname = "Pol";
authorizedKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPol" ];
};
# define-user + primary-user, for every user
yen.aspects.account = {
at = yen.kinds.user;
nixos =
{ user, ... }:
{
users.users.${user.name} = {
isNormalUser = true;
description = user.fullname;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = user.authorizedKeys;
};
};
};
}

Each row is a Den form and the yen form of it.

Den yen
den.aspects.base written in 26 files the same: yen.aspects.base in many files; nothing is lost, see One feature in many files
den.default.includes = [ base ] at = yen.kinds.host on the aspect
a host’s includes or provides.to-users.includes = [ desktop openssh … ] the host file adds the host to each feature’s at
(den.aspects.facter ./facter.json), (mount-cifs "//nas/media") a host field, and one aspect whose at tests the field
den.aspects.pol.provides.x280, host-aspects an aspect at all [ yen.user.pol (within yen.host.x280) ]; see Predicates
meta = { fullname; email; authorizedKeys; }, read with config.meta typed user fields, read as user.fullname or yen.user.pol.fullname
den.aspects.tools.provides.nix-trusted-user an aspect with its own name
hasAspect an aspect in a predicate: at = all [ yen.kinds.user (within yen.aspects.gnome) ]
ContributeCommunitySponsor