Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: standalone Home Manager configurations per host #54

Merged
merged 2 commits into from
Jan 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/folder-structure.md
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
* `checks/` for flake checks.
* `devshells/` for devshells.
* `hosts/` for machine configurations.
* `hosts/*/users/` for Home Manager configurations.
* `lib/` for Nix functions.
* `modules/` for NixOS and other modules.
* `packages/` for packages.
@@ -153,6 +154,51 @@ Flake outputs:

> Depending on the system type returned, the flake outputs will be the same as detailed for NixOS or Darwin above.
### `hosts/<hostname>/users/(<username>.nix|<username>/home-configuration.nix)`

Defines a configuration for a Home Manager user. Users can either be defined as a nix file or directory containing
a `home-configuration.nix` file.

clo4 marked this conversation as resolved.
Show resolved Hide resolved
Before using this mapping, add the `home-manager` input to your `flake.nix` file:

```nix
{
inputs = {
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
}
```

Additional values passed:

* `inputs` maps to the current flake inputs.
* `flake` maps to `inputs.self`.
* `perSystem`: contains the packages of all the inputs, filtered per system.
Eg: `perSystem.nixos-anywhere.default` is a shorthand for `inputs.nixos-anywhere.packages.<system>.default`.

> The simplest way to have a common/shared user configuration between multiple systems is to create a
> module at `modules/home/<name>.nix` ([docs](#modulestypenamenamenix)), and import that module
> from `inputs.self.homeModules.<name>` for each user that should inherit it. This pattern makes
> it easy to apply system-specific customizations on top of a shared, generic configuration.
#### NixOS and nix-darwin

If `home-manager` is an input to the flake, each host with any users defined will have the appropriate home-manager
module imported and each user created automatically.

The options `home-manager.useGlobalPkgs` and `home-manager.useUserPkgs` will default to true.

#### Standalone configurations

Users are also standalone Home Manager configurations. A user defined as `hosts/pc1/users/max.nix` can be
applied using the `home-manager` CLI as `.#max@pc1`. The output name can be elided entirely if the current username
and hostname match it, e.g. `home-manager switch --flake .` (note the lack of `#`).

Because the username is part of the path to the configuration, the `home.username` option will default to
this username. This can be overridden manually. Likewise, `home.homeDirectory` will be set by default based
on the username and operating system (`/Users/${username}` on macOS, `/home/${username}` on Linux).

### `lib/default.nix`

Loaded if it exists.
145 changes: 137 additions & 8 deletions lib/default.nix
Original file line number Diff line number Diff line change
@@ -148,31 +148,145 @@ let
_module.args.perSystem = systemArgs.${pkgs.system}.perSystem;
};

home-manager =
inputs.home-manager
or (throw ''home configurations require Home Manager. To fix this, add `inputs.home-manager.url = "github:nix-community/home-manager";` to your flake'');

# Sets up declared users without any user intervention, and sets the
# options that most people would set anyway. The module is only returned
# if home-manager is an input and the host has at least one user with a
# home manager configuration. With this module, most users will not need
# to manually configure Home Manager at all.
mkHomeUsersModule =
hostname: homeManagerModule:
let
module =
{ perSystem, ... }:
{
imports = [ homeManagerModule ];
home-manager.sharedModules = [ perSystemModule ];
home-manager.extraSpecialArgs = specialArgs;
home-manager.users = homesNested.${hostname};
home-manager.useGlobalPkgs = lib.mkDefault true;
home-manager.useUserPackages = lib.mkDefault true;
};
in
lib.optional (builtins.hasAttr hostname homesNested) module;

# Attribute set mapping hostname (defined in hosts/) to a set of home
# configurations (modules) for that host. If a host has no home
# configuration, it will be omitted from the set. Likewise, if the user
# directory does not contain a home-configuration.nix file, it will
# be silently omitted - not defining a configuration is not an error.
homesNested =
let
getEntryPath =
_username: userEntry:
if userEntry.type == "regular" then
userEntry.path
else if builtins.pathExists (userEntry.path + "/home-configuration.nix") then
userEntry.path + "/home-configuration.nix"
else
null;

# Returns an attrset mapping username to home configuration path. It may be empty
# if no users have a home configuration.
mkHostUsers =
userEntries:
let
hostUsers = lib.mapAttrs getEntryPath userEntries;
in
lib.filterAttrs (_name: value: value != null) hostUsers;

mkHosts =
hostEntries:
let
hostDirs = lib.filterAttrs (_: entry: entry.type == "directory") hostEntries;
hostToUsers = _hostname: entry: importDir (entry.path + "/users") mkHostUsers;
hosts = lib.mapAttrs hostToUsers hostDirs;
in
lib.filterAttrs (_hostname: users: users != { }) hosts;
in
importDir (src + "/hosts") mkHosts;

# Attrset of ${system}.homeConfigurations."${username}@${hostname}"
standaloneHomeConfigurations =
let
mkHomeConfiguration =
{
username,
modulePath,
pkgs,
}:
home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = specialArgs;
modules = [
perSystemModule
modulePath
{
home.username = lib.mkDefault username;
# Home Manager would use builtins.getEnv prior to 20.09, but
# this feature was removed to make it pure. However, since
# we know the operating system and username ahead of time,
# it's safe enough to automatically set a default for the home
# directory and let users customize it if they want. This is
# done automatically in the NixOS or nix-darwin modules too.
home.homeDirectory = lib.mkDefault (
if pkgs.stdenv.isDarwin then "/Users/${username}" else "/home/${username}"
);
}
];
};

homesFlat = lib.concatMapAttrs (
hostname: hostUserModules:
lib.mapAttrs' (username: modulePath: {
name = "${username}@${hostname}";
value = {
inherit hostname username modulePath;
};
}) hostUserModules
) homesNested;
in
eachSystem (
{ pkgs, ... }:
{
homeConfigurations = lib.mapAttrs (
_name: homeData:
mkHomeConfiguration {
inherit (homeData) modulePath username;
inherit pkgs;
}
) homesFlat;
}
);

hosts = importDir (src + "/hosts") (
entries:
let
loadDefaultFn = { class, value }@inputs: inputs;

loadDefault = path: loadDefaultFn (import path { inherit flake inputs; });

loadNixOS = path: {
loadNixOS = hostname: path: {
class = "nixos";
value = inputs.nixpkgs.lib.nixosSystem {
modules = [
perSystemModule
path
];
] ++ mkHomeUsersModule hostname home-manager.nixosModules.default;
inherit specialArgs;
};
};

loadNixDarwin = path: {
loadNixDarwin = hostname: path: {
class = "nix-darwin";
value = inputs.nix-darwin.lib.darwinSystem {
modules = [
perSystemModule
path
];
] ++ mkHomeUsersModule hostname home-manager.darwinModules.default;
inherit specialArgs;
};
};
@@ -181,15 +295,22 @@ let
name:
{ path, type }:
if builtins.pathExists (path + "/default.nix") then
loadDefault (path + "/default.nix")
loadDefault name (path + "/default.nix")
else if builtins.pathExists (path + "/configuration.nix") then
loadNixOS (path + "/configuration.nix")
loadNixOS name (path + "/configuration.nix")
else if builtins.pathExists (path + "/darwin-configuration.nix") then
loadNixDarwin (path + "/darwin-configuration.nix")
loadNixDarwin name (path + "/darwin-configuration.nix")
else if builtins.hasAttr name homesNested then
# If there are any home configurations defined for this host, they
# must be standalone configurations since there is no OS config.
# No config should be returned, but no error should be thrown either.
null
else
throw "host '${name}' does not have a configuration";

hostsOrNull = lib.mapAttrs loadHost entries;
in
lib.mapAttrs loadHost entries
lib.filterAttrs (_n: v: v != null) hostsOrNull
);

hostsByCategory = lib.mapAttrs (_: hosts: lib.listToAttrs hosts) (
@@ -298,6 +419,14 @@ let
)
);

# Defining homeConfigurations under legacyPackages allows the home-manager CLI
# to automatically detect the right output for the current system without
# either manually defining the pkgs set (requires explicit system) or breaking
# nix3 CLI output (`packages` output expects flat attrset)
# FIXME: Find another way to make this work without introducing legacyPackages.
# May involve changing upstream home-manager.
legacyPackages = lib.optionalAttrs (homesNested != { }) standaloneHomeConfigurations;

darwinConfigurations = lib.mapAttrs (_: x: x.value) (hostsByCategory.darwinConfigurations or { });
nixosConfigurations = lib.mapAttrs (_: x: x.value) (hostsByCategory.nixosConfigurations or { });