Baffled with infinite recursion building an attributeset to assign to home.file

I’m trying to convert a manually defined list of home.file=... declarations to one that can be dynamically generated from a list of ruby derivations.

What was working happily, though not dynamically, was this:

let
...
  mkRubyLoc = r: ".rubies/ruby-${r.version}";
  mkRubySrc = r: {
    source = r;
    target = mkRubyLoc r;
  };

  pkgs_ruby26 = import (builtins.fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/2cdd608fab0af07647da29634627a42852a8c97f.tar.gz";
    sha256 = "1szv364xr25yqlljrlclv8z2lm2n1qva56ad9vd02zcmn2pimdih";
  }) {};

  ruby_2_6 = pkgs_ruby26.ruby_2_6;
in
{
  ...
  home.file."${mkRubyLoc ruby_2_6}" = mkRubySrc ruby_2_6;
  home.file."${mkRubyLoc pkgs.ruby_2_7}" = mkRubySrc pkgs.ruby_2_7;
  home.file."${mkRubyLoc pkgs.ruby_3_0}" = mkRubySrc pkgs.ruby_3_0;
  home.file."${mkRubyLoc pkgs.ruby_3_1}" = mkRubySrc pkgs.ruby_3_1;
}

In trying to convert this concept to an optional module that provides a default list of ruby versions that can be overridden, I’m getting stuck with an infinite recursion. (See below).

Any help — much appreciated!

So the home-manager module that I’m trying to build which (if enabled) allows to generate the relevant list of ruby versions into ~/.rubies is as follows in its current incarnation and the error is captured here: https://pastebin.com/0LcuCrai:

{ pkgs, lib, config, ... }:

with lib;
with attrsets;

let
  cfg = config.programs.ruby;

  mkRubyAttrSet = dir: list:
    let
      mkName = r: "${dir}/ruby-${r.version}";
      mkSetsList = map (r: nameValuePair (mkName r) r) list;
    in
      listToAttrs (mkSetsList);
in
{
  options = {
    programs.ruby = {
      enable = mkEnableOption "A pseudo ruby-install module.";

      packages = mkOption {
        type = types.listOf types.package;
        default = with pkgs; [
          ruby_2_7
          ruby_3_0
          ruby_3_1
        ];
        example = [ pkgs.ruby_3_0 pkgs.ruby_3_1 ];
        description = "The set of ruby packages to appear in the user environment.";
      };

      path = mkOption {
        type = types.str;
        default = ".rubies";
        description = "The user directory to install the rubies into.";
      };
    };
  };

  config = mkIf cfg.enable {
    home = mapAttrs (name: r: {
      file."${name}" = {
        source = r;
        target = name;
      };
    }) (mkRubyAttrSet cfg.path cfg.packages);
  };
}

Perhaps there’s a much simpler syntax? Anyway, I’ve tried lots of variations (as posted on the #home-manager topic on discord). So if anyone is able to get me out of the wilderness on this one, that’d be awesome! Thanks!

Trying simpler syntax that’s closer to the original… still an infinite recursion :-/

{ pkgs, lib, config, ... }:

with lib;
with attrsets;

let
  cfg = config.programs.ruby;

  mkRubyLoc = r: ".rubies/ruby-${r.version}";
  mkRubySrc = r: {
    source = r;
    target = mkRubyLoc r;
  };

  mkRubyFile = r: {
    home.file."${mkRubyLoc r}" = mkRubySrc r;
  };

  mkRubies = list: map (r: mkRubyFile r) list;

in
{
  options = {
    programs.ruby = {
      enable = mkEnableOption "A pseudo ruby-install module.";

      packages = mkOption {
        type = types.listOf types.package;
        default = with pkgs; [
          ruby_2_7
          ruby_3_0
          ruby_3_1
        ];
        example = [ pkgs.ruby_3_0 pkgs.ruby_3_1 ];
        description = "The set of ruby packages to appear in the user environment.";
      };

      path = mkOption {
        type = types.str;
        default = ".rubies";
        description = "The user directory to install the rubies into.";
      };
    };
  };

  config = mkIf cfg.enable (mkMerge (mkRubies cfg.packages));
}

Ooh… @NobbZ — so perhaps there is something to your comment Discord

If you remove the default and example ? I am currently as surprised as you are

This works

{ pkgs, lib, config, ... }:

with lib;
with attrsets;

let
  cfg = config.programs.ruby;

  mkRubyLoc = r: ".rubies/ruby-${r.version}";
  mkRubySrc = r: {
    source = r;
    target = mkRubyLoc r;
  };

  mkRubyFile = r: {
    home.file."${mkRubyLoc r}" = mkRubySrc r;
  };

  mkRubies = list: map (r: mkRubyFile r) list;

  defaultRubies = with pkgs; [ ruby_2_7 ruby_3_0 ruby_3_1 ];

in
{
  options = {
    programs.ruby = {
      enable = mkEnableOption "A pseudo ruby-install module.";
    };
  };

  config = mkIf cfg.enable (mkMerge (mkRubies defaultRubies));
}

Well this is weird… infinite recursion using a default value. Help!

...
  options = {
    programs.ruby = {
      enable = mkEnableOption "A pseudo ruby-install module.";

      packages = mkOption {
        type = types.listOf types.package;
        default = defaultRubies;
        description = "The set of ruby packages to appear in the user environment.";
      };

      path = mkOption {
        type = types.str;
        default = ".rubies";
        description = "The user directory to install the rubies into.";
      };
    };
  };
...

@rycee is it possible this a bug in home-manager or am I missing something simple? Thanks!

{ pkgs, lib, config, ... }:

with lib;
with attrsets;

let
  cfg = config.programs.ruby;

  mkRubyLoc = dir: r: "${dir}/ruby-${r.version}";
  mkRubySrc = dir: r: {
    source = r;
    target = mkRubyLoc dir r;
  };

  mkRubyFile = dir: r: {
    home.file."${mkRubyLoc dir r}" = mkRubySrc dir r;
  };

  mkRubies = dir: list: map (r: mkRubyFile dir r) list;

  defaultRubies = with pkgs; [ ruby_2_7 ruby_3_0 ruby_3_1 ];

in
{
  options = {
    programs.ruby = {
      enable = mkEnableOption "A pseudo ruby-install module.";

      packages = mkOption {
        type = types.listOf types.package;
        default = defaultRubies;
        description = "The set of ruby packages to appear in the user environment.";
      };

      path = mkOption {
        type = types.str;
        default = ".rubies";
        description = "The user directory to install the rubies into.";
      };
    };
  };

  config = (mkMerge (mkRubies cfg.path cfg.packages));
}

So I thought I could avoid the infinite recursion by defining a default list of strings — the names of the packages to be installed. But, sadly this also fails with an infinite recursion :frowning: .

I was hoping that additional ones could be registered via overlaying the pkgs — which is less convenient than the original design of just overriding the optional list, but getting something working at this stage is preferable.

let
  cfg = config.programs.ruby;

  ...

  findRuby = rubyName:
    pkgs."${rubyName}";

  mkRubies = dir: list:
    map (rubyName: mkRubyFile dir (findRuby rubyName)) list;

in
{
  options = {
    programs.ruby = {
      ...
      list = mkOption {
        type = types.nonEmptyListOf types.str;
        default = [ "ruby_2_7" ... ];
        ...
      };
    };
  };

  config = mkIf cfg.enable (mkMerge (mkRubies cfg.path cfg.list));
}

Just to close this out, @cid-chan had helped me on discord 's home-manager topic.

So what is working for me is this…

config = mkIf cfg.enable {
    home.file = mapAttrs (name: r: {
      source = r;
      target = name;
    }) (mkRubyAttrSet cfg.path cfg.packages);