1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9
10 cfg = config.services.xserver.displayManager;
11 gdm = pkgs.gdm;
12 pamLogin = config.security.pam.services.login;
13 settingsFormat = pkgs.formats.ini { };
14 configFile = settingsFormat.generate "custom.conf" cfg.gdm.settings;
15
16 xSessionWrapper =
17 if (cfg.setupCommands == "") then
18 null
19 else
20 pkgs.writeScript "gdm-x-session-wrapper" ''
21 #!${pkgs.bash}/bin/bash
22 ${cfg.setupCommands}
23 exec "$@"
24 '';
25
26 # Solves problems like:
27 # https://wiki.archlinux.org/index.php/Talk:Bluetooth_headset#GDMs_pulseaudio_instance_captures_bluetooth_headset
28 # Instead of blacklisting plugins, we use Fedora's PulseAudio configuration for GDM:
29 # https://src.fedoraproject.org/rpms/gdm/blob/master/f/default.pa-for-gdm
30 pulseConfig = pkgs.writeText "default.pa" ''
31 load-module module-device-restore
32 load-module module-card-restore
33 load-module module-udev-detect
34 load-module module-native-protocol-unix
35 load-module module-default-device-restore
36 load-module module-always-sink
37 load-module module-intended-roles
38 load-module module-suspend-on-idle
39 load-module module-position-event-sounds
40 '';
41
42 defaultSessionName = config.services.displayManager.defaultSession;
43
44 setSessionScript = pkgs.callPackage ./account-service-util.nix { };
45in
46
47{
48 imports = [
49 (lib.mkRenamedOptionModule
50 [ "services" "xserver" "displayManager" "gdm" "autoLogin" "enable" ]
51 [
52 "services"
53 "displayManager"
54 "autoLogin"
55 "enable"
56 ]
57 )
58 (lib.mkRenamedOptionModule
59 [ "services" "xserver" "displayManager" "gdm" "autoLogin" "user" ]
60 [
61 "services"
62 "displayManager"
63 "autoLogin"
64 "user"
65 ]
66 )
67
68 (lib.mkRemovedOptionModule [
69 "services"
70 "xserver"
71 "displayManager"
72 "gdm"
73 "nvidiaWayland"
74 ] "We defer to GDM whether Wayland should be enabled.")
75 ];
76
77 meta = {
78 maintainers = lib.teams.gnome.members;
79 };
80
81 ###### interface
82
83 options = {
84
85 services.xserver.displayManager.gdm = {
86
87 enable = lib.mkEnableOption "GDM, the GNOME Display Manager";
88
89 debug = lib.mkEnableOption "debugging messages in GDM";
90
91 # Auto login options specific to GDM
92 autoLogin.delay = lib.mkOption {
93 type = lib.types.int;
94 default = 0;
95 description = ''
96 Seconds of inactivity after which the autologin will be performed.
97 '';
98 };
99
100 wayland = lib.mkOption {
101 type = lib.types.bool;
102 default = true;
103 description = ''
104 Allow GDM to run on Wayland instead of Xserver.
105 '';
106 };
107
108 autoSuspend = lib.mkOption {
109 default = true;
110 description = ''
111 On the GNOME Display Manager login screen, suspend the machine after inactivity.
112 (Does not affect automatic suspend while logged in, or at lock screen.)
113 '';
114 type = lib.types.bool;
115 };
116
117 banner = lib.mkOption {
118 type = lib.types.nullOr lib.types.lines;
119 default = null;
120 example = ''
121 foo
122 bar
123 baz
124 '';
125 description = ''
126 Optional message to display on the login screen.
127 '';
128 };
129
130 settings = lib.mkOption {
131 type = settingsFormat.type;
132 default = { };
133 example = {
134 debug.enable = true;
135 };
136 description = ''
137 Options passed to the gdm daemon.
138 See [here](https://help.gnome.org/admin/gdm/stable/configuration.html.en#daemonconfig) for supported options.
139 '';
140 };
141
142 };
143
144 };
145
146 ###### implementation
147
148 config = lib.mkIf cfg.gdm.enable {
149
150 services.xserver.displayManager.lightdm.enable = false;
151
152 users.users.gdm = {
153 name = "gdm";
154 uid = config.ids.uids.gdm;
155 group = "gdm";
156 home = "/run/gdm";
157 description = "GDM user";
158 };
159
160 users.groups.gdm.gid = config.ids.gids.gdm;
161
162 # GDM needs different xserverArgs, presumable because using wayland by default.
163 services.xserver.tty = null;
164 services.xserver.display = null;
165 services.xserver.verbose = null;
166
167 services.displayManager = {
168 # Enable desktop session data
169 enable = true;
170
171 environment =
172 {
173 GDM_X_SERVER_EXTRA_ARGS = toString (lib.filter (arg: arg != "-terminate") cfg.xserverArgs);
174 XDG_DATA_DIRS = lib.makeSearchPath "share" [
175 gdm # for gnome-login.session
176 config.services.displayManager.sessionData.desktops
177 pkgs.gnome-control-center # for accessibility icon
178 pkgs.adwaita-icon-theme
179 pkgs.hicolor-icon-theme # empty icon theme as a base
180 ];
181 }
182 // lib.optionalAttrs (xSessionWrapper != null) {
183 # Make GDM use this wrapper before running the session, which runs the
184 # configured setupCommands. This relies on a patched GDM which supports
185 # this environment variable.
186 GDM_X_SESSION_WRAPPER = "${xSessionWrapper}";
187 };
188 execCmd = "exec ${gdm}/bin/gdm";
189 preStart = lib.optionalString (defaultSessionName != null) ''
190 # Set default session in session chooser to a specified values – basically ignore session history.
191 ${setSessionScript}/bin/set-session ${config.services.displayManager.sessionData.autologinSession}
192 '';
193 };
194
195 systemd.tmpfiles.rules =
196 [
197 "d /run/gdm/.config 0711 gdm gdm"
198 ]
199 ++ lib.optionals config.services.pulseaudio.enable [
200 "d /run/gdm/.config/pulse 0711 gdm gdm"
201 "L+ /run/gdm/.config/pulse/${pulseConfig.name} - - - - ${pulseConfig}"
202 ]
203 ++ lib.optionals config.services.gnome.gnome-initial-setup.enable [
204 # Create stamp file for gnome-initial-setup to prevent it starting in GDM.
205 "f /run/gdm/.config/gnome-initial-setup-done 0711 gdm gdm - yes"
206 ];
207
208 # Otherwise GDM will not be able to start correctly and display Wayland sessions
209 systemd.packages = [
210 gdm
211 pkgs.gnome-session
212 pkgs.gnome-shell
213 ];
214 environment.systemPackages = [
215 pkgs.adwaita-icon-theme
216 pkgs.gdm # For polkit rules
217 ];
218
219 # We dont use the upstream gdm service
220 # it has to be disabled since the gdm package has it
221 # https://github.com/NixOS/nixpkgs/issues/108672
222 systemd.services.gdm.enable = false;
223
224 systemd.services.display-manager.wants = [
225 # Because sd_login_monitor_new requires /run/systemd/machines
226 "systemd-machined.service"
227 # setSessionScript wants AccountsService
228 "accounts-daemon.service"
229 ];
230
231 systemd.services.display-manager.after = [
232 "rc-local.service"
233 "systemd-machined.service"
234 "systemd-user-sessions.service"
235 "getty@tty${gdm.initialVT}.service"
236 "plymouth-quit.service"
237 "plymouth-start.service"
238 ];
239 systemd.services.display-manager.conflicts = [
240 "getty@tty${gdm.initialVT}.service"
241 "plymouth-quit.service"
242 ];
243 systemd.services.display-manager.onFailure = [
244 "plymouth-quit.service"
245 ];
246
247 # Prevent nixos-rebuild switch from bringing down the graphical
248 # session. (If multi-user.target wants plymouth-quit.service which
249 # conflicts display-manager.service, then when nixos-rebuild
250 # switch starts multi-user.target, display-manager.service is
251 # stopped so plymouth-quit.service can be started.)
252 systemd.services.plymouth-quit = lib.mkIf config.boot.plymouth.enable {
253 wantedBy = lib.mkForce [ ];
254 };
255
256 systemd.services.display-manager.serviceConfig = {
257 # Restart = "always"; - already defined in xserver.nix
258 KillMode = "mixed";
259 IgnoreSIGPIPE = "no";
260 BusName = "org.gnome.DisplayManager";
261 StandardError = "inherit";
262 ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
263 KeyringMode = "shared";
264 EnvironmentFile = "-/etc/locale.conf";
265 };
266
267 systemd.services.display-manager.path = [ pkgs.gnome-session ];
268
269 # Allow choosing an user account
270 services.accounts-daemon.enable = true;
271
272 services.dbus.packages = [ gdm ];
273
274 systemd.user.services.dbus.wantedBy = [ "default.target" ];
275
276 programs.dconf.profiles.gdm.databases =
277 lib.optionals (!cfg.gdm.autoSuspend) [
278 {
279 settings."org/gnome/settings-daemon/plugins/power" = {
280 sleep-inactive-ac-type = "nothing";
281 sleep-inactive-battery-type = "nothing";
282 sleep-inactive-ac-timeout = lib.gvariant.mkInt32 0;
283 sleep-inactive-battery-timeout = lib.gvariant.mkInt32 0;
284 };
285 }
286 ]
287 ++ lib.optionals (cfg.gdm.banner != null) [
288 {
289 settings."org/gnome/login-screen" = {
290 banner-message-enable = true;
291 banner-message-text = cfg.gdm.banner;
292 };
293 }
294 ]
295 ++ [ "${gdm}/share/gdm/greeter-dconf-defaults" ];
296
297 # Use AutomaticLogin if delay is zero, because it's immediate.
298 # Otherwise with TimedLogin with zero seconds the prompt is still
299 # presented and there's a little delay.
300 services.xserver.displayManager.gdm.settings = {
301 daemon = lib.mkMerge [
302 { WaylandEnable = cfg.gdm.wayland; }
303 # nested if else didn't work
304 (lib.mkIf (config.services.displayManager.autoLogin.enable && cfg.gdm.autoLogin.delay != 0) {
305 TimedLoginEnable = true;
306 TimedLogin = config.services.displayManager.autoLogin.user;
307 TimedLoginDelay = cfg.gdm.autoLogin.delay;
308 })
309 (lib.mkIf (config.services.displayManager.autoLogin.enable && cfg.gdm.autoLogin.delay == 0) {
310 AutomaticLoginEnable = true;
311 AutomaticLogin = config.services.displayManager.autoLogin.user;
312 })
313 ];
314 debug = lib.mkIf cfg.gdm.debug {
315 Enable = true;
316 };
317 };
318
319 environment.etc."gdm/custom.conf".source = configFile;
320
321 environment.etc."gdm/Xsession".source = config.services.displayManager.sessionData.wrapper;
322
323 # GDM LFS PAM modules, adapted somehow to NixOS
324 security.pam.services = {
325 gdm-launch-environment.text = ''
326 auth required pam_succeed_if.so audit quiet_success user = gdm
327 auth optional pam_permit.so
328
329 account required pam_succeed_if.so audit quiet_success user = gdm
330 account sufficient pam_unix.so
331
332 password required pam_deny.so
333
334 session required pam_succeed_if.so audit quiet_success user = gdm
335 session required pam_env.so conffile=/etc/pam/environment readenv=0
336 session optional ${config.systemd.package}/lib/security/pam_systemd.so
337 session optional pam_keyinit.so force revoke
338 session optional pam_permit.so
339 '';
340
341 gdm-password.text = ''
342 auth substack login
343 account include login
344 password substack login
345 session include login
346 '';
347
348 gdm-autologin.text = ''
349 auth requisite pam_nologin.so
350 auth required pam_succeed_if.so uid >= 1000 quiet
351 ${lib.optionalString (pamLogin.enable && pamLogin.enableGnomeKeyring) ''
352 auth [success=ok default=1] ${gdm}/lib/security/pam_gdm.so
353 auth optional ${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so
354 ''}
355 auth required pam_permit.so
356
357 account sufficient pam_unix.so
358
359 password requisite pam_unix.so nullok yescrypt
360
361 session optional pam_keyinit.so revoke
362 session include login
363 '';
364
365 # This would block password prompt when included by gdm-password.
366 # GDM will instead run gdm-fingerprint in parallel.
367 login.fprintAuth = lib.mkIf config.services.fprintd.enable false;
368
369 gdm-fingerprint.text = lib.mkIf config.services.fprintd.enable ''
370 auth required pam_shells.so
371 auth requisite pam_nologin.so
372 auth requisite pam_faillock.so preauth
373 auth required ${pkgs.fprintd}/lib/security/pam_fprintd.so
374 auth required pam_env.so
375 ${lib.optionalString (pamLogin.enable && pamLogin.enableGnomeKeyring) ''
376 auth [success=ok default=1] ${gdm}/lib/security/pam_gdm.so
377 auth optional ${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so
378 ''}
379
380 account include login
381
382 password required pam_deny.so
383
384 session include login
385 '';
386 };
387
388 };
389
390}