From 1faf0d76a9fdf030d18b43a653da8e1dcfb0fd2f Mon Sep 17 00:00:00 2001 From: Grimmauld Date: Wed, 15 Jan 2025 12:45:49 +0100 Subject: [PATCH] user activation --- modules/programs/ssh.nix | 29 ++++++-- nixos/default.nix | 155 +++++++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 52 deletions(-) diff --git a/modules/programs/ssh.nix b/modules/programs/ssh.nix index e062b232..b63d577d 100644 --- a/modules/programs/ssh.nix +++ b/modules/programs/ssh.nix @@ -493,6 +493,23 @@ in { for more information. ''; }; + + configPath = mkOption { + type = types.path; + internal = true; + description = '' + Path to the ssh configuration. + ''; + }; + + internallyManaged = mkOption { + type = types.bool; + default = true; + internal = true; + description = '' + Whether to link .ssh/config to programs.ssh.configPath + ''; + }; }; config = mkIf cfg.enable { @@ -518,14 +535,14 @@ in { home.packages = optional (cfg.package != null) cfg.package; - home.file.".ssh/config".text = let + home.file.".ssh/config".source = mkIf cfg.internallyManaged cfg.configPath; + + programs.ssh.configPath = let sortedMatchBlocks = hm.dag.topoSort cfg.matchBlocks; sortedMatchBlocksStr = builtins.toJSON sortedMatchBlocks; - matchBlocks = if sortedMatchBlocks ? result then - sortedMatchBlocks.result - else - abort "Dependency cycle in SSH match blocks: ${sortedMatchBlocksStr}"; - in '' + matchBlocks = sortedMatchBlocks.result or (abort + "Dependency cycle in SSH match blocks: ${sortedMatchBlocksStr}"); + in pkgs.writeText "ssh_config" '' ${concatStringsSep "\n" ((mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides) ++ (optional (cfg.includes != [ ]) '' diff --git a/nixos/default.nix b/nixos/default.nix index 4484d28f..9dc00a29 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -5,14 +5,28 @@ with lib; let cfg = config.home-manager; - - serviceEnvironment = optionalAttrs (cfg.backupFileExtension != null) { - HOME_MANAGER_BACKUP_EXT = cfg.backupFileExtension; - } // optionalAttrs cfg.verbose { VERBOSE = "1"; }; + baseService = username: { + Type = "oneshot"; + RemainAfterExit = "yes"; + TimeoutStartSec = "5m"; + SyslogIdentifier = "hm-activate-${username}"; + }; + baseUnit = username: { + description = "Home Manager environment for ${username}"; + stopIfChanged = false; + environment = optionalAttrs (cfg.backupFileExtension != null) { + HOME_MANAGER_BACKUP_EXT = cfg.backupFileExtension; + } // optionalAttrs cfg.verbose { VERBOSE = "1"; }; + serviceConfig = baseService username; + }; + # we use a service separated from nixos-activation + # to keep the logs separate + hmDropIn = "/share/systemd/user/home-manager.service.d"; in { imports = [ ./common.nix ]; - + options.home-manager.useUserService = mkEnableOption + "activation on each user login instead of every user together on system boot"; config = mkMerge [ { home-manager = { @@ -26,66 +40,115 @@ in { # fontconfig by default. fonts.fontconfig.enable = lib.mkDefault (cfg.useUserPackages && config.fonts.fontconfig.enable); - # Inherit glibcLocales setting from NixOS. i18n.glibcLocales = lib.mkDefault config.i18n.glibcLocales; + + # .ssh/config needs to exists before login to let ssh login as that user + programs.ssh.internallyManaged = + lib.mkDefault (!cfg.useUserService); }; }]; }; + + systemd.services = mapAttrs' (_: + { home, programs, ... }: + let inherit (home) username homeDirectory; + in nameValuePair "ssh_config-${utils.escapeSystemdPath username}" { + enable = with programs.ssh; enable && !internallyManaged; + description = "Linking ${username}' ssh config"; + wantedBy = [ "multi-user.target" ]; + before = [ "systemd-user-sessions.service" ]; + + unitConfig.RequiresMountsFor = homeDirectory; + stopIfChanged = false; + serviceConfig = (baseService username) // { + User = username; + ExecStart = [ + "${pkgs.coreutils}/bin/mkdir -p ${homeDirectory}/.ssh" + "${pkgs.coreutils}/bin/ln -s ${programs.ssh.configPath} ${homeDirectory}/.ssh/config" + ]; + }; + }) cfg.users; } - (mkIf (cfg.users != { }) { + (mkIf (cfg.users != { } && !cfg.useUserService) { systemd.services = mapAttrs' (_: usercfg: - let username = usercfg.home.username; - in nameValuePair ("home-manager-${utils.escapeSystemdPath username}") { - description = "Home Manager environment for ${username}"; + let inherit (usercfg.home) username homeDirectory activationPackage; + in nameValuePair "home-manager-${utils.escapeSystemdPath username}" + (attrsets.recursiveUpdate (baseUnit username) { wantedBy = [ "multi-user.target" ]; wants = [ "nix-daemon.socket" ]; after = [ "nix-daemon.socket" ]; before = [ "systemd-user-sessions.service" ]; - environment = serviceEnvironment; + unitConfig.RequiresMountsFor = homeDirectory; - unitConfig = { RequiresMountsFor = usercfg.home.homeDirectory; }; + serviceConfig.User = username; + serviceConfig.ExecStart = let + systemctl = + "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$UID} systemctl"; - stopIfChanged = false; + sed = "${pkgs.gnused}/bin/sed"; + exportedSystemdVariables = concatStringsSep "|" [ + "DBUS_SESSION_BUS_ADDRESS" + "DISPLAY" + "WAYLAND_DISPLAY" + "XAUTHORITY" + "XDG_RUNTIME_DIR" + ]; + setupEnv = pkgs.writeScript "hm-setup-env" '' + #! ${pkgs.runtimeShell} -el - serviceConfig = { - User = usercfg.home.username; - Type = "oneshot"; - TimeoutStartSec = "5m"; - SyslogIdentifier = "hm-activate-${username}"; + # The activation script is run by a login shell to make sure + # that the user is given a sane environment. + # If the user is logged in, import variables from their current + # session environment. + eval "$( + ${systemctl} --user show-environment 2> /dev/null \ + | ${sed} -En '/^(${exportedSystemdVariables})=/s/^/export /p' + )" - ExecStart = let - systemctl = - "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$UID} systemctl"; + exec "$1/activate" + ''; + in "${setupEnv} ${activationPackage}"; + })) cfg.users; + }) + (mkIf (cfg.users != { } && cfg.useUserService) { + systemd.user.services.home-manager = (baseUnit "%u") // { + # user units cannot depend on system units + # TODO: Insert in the script logic for waiting on the nix socket via dbus + # like https://github.com/mogorman/systemd-lock-handler + # wants = [ "nix-daemon.socket" ]; + # after = [ "nix-daemon.socket" ]; - sed = "${pkgs.gnused}/bin/sed"; + unitConfig.RequiresMountsFor = "%h"; + # no ExecStart= is defined for any user that has not defined + # config.home-manager.users.${username} + # this will be overridden by the below drop-in + }; - exportedSystemdVariables = concatStringsSep "|" [ - "DBUS_SESSION_BUS_ADDRESS" - "DISPLAY" - "WAYLAND_DISPLAY" - "XAUTHORITY" - "XDG_RUNTIME_DIR" - ]; - - setupEnv = pkgs.writeScript "hm-setup-env" '' - #! ${pkgs.runtimeShell} -el - - # The activation script is run by a login shell to make sure - # that the user is given a sane environment. - # If the user is logged in, import variables from their current - # session environment. - eval "$( - ${systemctl} --user show-environment 2> /dev/null \ - | ${sed} -En '/^(${exportedSystemdVariables})=/s/^/export /p' - )" - - exec "$1/activate" - ''; - in "${setupEnv} ${usercfg.home.activationPackage}"; - }; + users.users = mapAttrs (_: + { home, ... }: { + # unit files are taken from $XDG_DATA_DIRS too + # but are loaded after units from /etc + # we write a drop in so that it will take precedence + # over the above unit declaration + packages = [ + (pkgs.writeTextDir "${hmDropIn}/10-user-activation.conf" '' + [Service] + ExecStart=${home.activationPackage}/activate + '') + ]; }) cfg.users; + environment.pathsToLink = [ hmDropIn ]; + + # Without this will not reload home conf + # of logged user on system activation + # it will also start the unit on startup + system.userActivationScripts.home-manager = { + text = "${pkgs.systemd}/bin/systemctl --user restart home-manager"; + deps = [ ]; + }; }) ]; } +