blog.anqou.net
rss
author
tags

NixOS module の infinite recursion encounteredlib.mkMergelib.mkIf で解決する

NixOS module を書いているときに、入力の config の値によって出力の config の値を変えるようなものを書くと infinite recursion encountered と言われてエラーになってしまいます。具体的には以下のような例で困ります:

{
  config,
  pkgs,
  lib,
  ...
}: {
  options = {
    hoge = { # hoge.foo というオプションを用意する。
      enable = lib.mkEnableOption "hoge enable";
      foo = lib.mkOption {
        type = lib.types.enum ["opt1" "opt2"];
        default = "value1";
      };
    };
  };
  config = let
    cfg = config.hoge;
    value = {
      opt1 = {
        environment.systemPackages = [pkgs.firefox];
      };
      opt2 = {
        environment.systemPackages = [pkgs.thunderbird];
      };
    };
  in
    # hoge.foo の値によって何を設定するか変える。
    lib.mkIf cfg.enable value."${cfg.foo}";
}

これを実行すると以下のような infinite recursion encountered のエラーが出ます:

error: infinite recursion encountered
at /nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/lib/modules.nix:257:21:
   256|                     options
   257|                     config
      |                     ^
   258|                     specialArgs

なぜ inifinite recursion になるのかは正直よく分からないのですが、色々変えながらテストしてみると、どうやら出力の config の一番外側の attrset を評価するときに入力の config を使うとダメなようです。逆に、attrset の内側で使うようにすればエラーにはなりません。例えば、今回の例では environment の値しか設定しないので、このフィールドを明示的に展開するとうまくいきます:

# ...

    lib.mkIf cfg.enable {
      # environment の value の部分でのみ config を使うと大丈夫。
      environment = value."${cfg.foo}".environment;
    };
}

ただこの方法だと、設定したい全てのフィールドについて展開する必要があり、フィールドの数が増えると面倒になってしまいます。

フィールドの数が増えてもスケールする方法として lib.mkMergelib.mkIf を組み合わせる方法があります。自分が試した限りではこの方法が一番良さそうです:

# ...

    lib.mkIf cfg.enable (lib.mkMerge [
      (lib.mkIf (cfg.foo == "opt1") value.opt1)
      (lib.mkIf (cfg.foo == "opt2") value.opt2)
    ]);
}

opt1opt2 に当たるものを二回書く必要があるのが微妙なのですが、とりあえずこれで運用してみます。もっと良い方法を知っている方は @[email protected] までぜひ教えてください。