Coming From the Module System
If you have written NixOS modules, you already know most of gen’s module system. gen-merge
merges modules with the same grammar as nixpkgs’ lib.evalModules: options, imports,
priorities, mkIf, submodules. It needs no nixpkgs, and it records which file set every
value.
The mapping
Section titled “The mapping”| nixpkgs module system | gen |
|---|---|
lib.evalModules { modules; specialArgs; } | genMerge.evalModuleTree { specialArgs; } modules (options first, the module list last) |
lib.mkOption, lib.types.* | genMerge.mkOption, genMerge.types.* |
mkIf, mkMerge, mkDefault, mkForce, mkOverride, mkBefore, mkAfter, mkOrder | the same names on genMerge |
imports = [ … ] | imports = [ … ] inside one module tree. Across classes and hosts, an aspect’s includes. |
specialArgs, _module.args | specialArgs on evalModuleTree. To bind a named argument into one module function, gen-bind’s wrap. |
mkIf on a condition about the machine | mkIf still works. A condition over the graph, such as “every host in prod”, is a guard on an aspect or a rule. |
options.<o>.definitionsWithLocations | result.provenance.<o>: the winning files, their priority and every definition |
lib.types.submodule | genMerge.types.submodule |
lib.nixosSystem | a terminal, called by gen-delivery once per node |
| a value with a type check | a gen-types checker: types.int.verify 5 returns null or an error message |
One module tree, both ways
Section titled “One module tree, both ways”This tree uses options, imports, mkIf, mkDefault and a special argument:
let m = genMerge; # inputs.gen-merge.lib, or (inputs.gen.lib.mkGenLibs { }).merge t = m.types; result = m.evalModuleTree { specialArgs = { site = "us-east"; }; } [ # (1) { options.services.ssh.enable = m.mkOption { type = t.bool; default = false; }; options.services.ssh.port = m.mkOption { type = t.int; default = 22; }; options.networking.firewall.allowedTCPPorts = m.mkOption { type = t.listOf t.int; default = [ ]; }; options.motd = m.mkOption { type = t.str; }; } ({ config, ... }: { networking.firewall.allowedTCPPorts = m.mkIf config.services.ssh.enable [ config.services.ssh.port ]; # (2) }) { imports = [ { services.ssh.enable = true; } ]; } # (3) { services.ssh.port = m.mkDefault 2222; } ({ site, ... }: { motd = "welcome to ${site}"; }) ];inresult.configspecialArgsreach every module as function arguments, as in nixpkgs.mkIfbehaves as it does in nixpkgs.importspulls in another module.
This evaluates to:
{ motd = "welcome to us-east"; networking.firewall.allowedTCPPorts = [ 2222 ]; services.ssh = { enable = true; port = 2222; };}Written with lib.evalModules and lib.mkOption, the same tree gives the same config.
Where a value came from
Section titled “Where a value came from”Each option in the result carries its provenance:
m.evalModuleTree { } [ { options.services.ssh.port = m.mkOption { type = t.int; default = 22; }; } { _file = "/etc/gen/ssh.nix"; services.ssh.port = m.mkDefault 2222; }]# .provenance.services.ssh.port# ⇒ { winners = [ { file = "/etc/gen/ssh.nix"; } ]; priority = 1000; defs = [ … ]; … }winners names the file whose definition won, and priority is its priority. mkDefault
is priority 1000, the same number as in nixpkgs. See Inspection.
A definition of the wrong type fails with an error naming the option and both types:
error: gen-merge: a definition for option `port' is not of the expected type:expected type 'int' but value "eighty" is of type 'string'What differs
Section titled “What differs”-
No
libargument. Modules receiveconfig,optionsand yourspecialArgs. A module that readslibfails withmodule argument `lib' is not defined. Pass nixpkgs’libinspecialArgsif your modules need it. -
A smaller native type set.
int,float,str,bool,path,raw,anything,enum,nullOr,either,oneOf,listOf,attrsOf,lazyAttrsOf,submoduleanddeferredModuleare available.port,lines,package,strMatchingandfunctionToare not, and there is nomkEnableOption. WritemkOption { type = types.bool; default = false; }, or bring the nixpkgs type along as shown below. -
Argument injection by name. gen-bind’s
wrapbinds arguments such ashostinto a module function before the module system sees it:w = genBind.wrap { bindings = { host = { name = "web-1"; }; }; }({ host, ... }: { hostName = host.name; });# evalModuleTree with w.module sets hostName = "web-1" -
Nodes and edges. nixpkgs evaluates one machine at a time. In gen, hosts, users and features are nodes in one graph, so a module can ask which users belong to this host. From an Aspect to a NixOS System shows this.
Bringing nixpkgs types along
Section titled “Bringing nixpkgs types along”gen-merge also accepts nixpkgs’ own lib.mkOption, lib.types and lib.mkDefault. nixpkgs
option types use the same merge interface as gen-merge’s, so a module written for nixpkgs can
be evaluated by gen-merge while you port it:
genMerge.evalModuleTree { specialArgs = { inherit (inputs.nixpkgs) lib; }; } [ ({ lib, ... }: { options.services.ssh.port = lib.mkOption { type = lib.types.port; default = 22; }; options.services.ssh.banner = lib.mkOption { type = lib.types.lines; default = ""; }; }) ({ lib, ... }: { services.ssh.port = lib.mkDefault 2222; }) { services.ssh.banner = "authorised use only"; } ]# .config ⇒ { services.ssh = { banner = "authorised use only"; port = 2222; }; }The result is identical to lib.evalModules over the same modules. gen’s benchmark corpus,
run this way, produces byte-identical output. Leaf types such as port and lines add no
merge work of their own, because gen-merge does the merging. A nixpkgs submodule type runs
lib.evalModules for every instance, so it gives back the speed gen-merge gains on nested
records. Use this mode to carry custom nixpkgs types through a port. See
Benchmarks for the figures.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).