Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
users/ryan/modules/targets/guix/default.nix
aeb9107982a669bb3d2f270a6d8e78eb021f6501
· 10.3 KB · 254 lines
raw
| 1 | # targets.guix: make standalone home-manager behave on Guix System. |
| 2 | # |
| 3 | # The point is that other modules stay written against upstream |
| 4 | # home-manager options (systemd.user.services, services.*, programs.*) and |
| 5 | # this target adapts them, instead of each module growing a Guix branch. |
| 6 | { config, lib, pkgs, osConfig ? null, ... }: |
| 7 | let |
| 8 | cfg = config.targets.guix; |
| 9 | sh = import ./shepherd.nix { inherit lib; setpriv = pkgs.util-linux; }; |
| 10 | |
| 11 | result = sh.translate { |
| 12 | systemdUser = config.systemd.user; |
| 13 | inherit (cfg.shepherd) unenforced ignoreUnits; |
| 14 | }; |
| 15 | |
| 16 | servicesDir = pkgs.runCommandLocal "hm-shepherd-services" { } ('' |
| 17 | mkdir -p $out |
| 18 | '' + lib.concatStrings (lib.mapAttrsToList (file: text: '' |
| 19 | cp ${pkgs.writeText file text} $out/${file} |
| 20 | '') result.files)); |
| 21 | |
| 22 | stateDir = "${config.xdg.stateHome}/home-manager/shepherd"; |
| 23 | |
| 24 | findHerd = '' |
| 25 | herd="$HOME/.guix-home/profile/bin/herd" |
| 26 | [ -x "$herd" ] || herd="$(command -v herd || true)" |
| 27 | ''; |
| 28 | |
| 29 | # Compositor -> Shepherd environment bridge (replaces |
| 30 | # `dbus-update-activation-environment --systemd` + hyprland-session.target). |
| 31 | sessionBridge = pkgs.writeShellScript "hm-shepherd-session" '' |
| 32 | ${findHerd} |
| 33 | [ -n "$herd" ] || { echo "hm-shepherd-session: herd not found" >&2; exit 1; } |
| 34 | # Stop first: dependents go down with it and come back with the new env. |
| 35 | "$herd" stop ${sh.graphicalSym} >/dev/null 2>&1 || true |
| 36 | expr="(begin" |
| 37 | for v in ${lib.escapeShellArgs cfg.shepherd.sessionVariables}; do |
| 38 | if [ -n "''${!v+x}" ]; then |
| 39 | val="''${!v}"; val="''${val//\\/\\\\}"; val="''${val//\"/\\\"}" |
| 40 | expr+=" (setenv \"$v\" \"$val\")" |
| 41 | else |
| 42 | expr+=" (unsetenv \"$v\")" |
| 43 | fi |
| 44 | done |
| 45 | expr+=" #t)" |
| 46 | "$herd" eval root "$expr" |
| 47 | "$herd" start ${sh.graphicalSym} |
| 48 | ${lib.concatMapStrings (s: '' |
| 49 | "$herd" start ${lib.escapeShellArg s} |
| 50 | '') result.graphical} |
| 51 | ''; |
| 52 | in |
| 53 | { |
| 54 | options.targets.guix = { |
| 55 | enable = lib.mkEnableOption "Guix System integration for standalone home-manager"; |
| 56 | |
| 57 | pamWrapped = lib.mkOption { |
| 58 | type = lib.types.listOf lib.types.package; |
| 59 | default = [ ]; |
| 60 | example = lib.literalExpression "[ pkgs.hyprlock ]"; |
| 61 | description = '' |
| 62 | Packages that authenticate through PAM. Nix's libpam can't drive Guix's |
| 63 | PAM stack, so their binaries are re-exported with Guix's libpam |
| 64 | preloaded. Each must set meta.mainProgram. |
| 65 | ''; |
| 66 | }; |
| 67 | |
| 68 | shepherd = { |
| 69 | unenforced = lib.mkOption { |
| 70 | type = with lib.types; attrsOf (listOf str); |
| 71 | default = { }; |
| 72 | example = { "foo.service" = [ "Service.ProtectSystem" "Service.PrivateTmp" ]; }; |
| 73 | description = '' |
| 74 | Per-unit acknowledgement that the listed `Section.Key`s (or |
| 75 | `Section.*`) are dropped when translating to Shepherd. Without an |
| 76 | entry, any untranslatable key is an evaluation error. Each dropped |
| 77 | key is reported as a build warning on every switch. |
| 78 | ''; |
| 79 | }; |
| 80 | ignoreUnits = lib.mkOption { |
| 81 | type = with lib.types; listOf str; |
| 82 | default = [ ]; |
| 83 | example = [ "tray.target" ]; |
| 84 | description = "Full unit names to skip entirely (not translated, no error)."; |
| 85 | }; |
| 86 | sessionBridge = lib.mkOption { |
| 87 | type = lib.types.package; |
| 88 | readOnly = true; |
| 89 | default = sessionBridge; |
| 90 | description = "Script the compositor runs (exec-once) to start graphical services; also usable by hand."; |
| 91 | }; |
| 92 | sessionVariables = lib.mkOption { |
| 93 | type = with lib.types; listOf str; |
| 94 | default = config.wayland.windowManager.hyprland.systemd.variables |
| 95 | ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ]; |
| 96 | defaultText = lib.literalExpression ''hyprland.systemd.variables ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ]''; |
| 97 | description = "Variables the compositor pushes into Shepherd before starting graphical services."; |
| 98 | }; |
| 99 | }; |
| 100 | }; |
| 101 | |
| 102 | config = lib.mkIf cfg.enable (lib.mkMerge [ |
| 103 | { |
| 104 | assertions = [ |
| 105 | { |
| 106 | # osConfig is only passed when home-manager runs as a NixOS or |
| 107 | # nix-darwin module, i.e. never on a Guix host. |
| 108 | assertion = osConfig == null; |
| 109 | message = "targets.guix is enabled inside a NixOS/nix-darwin home-manager module; it only applies to standalone home-manager on Guix System."; |
| 110 | } |
| 111 | { |
| 112 | assertion = config.targets.genericLinux.enable; |
| 113 | message = "targets.guix builds on targets.genericLinux; enable it too."; |
| 114 | } |
| 115 | ] ++ map (e: { assertion = false; message = "targets.guix: ${e}"; }) result.errors; |
| 116 | |
| 117 | warnings = result.warnings; |
| 118 | |
| 119 | # HM's ssh-auth-sock module (pulled in by gpg-agent's SSH support) adds |
| 120 | # a oneshot that runs `dbus-update-activation-environment --systemd |
| 121 | # SSH_AUTH_SOCK`; its whole job is systemd/D-Bus environment plumbing. |
| 122 | # The shell integration from the same module still sets the variable, |
| 123 | # and the session bridge pushes it into Shepherd (sessionVariables). |
| 124 | targets.guix.shepherd.ignoreUnits = [ "set-SSH_AUTH_SOCK.service" ]; |
| 125 | |
| 126 | # No systemd user manager: units are translated to Shepherd instead. |
| 127 | systemd.user.startServices = false; |
| 128 | |
| 129 | # HM's GPU setup installs /etc/tmpfiles.d + runs systemd-tmpfiles as |
| 130 | # root; neither exists on Guix, so it only produces a warning per switch. |
| 131 | targets.genericLinux.gpu.enable = lib.mkDefault false; |
| 132 | |
| 133 | # Guix's SSH patches openssh to not allow files outside of the GNU store |
| 134 | # so we need to copy the SSH config from the store into the userr's |
| 135 | # home directory to not get a permission error |
| 136 | home.activation.removeSshConfigSymlink = lib.mkIf config.programs.ssh.enable ( |
| 137 | lib.hm.dag.entryBefore [ "checkLinkTargets" ] '' |
| 138 | if [ -n "''${HOME:-}" ] && [ -e "$HOME/.ssh/config" ]; then |
| 139 | rm -f "$HOME/.ssh/config" |
| 140 | fi |
| 141 | '' |
| 142 | ); |
| 143 | |
| 144 | home.activation.fixSshPermissions = lib.mkIf config.programs.ssh.enable ( |
| 145 | lib.hm.dag.entryAfter [ "linkGeneration" ] '' |
| 146 | run install -d -m 0700 "$HOME/.ssh" |
| 147 | if [ -L "$HOME/.ssh/config" ]; then |
| 148 | src="$(readlink -f "$HOME/.ssh/config")" |
| 149 | run rm -f "$HOME/.ssh/config" |
| 150 | run install -m 0600 "$src" "$HOME/.ssh/config" |
| 151 | fi |
| 152 | '' |
| 153 | ); |
| 154 | |
| 155 | # Its exec-once runs `dbus-update-activation-environment --systemd && |
| 156 | # systemctl --user ...`, which fails at the first step on Guix. The |
| 157 | # bridge below does the Shepherd equivalent. |
| 158 | wayland.windowManager.hyprland.systemd.enable = lib.mkDefault false; |
| 159 | wayland.windowManager.hyprland.extraConfig = lib.mkIf config.wayland.windowManager.hyprland.enable '' |
| 160 | exec-once = ${sessionBridge} |
| 161 | ''; |
| 162 | |
| 163 | dconf.enable = lib.mkDefault false; |
| 164 | |
| 165 | # Guix Home owns ~/.zprofile's job of loading its environment (and |
| 166 | # starting the user Shepherd via on-first-login); keep that working |
| 167 | # under an HM-managed zsh. |
| 168 | programs.zsh.profileExtra = lib.mkBefore '' |
| 169 | if [ -f "$HOME/.guix-home/setup-environment" ]; then |
| 170 | HOME_ENVIRONMENT="$HOME/.guix-home" |
| 171 | . "$HOME_ENVIRONMENT/setup-environment" |
| 172 | "$HOME_ENVIRONMENT/on-first-login" |
| 173 | unset HOME_ENVIRONMENT |
| 174 | fi |
| 175 | |
| 176 | # /etc/profile puts ~/.config/guix/current (guix pull profile with |
| 177 | # our channels) ahead of the system profile, but only bash login |
| 178 | # shells source it. |
| 179 | if [ -d "$HOME/.config/guix/current" ]; then |
| 180 | export PATH="$HOME/.config/guix/current/bin:$PATH" |
| 181 | export INFOPATH="$HOME/.config/guix/current/share/info''${INFOPATH:+:}$INFOPATH" |
| 182 | fi |
| 183 | ''; |
| 184 | |
| 185 | # No /etc/fonts/fonts.conf from Nix's point of view on Guix. |
| 186 | xdg.configFile."fontconfig-nix/fonts.conf".text = '' |
| 187 | <?xml version='1.0'?> |
| 188 | <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'> |
| 189 | <fontconfig> |
| 190 | <include ignore_missing="yes">${pkgs.fontconfig.out}/etc/fonts/conf.d</include> |
| 191 | <include ignore_missing="yes">${config.xdg.configHome}/fontconfig/conf.d</include> |
| 192 | <dir>${config.home.homeDirectory}/.guix-home/profile/share/fonts</dir> |
| 193 | <cachedir>${config.xdg.cacheHome}/fontconfig</cachedir> |
| 194 | </fontconfig> |
| 195 | ''; |
| 196 | home.sessionVariables.FONTCONFIG_FILE = "${config.xdg.configHome}/fontconfig-nix/fonts.conf"; |
| 197 | |
| 198 | home.packages = map |
| 199 | (p: lib.hiPrio (pkgs.writeShellScriptBin p.meta.mainProgram '' |
| 200 | export LD_PRELOAD="/run/current-system/profile/lib/libpam.so.0''${LD_PRELOAD:+:$LD_PRELOAD}" |
| 201 | exec ${lib.getExe p} "$@" |
| 202 | '')) |
| 203 | cfg.pamWrapped; |
| 204 | |
| 205 | # Sync generated Shepherd services; reload only what changed. |
| 206 | home.activation.shepherdServices = lib.hm.dag.entryAfter [ "linkGeneration" "reloadSystemd" ] '' |
| 207 | if [ ! -x /run/current-system/profile/bin/guix ]; then |
| 208 | errorEcho "targets.guix: this host isn't Guix System; refusing to manage Shepherd services." |
| 209 | exit 1 |
| 210 | fi |
| 211 | |
| 212 | ${findHerd} |
| 213 | live= |
| 214 | if [ -n "$herd" ] && "$herd" status root >/dev/null 2>&1; then live=1; fi |
| 215 | |
| 216 | dst=${lib.escapeShellArg stateDir} |
| 217 | run mkdir -p "$dst" |
| 218 | |
| 219 | for f in "$dst"/*.scm; do |
| 220 | [ -e "$f" ] || continue |
| 221 | n="$(basename "$f" .scm)" |
| 222 | if [ ! -e ${servicesDir}/"$n".scm ]; then |
| 223 | [ -n "$live" ] && run "$herd" unload root "$n" || true |
| 224 | run rm -f "$f" |
| 225 | fi |
| 226 | done |
| 227 | |
| 228 | for f in ${servicesDir}/*.scm; do |
| 229 | n="$(basename "$f" .scm)" |
| 230 | if ! cmp -s "$f" "$dst/$n.scm"; then |
| 231 | run install -m 0644 "$f" "$dst/$n.scm" |
| 232 | if [ -n "$live" ]; then |
| 233 | run "$herd" unload root "$n" >/dev/null 2>&1 || true |
| 234 | run "$herd" load root "$dst/$n.scm" |
| 235 | # Autostart services restart themselves from the loaded file; |
| 236 | # graphical ones only if a session is up (else they'd start |
| 237 | # with no compositor environment). |
| 238 | case " ${lib.concatStringsSep " " result.graphical} " in |
| 239 | *" $n "*) |
| 240 | if "$herd" status ${sh.graphicalSym} 2>/dev/null | grep -q 'It is running'; then |
| 241 | run "$herd" start "$n" |
| 242 | fi ;; |
| 243 | esac |
| 244 | fi |
| 245 | fi |
| 246 | done |
| 247 | |
| 248 | if [ -z "$live" ]; then |
| 249 | warnEcho "targets.guix: user Shepherd not reachable; services register at next login via the Guix Home loader." |
| 250 | fi |
| 251 | ''; |
| 252 | } |
| 253 | ]); |
| 254 | } |