Idiomatic way to use one option to config both system and home-manager modules?

Context:

  • I have home-manager installed as a NixOS module
  • In order to have USB block devices auto-mounted, I need to both enable udiskie in home-manager and udisks2 in the general system configuration
  • I don’t want to have two separate options / modules to control the same functionality. More specifically, I want to have a single module with a single option that enables both.

What I have tried:

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

{

  imports = [
  ];

  options = {
    homeConf.udiskie.enable = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Enable udiskie module";
    };
  };

  osConfig = lib.mkIf config.homeConf.udiskie.enable {
    services.udisks2.enable = true;
  };

  config = lib.mkIf config.homeConf.udiskie.enable {
    services.udiskie.enable = true;
  };
}

The above fails and I found here on Discourse a good explanation of why.

However, it is not clear to me what would be the correct, idiomatic way to achieve what I want (I can obviously “survive” with two separate modules and options, but it feels brittle, confusing, and overall not a good practice.

The error message does contain a suggestion:

error: Module /nix/store/pn1mrnqzx9sznd49g99civq00va9jsn6-source/homeManagerModules/udiskie.nix' has an unsupported attribute osConfig’. This is caused by introducing a top-level config' or options’ attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: osConfig) into the explicit `config’ attribute.

but if I understand it correctly, it is not applicable in this case, osConfig not being an arbitrary config tree of my creation, but a link to the systemConfig that is exposed by home-manager.

Any help is welcome! :pray:

Not sure if this is idiomatic or what, but I have created options in a nixos module and an identical option in a home-manager module.

Then, from the nixos module, I use home-manager.sharedModules to apply the same option into home-manager. For example,

options.hdwlinux = {
    tags = lib.mkOption {
      description = "Tags used to enable components in the system.";
      type = lib.hdwlinux.types.allTags;
    };
}

config.home-manager.sharedModules = [
  {
    hdwlinux.tags = config.hdwlinux.tags;
  }
]

Hope that helps. Also, would love to know if there is a better way to do this.

I mean, you can just set home-manager options from your NixOS config:

config = lib.mkIf config.homeConf.udiskie.enable {
  services.udisks2.enable = true;
  home-manager.users.<user>.services.udiskie.enable = true;
};

Personally I would also skip creating that option, granularizing things to this extent is excessive. I had a time when I did things like this too, but have since found that it doesn’t really do anything for me while adding a load of boilerplate.

You can easily remove two lines of config if you want to turn it off, and if you want to condition something else on udisks being defined you can just use config.services.udisks2.enable. If you want to conditionally add it to a specific system config, you can skip a file containing these lines in imports instead of setting options.

But yeah, that comes down to style.


Personally I would keep my home-manager and NixOS config separate, and set in the home-manager modules:

# Don't forget to add the `nixos-config` to the module attributes
services.udiskie.enable = nixos-config.services.udisks2.enable;

Then I can separate the user-specific logic from what the system provides, and not think in individual functions but group stuff like “system utilities”, again to curtail the proliferation of super tiny, granular modules with hardly any business logic. But that is also just me.

Another option would be to use something like

config.home-manager.sharedModules = [ {
  services.udiskie.enable = lib.mkDefault config.services.udisks2.enable;
} ];

to make udiskie contingent on udisks2 for all users by default, while still keeping the option open to set services.udiskie.enable in any individual user’s HM config (without needing lib.mkForce).

And that way also means you don’t need to create a new option.

1 Like

Thank you for the extensive answer! :pray:

Correct me if I’m wrong, but none of the two alternatives that you presented are functionally equivalent to what my broken attempt was trying to do, correct? I was trying to have my HM configuration also set an option for the system. The two suggestions:

  • First → Use the system configuration to also set an option for HM
  • Second → Ensure that two different options are in sync (and the setting that dominates both is still defined in the system configuration (not in HM).

Also: I take that nixos-config is just the non-portable version of osConfig?

Oh, I certainly missed that you were trying to go the other way. Perhaps the question is why?

Home-manager is a per-user config, while the nixos config is machine wide. Perhaps you don’t have multiple users, so this isn’t a big deal. Also, if you ever need to use your home-manager config without being on nixos (macos, ubuntu), this will break. Perhaps you’ll never encounter that either, so also not a big deal. Regardless, you are fighting against the natural direction of flow and that’s always hard to overcome.

As waffle8946 said, you don’t need a custom option… that’s just what I did because it helps me stay more organized. You can just apply it directly through shared modules.

I’m new to NixOS, so I’m very happy to share my thinking and be given feedback, in case I’m misusing or misunderstanding anything.

For my NixOS config, I have a single repo with several hosts and several users.

The hosts are different in nature: a laptop, a desktop, a VM an home server, some with an external GPU, some not, etc… so they have different hardware requirement. For example: I only need a fingerprint reader on my laptop, while I don’t need automount of USB drives on the VM.

The users are also different in nature. My kids use gnome and steam. I use hyprland and freecad… and not all machines have all users.

So, I implemented my logic as follow:

  • things that have to do with the specifics of the machine are configured in the system configuration (which in my set-up is dependent on the host),
  • things that have to do with user-facing software and the behavior of the desktop environment are configured in home-manager (so they are dependent from the user).

In this case, the automount of USBs is a behaviour, so I am trying to put it in the HM config.

Does it make sense?

On the other account: I don’t use HM in standalone as I find the performance tradeoffs linked to FUSE not worth the effort (especially since I only use gnu/linux, so it’s not clear what I would stand to gain in using HM on an Arch or Fedora, rather than converting the reinstalling NixOS instead… but again: I am new around here, so feel free to correct my understanding if I’m not spotting the obvious! :see_no_evil: ).

EDIT: The issue with FUSE is there only if you have an impermanent setup, which I do.

I don’t use HM in standalone as I find the performance tradeoffs linked to fuse not worth the effort

I’m confused about the link between home-manager standalone and FUSE. Could you clarify?

I’m confused about the link between home-manager standalone and FUSE. Could you clarify?

Sorry, you are right: I omitted a key piece of information. I use impermanence. The relevant issue is this one.

Oh interesting. I’ve not bothered with impermanence, I guess I would have always assumed it did something with snapshots or ramfs. Live and learn :slight_smile:

I was trying to build on this suggestion…

config = lib.mkIf config.homeConf.udiskie.enable {
  services.udisks2.enable = true;
  home-manager.users.<user>.services.udiskie.enable = true;
};

…by using the first 2 lines. However my build fails with:

error: attribute 'homeConf' missing
at /nix/store/jgzzq0xr3sjhnlv6k4b2hwg79acfis5k-source/nixosModules/dependencies.nix:12:21:
    12|   config = lib.mkIf config.homeConf.udiskie.enable {
      |                     ^

I feel like the fix should be something obvious, but after more than an hour of frustration I’m stomped. Maybe a kind soul can give me a pointer? I suspect that maybe the lack of homeConf in the config hierarchy is due to the way I generate the configurations in the first place? Here is my code:

  outputs =
    { nixpkgs, ... }@inputs:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in
    {
      nixosConfigurations = nixpkgs.lib.foldl' (
        configs: hostname:
        configs
        // {
          "${hostname}" = nixpkgs.lib.nixosSystem {
            specialArgs = { inherit inputs hostname; };
            modules = [
              ./hosts/${hostname}/configuration.nix
              ./nixosModules
            ];
          };
        }
      ) { } (nixpkgs.lib.attrsets.mapAttrsToList (name: value: name) (builtins.readDir ./hosts));
      homeManagerModules.default = ./homeManagerModules;
    };

(the code that is not working is under ./nixosModules, while all the homeConf options are defined under ./homeManagerModules).

Yes, and this is intentional, I assumed you just picked that direction because you were trying random things. Going the other way is simply not the right abstraction; Your user should not be changing system settings.

System settings can have an impact on what your user wants to do.

Given the architecture involved, setting this up in the opposite direction is impossible anyway.

Apparently! My bad, this has only been a feature for 3 years, looks like I’m outdated. This explains what you were intending with your code.

Setting osConfig the way you do doesn’t work, it’s practically read-only.

Yes, but you’re not following your own abstraction in this case. The UI for mounting USBs is separate from the tooling to enable user-owned mounts. This is why the model breaks down.

Consistency of home environments across distros!

Ah, well, if that’s an option…

It does, or rather, it can. This is purely a limitation if you want to use it to manage your home directory, because in that case fundamentally you have to mount things as a user.

You could still use home-manager standalone on non-NixOS hosts and simply not configure impermanence for it. It’d behave no differently from the way you have it set up on NixOS currently; using impermanence for users on NixOS also uses FUSE aiui.

No, homeConf just isn’t an option. config only contains an attrset that reflects all the options in the system. To get the home configuration, you need to use config.home-manager.users.<user>.services.udiskie.enable.

Also note that this will only work if you set a specific user for <user>. All settings set in sharedModules will be set on all users, at least. But this is the issue with leaking user settings into your system config; you’re configuring your system around a single user, and multi-user setups become conceptually weird. It makes way more sense to configure your users around system settings.

3 Likes

Thank you for this, it was a very enlightening reply. :pray: :heart:

No, homeConf just isn’t an option. config only contains an attrset that reflects all the options in the system. To get the home configuration, you need to use config.home-manager.users.<user>.services.udiskie.enable.

I’m aware homeConf is not a default option of NixOS, it is just the container/prefix for all the options I set for my own HM modules. I did that because I expected all my options to belong to config (as in: conf.homeConf.udiskie.enable) and I wanted a common namespace for them. I have now learnt from you that the config for HM is already in its own namespace (home-manager.users.<user>, so I will refactor my code and remove homeConf which serves no purpose). :see_no_evil:

Yes, but you’re not following your own abstraction in this case. The UI for mounting USBs is separate from the tooling to enable user-owned mounts. This is why the model breaks down.

Hm. Am I not? After all I am keeping the activation of the tooling (udsiks2) in the configuration that has to do with the system, while I keep the activation of the functionality/behaviour (udiskie) in the configuration for HM. What I do is to have the enablement of the tooling contingent to the desired behaviour (IF the behaviour is need THEN install the tooling)…

In other words: I see udisks2 as a dependency of udiskie. And in fact, thanks to your input I was even able to have it working the way I wanted! :open_mouth: I created a file in the system configuration hierarchy called dependencies.nix and in there I put all the options enablements that may or may not be needed depending on the existence of a user requiring a given functionality. Like this:

{
  inputs,
  lib,
  config,
  pkgs,
  ...
}:
{
  config = lib.mkIf config.home-manager.users.mac.homeConf.udiskie.enable {
    services.udisks2.enable = true;
  };
}

(I still need to port other things like “gamemode if there is a steam user” or "cuda if a user does AI… but you get the gist).

How does this approach sounds to you? Complete anathema in a NixOS way of thinking, or just personal preference from my part?

I think the concept you describe is fine, the issue is implementation now. If you want “if any user has x enabled” you’ll need to express that by folding over that value in every user attrset, not just mac.

This may also come with a bit of a performance hit because of all the evaluation involved if you do that for each option, but it probably isn’t too bad.