1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-03-31 04:04:32 +00:00
This commit is contained in:
Matt Schreiber 2025-03-30 15:05:12 +07:00 committed by GitHub
commit c230c284b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 248 additions and 88 deletions

View file

@ -22,6 +22,23 @@ This release has the following notable changes:
tests are available through a Nix Flake file inside the `tests`
directory. See [](#sec-tests) for example commands.
- The Home Manager NixOS module now supports
[home-manager.users](#nixos-opt-home-manager.users) entries that do not have
corresponding `users.users.<name>` entries, making it easier to provide Home
Manager configurations for users managed through external identity management
systems. To take advantage of this, such users must have their
[](#opt-home.username) and [](#opt-home.homeDirectory) attributes set to
high-precedence values with `lib.mkForce` or similar, overriding the defaults
that pull from, respectively, `users.users.<name>.name` and
`users.users.<name>.home`.
- The Home Manager NixOS module option
[home-manager.useUserPackages](#nixos-opt-home-manager.useUserPackages) now
sets the default value of the newly-introduced per-user
[](#opt-home.useUserPackages) option, making it possible to define a policy
on the use of `users.users.<name>.packages` for package installation, and to
override that policy on a user-specific basis.
## State Version Changes {#sec-release-25.05-state-version-changes}
The state version in this release includes the changes below. These

View file

@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, ... }@args:
let
inherit (lib) literalExpression mkOption types;
@ -500,6 +500,21 @@ in
Whether to make programs use XDG directories whenever supported.
'';
};
home.useUserPackages = mkOption {
type = types.bool;
default = args.nixosConfig.home-manager.useUserPackages or false;
defaultText = lib.literalExpression "home-manager.useUserPackages";
description = ''
Enable installation of user packages through the
{option}`users.users.<name>.packages` option.
::: {.warning}
This option is specific to NixOS configurations and has no effect
elsewhere.
:::
'';
};
};
config = {
@ -521,6 +536,7 @@ in
releaseMismatch =
config.home.enableNixpkgsReleaseCheck
&& hmRelease != nixpkgsRelease;
wronglyUsingUserPackages = cfg.useUserPackages && (!(args ? nixosConfig));
in
lib.optional releaseMismatch ''
You are using
@ -537,7 +553,11 @@ in
home.enableNixpkgsReleaseCheck = false;
to your configuration.
'';
''
++ lib.optional wronglyUsingUserPackages
"You have enabled `home.useUserPackages`, but this option has no effect outside of NixOS configurations.";
home.username =
lib.mkIf (lib.versionOlder config.home.stateVersion "20.09")

View file

@ -10,6 +10,27 @@ let
extendedLib = import ../modules/lib/stdlib-extended.nix lib;
usersUsingUserPackages =
lib.filterAttrs (_username: usercfg: usercfg.home.useUserPackages)
cfg.users;
# `defaultOverridePriority` (and its legacy counterpart `defaultPriority`)
# represents the priority of option definitions that do not explicity specify
# a priority; that is, definitions of the form `{ foo = "bar"; }`.
#
# Setting an option with `mkUserDefault` permits the module system to resolve
# the so-defined option's priority without evaluating its value. We use this
# to define `home.username` and `home.homeDirectory` at the default priority,
# while also allowing consumers to override these values with `mkForce`, and,
# crucially, allowing consumers to define `home-manager.users` entries
# **without those users needing to have corresponding entries in
# `config.users.users`**.
#
# In other respects, `{ foo = mkUserDefault "bar"; }` quacks like
# `{ foo = "bar"; }`.
mkUserDefault = lib.mkOverride
(lib.modules.defaultOverridePriority or lib.modules.defaultPriority);
hmModule = types.submoduleWith {
description = "Home Manager module";
class = "homeManager";
@ -19,7 +40,7 @@ let
modulesPath = builtins.toString ../modules;
} // cfg.extraSpecialArgs;
modules = [
({ name, ... }: {
({ name, ... }@usercfg: {
imports = import ../modules/modules.nix {
inherit pkgs;
lib = extendedLib;
@ -28,10 +49,11 @@ let
config = {
submoduleSupport.enable = true;
submoduleSupport.externalPackageInstall = cfg.useUserPackages;
submoduleSupport.externalPackageInstall =
usercfg.config.home.useUserPackages;
home.username = config.users.users.${name}.name;
home.homeDirectory = config.users.users.${name}.home;
home.username = mkUserDefault config.users.users.${name}.name;
home.homeDirectory = mkUserDefault config.users.users.${name}.home;
# Forward `nix.enable` from the OS configuration. The
# conditional is to check whether nix-darwin is new enough
@ -52,8 +74,8 @@ let
in {
options.home-manager = {
useUserPackages = mkEnableOption ''
installation of user packages through the
{option}`users.users.<name>.packages` option'';
the per-user option {option}`home-manager.users.<name>.useUserPackages`
by default'';
useGlobalPkgs = mkEnableOption ''
using the system configuration's `pkgs`
@ -104,10 +126,10 @@ in {
};
config = (lib.mkMerge [
# Fix potential recursion when configuring home-manager users based on values in users.users #594
(mkIf (cfg.useUserPackages && cfg.users != { }) {
(mkIf (usersUsingUserPackages != { }) {
users.users = (lib.mapAttrs
(_username: usercfg: { packages = [ usercfg.home.path ]; }) cfg.users);
(_username: usercfg: { packages = [ usercfg.home.path ]; })
usersUsingUserPackages);
environment.pathsToLink = [ "/etc/profile.d" ];
})
(mkIf (cfg.users != { }) {

View file

@ -16,19 +16,22 @@ in {
home-manager = {
extraSpecialArgs.nixosConfig = config;
sharedModules = [{
key = "home-manager#nixos-shared-module";
sharedModules = [
({ ... }@usercfg: {
key = "home-manager#nixos-shared-module";
config = {
# The per-user directory inside /etc/profiles is not known by
# fontconfig by default.
fonts.fontconfig.enable = lib.mkDefault
(cfg.useUserPackages && config.fonts.fontconfig.enable);
config = {
# The per-user directory inside /etc/profiles is not known by
# fontconfig by default.
fonts.fontconfig.enable = lib.mkDefault
(usercfg.config.home.useUserPackages != { }
&& config.fonts.fontconfig.enable);
# Inherit glibcLocales setting from NixOS.
i18n.glibcLocales = lib.mkDefault config.i18n.glibcLocales;
};
}];
# Inherit glibcLocales setting from NixOS.
i18n.glibcLocales = lib.mkDefault config.i18n.glibcLocales;
};
})
];
};
}
(lib.mkIf (cfg.users != { }) {

View file

@ -1,94 +1,184 @@
{ pkgs, ... }:
{ lib, pkgs, ... }:
{
name = "nixos-basics";
meta.maintainers = [ pkgs.lib.maintainers.rycee ];
nodes.machine = { ... }: {
imports = [ ../../../nixos ]; # Import the HM NixOS module.
nodes.machine = { config, pkgs, ... }:
let inherit (config.home-manager.users) bob;
in {
imports = [ ../../../nixos ]; # Import the HM NixOS module.
virtualisation.memorySize = 2048;
security.pam.services = {
chpasswd = { };
passwd = { };
};
users.users.alice = {
isNormalUser = true;
description = "Alice Foobar";
system.activationScripts.addBobUser = lib.fullDepEntry ''
${pkgs.shadow}/bin/useradd \
--comment 'Manually-created user' \
--create-home --no-user-group \
--gid ${lib.escapeShellArg config.users.groups.users.name} \
--home-dir ${lib.escapeShellArg bob.home.homeDirectory} \
--no-user-group \
--shell ${
lib.escapeShellArg (lib.getExe config.users.defaultUserShell)
} \
${lib.escapeShellArg bob.home.username}
'' [ "groups" "users" ];
virtualisation.memorySize = 2048;
# To be able to add the `bob` account with `useradd`.
users.mutableUsers = true;
users.users.alice = {
isNormalUser = true;
description = "Alice Foobar";
uid = 1000;
};
home-manager.useUserPackages = true;
home-manager.users = let
common = {
home.stateVersion = "24.11";
home.file.test.text = "testfile";
home.packages = [ pkgs.hello ];
# Enable a light-weight systemd service.
services.pueue.enable = true;
};
in {
alice = common;
# User without corresponding entry in `users.users`.
bob = { name, ... }: {
imports = [ common ];
home.stateVersion = "24.11";
home.username = lib.mkForce name;
home.homeDirectory = lib.mkForce "/var/tmp/hm/home/bob";
home.useUserPackages = false;
};
};
};
testScript = { nodes, ... }:
let
inherit (nodes.machine.home-manager.users) alice bob;
password = "foobar";
uid = 1000;
};
in ''
import pprint
from contextlib import contextmanager
from typing import Callable, Dict, Optional
home-manager.users.alice = { ... }: {
home.stateVersion = "24.11";
home.file.test.text = "testfile";
# Enable a light-weight systemd service.
services.pueue.enable = true;
};
};
def wait_for_unit_properties(
machine: Machine, unit: str, check: Callable[[Dict[str, str]], Dict[str, Optional[str]]], user: Optional[str] = None, timeout: int = 900,
) -> None:
def unit_has_properties(_):
info = machine.get_unit_info(unit, user)
mismatches = check(info)
if mismatches == {}:
return True
else:
machine.log(f"unit {unit} has unexpected properties {pprint.pformat(mismatches)}")
return False
testScript = ''
def login_as_alice():
machine.wait_until_tty_matches("1", "login: ")
machine.send_chars("alice\n")
machine.wait_until_tty_matches("1", "Password: ")
machine.send_chars("foobar\n")
machine.wait_until_tty_matches("1", "alice\\@machine")
with machine.nested(f"waiting for unit {unit} to satisfy expected properties"):
retry(unit_has_properties, timeout)
def logout_alice():
machine.send_chars("exit\n")
def wait_for_oneshot_successful_completion(
machine: Machine, unit: str, user: Optional[str] = None, timeout: int = 900
) -> None:
def unit_is_successfully_completed_oneshot(info: Dict[str, str]) -> Dict[str, Optional[str]]:
assert "Type" in info, f"unit {unit}'s properties include 'Type'"
assert info["Type"] == "oneshot", f"expected unit {unit}'s 'Type' to be 'oneshot'; got {info['Type']}"
def alice_cmd(cmd):
return f"su -l alice --shell /bin/sh -c $'export XDG_RUNTIME_DIR=/run/user/$UID ; {cmd}'"
props = ["ActiveState", "Result", "SubState"]
def succeed_as_alice(cmd):
return machine.succeed(alice_cmd(cmd))
if all([prop in info for prop in props]):
if "RemainAfterExit" in info and info["RemainAfterExit"] == "yes":
if info["ActiveState"] == "active" and info["Result"] == "success" and info["SubState"] == "exited":
return {}
elif info["ActiveState"] == "inactive" and info["Result"] == "success" and info["SubState"] == "dead":
return {}
def fail_as_alice(cmd):
return machine.fail(alice_cmd(cmd))
return {prop: info.get(prop) for prop in props}
start_all()
return wait_for_unit_properties(
machine, unit, unit_is_successfully_completed_oneshot, user=user, timeout=timeout
)
machine.wait_for_console_text("Finished Home Manager environment for alice.")
@contextmanager
def user_login(machine: Machine, user: str, password: str):
machine.wait_until_tty_matches("1", "login: ")
machine.send_chars(f"{user}\n")
machine.wait_until_tty_matches("1", "Password: ")
machine.send_chars(f"{password}\n")
machine.wait_until_tty_matches("1", f"{user}\\@{machine.name}")
with subtest("Home Manager file"):
# The file should be linked with the expected content.
path = "/home/alice/test"
machine.succeed(f"test -L {path}")
actual = machine.succeed(f"cat {path}")
expected = "testfile"
assert actual == expected, f"expected {path} to contain {expected}, but got {actual}"
try:
yield
finally:
machine.send_chars("exit\n")
with subtest("Pueue service"):
login_as_alice()
def cmd_for_user(user: str, cmd: str) -> str:
return f"su -l {user} --shell /bin/sh -c $'export XDG_RUNTIME_DIR=/run/user/$UID ; {cmd}'"
actual = succeed_as_alice("pueue status")
expected = "running"
assert expected in actual, f"expected pueue status to contain {expected}, but got {actual}"
def succeed_as_user(user: str, cmd: str) -> str:
return machine.succeed(cmd_for_user(user, cmd))
# Shut down pueue, then run the activation again. Afterwards, the service
# should be running.
machine.succeed("systemctl --user -M alice@.host stop pueued.service")
def fail_as_user(user: str, cmd: str) -> str:
return machine.fail(cmd_for_user(user, cmd))
fail_as_alice("pueue status")
start_all()
machine.systemctl("restart home-manager-alice.service")
machine.wait_for_console_text("Finished Home Manager environment for alice.")
for user in ["${alice.home.username}", "${bob.home.username}"]:
user_hm_unit = f"home-manager-{user}.service"
wait_for_oneshot_successful_completion(machine, user_hm_unit)
actual = succeed_as_alice("pueue status")
expected = "running"
assert expected in actual, f"expected pueue status to contain {expected}, but got {actual}"
machine.succeed(f"echo '{user}:${password}' | ${pkgs.shadow}/bin/chpasswd")
logout_alice()
with subtest(f"Home Manager file (user {user})"):
# The file should be linked with the expected content.
path = f"~{user}/test"
machine.succeed(f"test -L {path}")
actual = machine.succeed(f"cat {path}")
expected = "testfile"
assert actual == expected, f"expected {path} to contain {expected}, but got {actual}"
with subtest("GC root and profile"):
# There should be a GC root and Home Manager profile and they should point
# to the same path in the Nix store.
gcroot = "/home/alice/.local/state/home-manager/gcroots/current-home"
gcrootTarget = machine.succeed(f"readlink {gcroot}")
with subtest(f"Command from `home.packages` (user {user})"):
succeed_as_user(user, "hello")
profile = "/home/alice/.local/state/nix/profiles"
profileTarget = machine.succeed(f"readlink {profile}/home-manager")
profile1Target = machine.succeed(f"readlink {profile}/{profileTarget}")
with subtest(f"Pueue service (user {user})"):
with user_login(machine, user, "${password}"):
actual = succeed_as_user(user, "pueue status")
expected = "running"
assert expected in actual, f"expected pueue status to contain {expected}, but got {actual}"
assert gcrootTarget == profile1Target, \
f"expected GC root and profile to point to same, but pointed to {gcrootTarget} and {profile1Target}"
'';
# Shut down pueue, then run the activation again. Afterwards, the
# service should be running.
machine.succeed(f"systemctl --user -M {user}@.host stop pueued.service")
fail_as_user(user, "pueue status")
machine.systemctl(f"restart {user_hm_unit}")
wait_for_oneshot_successful_completion(machine, user_hm_unit)
actual = succeed_as_user(user, "pueue status")
expected = "running"
assert expected in actual, f"expected pueue status to contain {expected}, but got {actual}"
with subtest(f"GC root and profile (user {user})"):
# There should be a GC root and Home Manager profile and they should
# point to the same path in the Nix store.
gcroot = f"~{user}/.local/state/home-manager/gcroots/current-home"
gcrootTarget = machine.succeed(f"readlink {gcroot}")
profile = f"~{user}/.local/state/nix/profiles"
profileTarget = machine.succeed(f"readlink {profile}/home-manager")
profile1Target = machine.succeed(f"readlink {profile}/{profileTarget}")
assert gcrootTarget == profile1Target, \
f"expected GC root and profile to point to same, but pointed to {gcrootTarget} and {profile1Target}"
'';
}

View file

@ -2,4 +2,5 @@
home-session-path = ./session-path.nix;
home-session-search-variables = ./session-search-variables.nix;
home-session-variables = ./session-variables.nix;
home-use-user-packages = ./use-user-packages.nix;
}

View file

@ -0,0 +1,7 @@
{
home.useUserPackages = true;
test.asserts.warnings.expected = [
"You have enabled `home.useUserPackages`, but this option has no effect outside of NixOS configurations."
];
}