mirror of
https://github.com/nix-community/home-manager.git
synced 2025-03-31 04:04:32 +00:00
Merge 08cbdd86b2
into 7170300119
This commit is contained in:
commit
c230c284b0
7 changed files with 248 additions and 88 deletions
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 != { }) {
|
||||
|
|
|
@ -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 != { }) {
|
||||
|
|
|
@ -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}"
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
7
tests/modules/home-environment/use-user-packages.nix
Normal file
7
tests/modules/home-environment/use-user-packages.nix
Normal 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."
|
||||
];
|
||||
}
|
Loading…
Add table
Reference in a new issue