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

3c1f45ed7ea27458db27a2af96ef6ec1392dbc15 · 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 }