Hello,
While writing a custom NixOS module I faced the infinite recursion encountered error.
The module provides a configuration for multiple systemd units. It is split into 3 parts, I naively thought it was simpler to maintain like that : the module definition in default.nix, the options of one unit in options.nix and the configuration of one unit in module.nix.
I expected the module to work this way:
- Read the system configuration from
config.services.custom-service, - Evict disabled units,
- Generate an attr. set for each unit, merge them together and assign the final attr. set to the module
configattribute.
See the nix code describing this module:
Custom service
# /etc/nixos/services.nix
{ config, lib, pkgs, ... }:
{
imports = [
/etc/nixos/modules/custom-service/default.nix
];
services.custom-service."foo" = {
enable = true;
content = "hello world";
};
}
# /etc/nixos/modules/custom-service/default.nix
{
lib,
config,
}:
let
inherit (lib)
filterAttrs
mapAttrsToList
mergeAttrsList
mkOption
types
;
mkModule = import ./module.nix;
enabled = filterAttrs (_: cfg: cfg.enable) config.services.custom-service;
result = mergeAttrsList (mapAttrsToList (name: cfg: mkModule { name = name; cfg = cfg; }) enabled);
in
{
options.services.custom-service = mkOption {
default = {};
type = types.attrsOf (types.submodule (import ./options.nix { inherit lib; }));
};
config = result;
}
# /etc/nixos/modules/custom-service/options.nix
{ lib }:
let
inherit (lib)
mkEnableOption
mkOption
types
;
in
{
options = {
enable = mkEnableOption "";
content = mkOption {
type = types.str;
};
};
}
# /etc/nixos/modules/custom-service/module.nix
{
name,
cfg,
}:
{
systemd.services."custom-service-${name}" = {
description = "Custom service ${name}";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
};
script = ''
cat <<EOF
Service: ${name}.
Content:
${cfg.content}
EOF
'';
};
systemd.timers."custom-service-${name}" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnUnitActiveSec = "30s";
};
};
}
While nixos-rebuild dry-build fails to build this NixOS configuration, I can produce the attr. set of the module in nix repl:
nix-repl> lib = import <nixpkgs/lib>
nix-repl> config = { services = { custom-service = { foo = { enable = true; content = "hello world"; }; }; }; }
nix-repl> module = import ./default.nix { lib = lib; config = config; }
nix-repl> :p module.config.systemd
{
services = {
custom-service-foo = {
description = "Custom service foo";
script = "cat <<EOF\n Service: foo.\n Content:\n hello world\nEOF\n";
serviceConfig = { DynamicUser = true; };
wantedBy = [ "multi-user.target" ];
};
};
timers = {
custom-service-foo = {
timerConfig = { OnUnitActiveSec = "30s"; };
wantedBy = [ "timers.target" ];
};
};
}
Alternatively I looked at different approaches and the most common Iâve seen in nixpkgs was to âmerge manuallyâ my content of module.nix in the top level module definition, Iâve adapted my original structure of the module and it now works. Iâm not sure this is super clear and I hope the following code will capture the idea.
Custom service without `module.nix`
{
lib,
config,
}:
let
inherit (lib)
filterAttrs
mapAttrs'
mkOption
types
;
enabled = filterAttrs (_: cfg: cfg.enable) config.services.custom-service;
in
{
options.services.custom-service = mkOption {
default = {};
type = types.attrsOf (types.submodule (import ./options.nix { inherit lib; }));
};
config = {
systemd.services = mapAttrs' (name: cfg: {
name = "custom-service-${name}";
value = {
description = "Custom service ${name}";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
};
script = ''
cat <<EOF
Service: ${name}.
Content:
${cfg.content}
EOF
'';
};
}) enabled;
systemd.timers = mapAttrs' (name: cfg: {
name = "custom-service-${name}";
value = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnUnitActiveSec = "30s";
};
};
}) enabled;
};
}
From a programming perspective, I expect both solutions to produce the exact same configuration and it seems to be the case when tested with nix repl.
But the former approach fails against nixos-rebuild dry-build while the latter works.
Personally I prefer the former but Iâm fine to adapt. Still I would like to understand whatâs happening but my knowledge of NixOS is too limited to understand the issue, would anyone be able to explain it?
Also, Iâm interested if there exists a method to debug from the trace produced by nixos-rebuild.
Thank you.
PS: I originally asked the question on IRC and thought it would be better documented on discourse.