# targets.guix: make standalone home-manager behave on Guix System. # # The point is that other modules stay written against upstream # home-manager options (systemd.user.services, services.*, programs.*) and # this target adapts them, instead of each module growing a Guix branch. { config, lib, pkgs, osConfig ? null, ... }: let cfg = config.targets.guix; sh = import ./shepherd.nix { inherit lib; setpriv = pkgs.util-linux; }; result = sh.translate { systemdUser = config.systemd.user; inherit (cfg.shepherd) unenforced ignoreUnits; }; servicesDir = pkgs.runCommandLocal "hm-shepherd-services" { } ('' mkdir -p $out '' + lib.concatStrings (lib.mapAttrsToList (file: text: '' cp ${pkgs.writeText file text} $out/${file} '') result.files)); stateDir = "${config.xdg.stateHome}/home-manager/shepherd"; findHerd = '' herd="$HOME/.guix-home/profile/bin/herd" [ -x "$herd" ] || herd="$(command -v herd || true)" ''; # Compositor -> Shepherd environment bridge (replaces # `dbus-update-activation-environment --systemd` + hyprland-session.target). sessionBridge = pkgs.writeShellScript "hm-shepherd-session" '' ${findHerd} [ -n "$herd" ] || { echo "hm-shepherd-session: herd not found" >&2; exit 1; } # Stop first: dependents go down with it and come back with the new env. "$herd" stop ${sh.graphicalSym} >/dev/null 2>&1 || true expr="(begin" for v in ${lib.escapeShellArgs cfg.shepherd.sessionVariables}; do if [ -n "''${!v+x}" ]; then val="''${!v}"; val="''${val//\\/\\\\}"; val="''${val//\"/\\\"}" expr+=" (setenv \"$v\" \"$val\")" else expr+=" (unsetenv \"$v\")" fi done expr+=" #t)" "$herd" eval root "$expr" "$herd" start ${sh.graphicalSym} ${lib.concatMapStrings (s: '' "$herd" start ${lib.escapeShellArg s} '') result.graphical} ''; in { options.targets.guix = { enable = lib.mkEnableOption "Guix System integration for standalone home-manager"; pamWrapped = lib.mkOption { type = lib.types.listOf lib.types.package; default = [ ]; example = lib.literalExpression "[ pkgs.hyprlock ]"; description = '' Packages that authenticate through PAM. Nix's libpam can't drive Guix's PAM stack, so their binaries are re-exported with Guix's libpam preloaded. Each must set meta.mainProgram. ''; }; shepherd = { unenforced = lib.mkOption { type = with lib.types; attrsOf (listOf str); default = { }; example = { "foo.service" = [ "Service.ProtectSystem" "Service.PrivateTmp" ]; }; description = '' Per-unit acknowledgement that the listed `Section.Key`s (or `Section.*`) are dropped when translating to Shepherd. Without an entry, any untranslatable key is an evaluation error. Each dropped key is reported as a build warning on every switch. ''; }; ignoreUnits = lib.mkOption { type = with lib.types; listOf str; default = [ ]; example = [ "tray.target" ]; description = "Full unit names to skip entirely (not translated, no error)."; }; sessionBridge = lib.mkOption { type = lib.types.package; readOnly = true; default = sessionBridge; description = "Script the compositor runs (exec-once) to start graphical services; also usable by hand."; }; sessionVariables = lib.mkOption { type = with lib.types; listOf str; default = config.wayland.windowManager.hyprland.systemd.variables ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ]; defaultText = lib.literalExpression ''hyprland.systemd.variables ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ]''; description = "Variables the compositor pushes into Shepherd before starting graphical services."; }; }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { assertions = [ { # osConfig is only passed when home-manager runs as a NixOS or # nix-darwin module, i.e. never on a Guix host. assertion = osConfig == null; message = "targets.guix is enabled inside a NixOS/nix-darwin home-manager module; it only applies to standalone home-manager on Guix System."; } { assertion = config.targets.genericLinux.enable; message = "targets.guix builds on targets.genericLinux; enable it too."; } ] ++ map (e: { assertion = false; message = "targets.guix: ${e}"; }) result.errors; warnings = result.warnings; # HM's ssh-auth-sock module (pulled in by gpg-agent's SSH support) adds # a oneshot that runs `dbus-update-activation-environment --systemd # SSH_AUTH_SOCK`; its whole job is systemd/D-Bus environment plumbing. # The shell integration from the same module still sets the variable, # and the session bridge pushes it into Shepherd (sessionVariables). targets.guix.shepherd.ignoreUnits = [ "set-SSH_AUTH_SOCK.service" ]; # No systemd user manager: units are translated to Shepherd instead. systemd.user.startServices = false; # HM's GPU setup installs /etc/tmpfiles.d + runs systemd-tmpfiles as # root; neither exists on Guix, so it only produces a warning per switch. targets.genericLinux.gpu.enable = lib.mkDefault false; # Guix's SSH patches openssh to not allow files outside of the GNU store # so we need to copy the SSH config from the store into the userr's # home directory to not get a permission error home.activation.removeSshConfigSymlink = lib.mkIf config.programs.ssh.enable ( lib.hm.dag.entryBefore [ "checkLinkTargets" ] '' if [ -n "''${HOME:-}" ] && [ -e "$HOME/.ssh/config" ]; then rm -f "$HOME/.ssh/config" fi '' ); home.activation.fixSshPermissions = lib.mkIf config.programs.ssh.enable ( lib.hm.dag.entryAfter [ "linkGeneration" ] '' run install -d -m 0700 "$HOME/.ssh" if [ -L "$HOME/.ssh/config" ]; then src="$(readlink -f "$HOME/.ssh/config")" run rm -f "$HOME/.ssh/config" run install -m 0600 "$src" "$HOME/.ssh/config" fi '' ); # Its exec-once runs `dbus-update-activation-environment --systemd && # systemctl --user ...`, which fails at the first step on Guix. The # bridge below does the Shepherd equivalent. wayland.windowManager.hyprland.systemd.enable = lib.mkDefault false; wayland.windowManager.hyprland.extraConfig = lib.mkIf config.wayland.windowManager.hyprland.enable '' exec-once = ${sessionBridge} ''; dconf.enable = lib.mkDefault false; # Guix Home owns ~/.zprofile's job of loading its environment (and # starting the user Shepherd via on-first-login); keep that working # under an HM-managed zsh. programs.zsh.profileExtra = lib.mkBefore '' if [ -f "$HOME/.guix-home/setup-environment" ]; then HOME_ENVIRONMENT="$HOME/.guix-home" . "$HOME_ENVIRONMENT/setup-environment" "$HOME_ENVIRONMENT/on-first-login" unset HOME_ENVIRONMENT fi # /etc/profile puts ~/.config/guix/current (guix pull profile with # our channels) ahead of the system profile, but only bash login # shells source it. if [ -d "$HOME/.config/guix/current" ]; then export PATH="$HOME/.config/guix/current/bin:$PATH" export INFOPATH="$HOME/.config/guix/current/share/info''${INFOPATH:+:}$INFOPATH" fi ''; # No /etc/fonts/fonts.conf from Nix's point of view on Guix. xdg.configFile."fontconfig-nix/fonts.conf".text = '' ${pkgs.fontconfig.out}/etc/fonts/conf.d ${config.xdg.configHome}/fontconfig/conf.d ${config.home.homeDirectory}/.guix-home/profile/share/fonts ${config.xdg.cacheHome}/fontconfig ''; home.sessionVariables.FONTCONFIG_FILE = "${config.xdg.configHome}/fontconfig-nix/fonts.conf"; home.packages = map (p: lib.hiPrio (pkgs.writeShellScriptBin p.meta.mainProgram '' export LD_PRELOAD="/run/current-system/profile/lib/libpam.so.0''${LD_PRELOAD:+:$LD_PRELOAD}" exec ${lib.getExe p} "$@" '')) cfg.pamWrapped; # Sync generated Shepherd services; reload only what changed. home.activation.shepherdServices = lib.hm.dag.entryAfter [ "linkGeneration" "reloadSystemd" ] '' if [ ! -x /run/current-system/profile/bin/guix ]; then errorEcho "targets.guix: this host isn't Guix System; refusing to manage Shepherd services." exit 1 fi ${findHerd} live= if [ -n "$herd" ] && "$herd" status root >/dev/null 2>&1; then live=1; fi dst=${lib.escapeShellArg stateDir} run mkdir -p "$dst" for f in "$dst"/*.scm; do [ -e "$f" ] || continue n="$(basename "$f" .scm)" if [ ! -e ${servicesDir}/"$n".scm ]; then [ -n "$live" ] && run "$herd" unload root "$n" || true run rm -f "$f" fi done for f in ${servicesDir}/*.scm; do n="$(basename "$f" .scm)" if ! cmp -s "$f" "$dst/$n.scm"; then run install -m 0644 "$f" "$dst/$n.scm" if [ -n "$live" ]; then run "$herd" unload root "$n" >/dev/null 2>&1 || true run "$herd" load root "$dst/$n.scm" # Autostart services restart themselves from the loaded file; # graphical ones only if a session is up (else they'd start # with no compositor environment). case " ${lib.concatStringsSep " " result.graphical} " in *" $n "*) if "$herd" status ${sh.graphicalSym} 2>/dev/null | grep -q 'It is running'; then run "$herd" start "$n" fi ;; esac fi fi done if [ -z "$live" ]; then warnEcho "targets.guix: user Shepherd not reachable; services register at next login via the Guix Home loader." fi ''; } ]); }