Enable+config an unofficial service with Home Manager

Hello, Nix Community,

I’m currently in the process of modularizing my NixOS config, moving it to flakes, and finally using home-manager (as a module). Still pretty noob so, sorry in advance.

Goal: I want to enable a service (in my case, onedrive) that is not among builtin home-manager options, and I would like to configure it declaratively with home-manager (by setting home.file.".config/onedrive/sync_list".text = ''# some config''). Pretty standard I guess?

Problem: I would like all this (enable+config) to be in a single onedrive.nix file that I can import. But I’m having a fundamental doubt on how / whether this is possible. Take this module for onedrive located in /modules/onedrive.nix as an example:

{ user, pkgs, config, ... }:
let
  onePkg = pkgs.onedrive;
in
{
  config = {
    services.onedrive = {
      enable = true;
      package = onePkg;
    };
    systemd.services.onedrive.serviceConfig = {
      ExecStart = "${onePkg}/bin/onedrive --monitor --confdir ${config.users.users.${user}.home}/.config/onedrive";    
    };
    home.file.".config/onedrive/sync_list".text = ''
      # onedrive config file content
    '';
  };
}

which can be imported either as a Nix module or a HM module (sorry if the terms are not precise) from my current main flake.nix config:

# ...
nixosConfigurations = {
  "nixos" = nixpkgs.lib.nixosSystem {
      modules = [
        ./modules   # --> OPTION 1: default.nix imports onedrive.nix as a Nix module
        home-manager.nixosModules.home-manager {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
          home-manager.users.${user} = import ./modules/home.nix;    # --> OPTION 2: home.nix imports onedrive.nix as a HM module
        }
      ];
    };
};

My dilemma is this: If I import this module as a typical system module, it doesn’t recognize home. Conversely, importing it as a Home Manager module means it doesn’t recognize services since it’s expecting a home-manager.users.user.services.onedrive option, which doesn’t exist. I’m striving for a solution where a single module can be loaded to both enable the service and configure it, avoiding the need to separately import service and configuration modules.

Does anyone have insights or advice on achieving this? Is there a way to bridge or integrate these configurations more seamlessly, possibly by defining a system service and its user-level configuration within the same modular structure?

A less elegant solution would be to have some kind of mirror folder structure for modules and their configurations, but I would like to know what are the best practices in cases like this.

Any guidance or examples would be greatly appreciated. And forgive me for my noobness.
Thank you for your time and help!

1 Like

If you really want it to be a single file, you can turn your module into a module-producing function:

{ amHomeManagerModule }:
{ user, pkgs, config, ... }:
if amHomeManagerModule then
{
# home-manager module syntax
}
else 
{
# nixos module syntax
}

However:

  1. You would need to import the result of the import call, like so:

    modules = [
      (import ./module.nix { amHomeManagerModule = false; })
      # ...
      {
        home-manager.users.${user} = (import ./module.nix { amHomeManagerModule = true; })
      }
    ]
    
  2. user in your module’s arguments is (as far as I am aware) not a standard argument. You would need to pass it as specialArgs to make it available to NixOS module and as extraSpecialArgs to home-manager module if code inside both of the if paths depend on it. Since Nix is lazy, that might not be needed for both.

  3. config in case of a NixOS module is different from config for a Home Manager module. NixOS config is exposed to Home Manager modules (I believe, read-only) as osConfig. There could be more differences between similarly-named module arguments which might lead to a lot of headaches.

More common approach is to just have separate files for system configuration and for home-manager one and import them where appropriate. Since Nix merges modules when evaluating them – outcome would be the same as with the module-producing function above.

1 Like

Brilliant, thanks! I think I will resort to the more common approach then, but that is all very nice to know - thank you for your time.

Btw yes, user is not standard, but I removed specialArgs and extraSpecialArgs here for cleanliness. For the sake of completeness for the casual reader, since I know that Discourse is often the main Nix documentation for people…:

# main system flake.nix
{
inputs = {
    # nix
    nixpkgs = {
      url = "github:nixos/nixpkgs?ref=nixos-unstable";
    };
    # home manager
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

outputs = { self, nixpkgs, home-manager, ... }@inputs:
  let
    system = "x86_64-linux";
    user = "facc";
    pkgs = import nixpkgs { inherit system; };
  in
  {
nixosConfigurations = {
  "nixos" = nixpkgs.lib.nixosSystem {
      modules = [
        ./modules # nix modules
        home-manager.nixosModules.home-manager {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.${user} = import ./modules/home.nix; # hm modules
            home-manager.extraSpecialArgs = {
                inherit user system;    # <-- pass user to hm modules
            };
          }
      ];
      specialArgs = {
        inherit user system;    # <-- pass user to nix modules
      };
  };
};
};
}

Then the called modules will know the user and system arguments if you also set in header:

{ config, pkgs, user, system, ... }:    # <-- get user
{
  # ... use it as variable user, or "${user}" inside strings, for example:
  home.username = user;
  home.homeDirectory = "/home/${user}";
}