1{ lib, options, config, utils, pkgs, ... }:
2
3with lib;
4
5let
6 inherit (utils) systemdUtils escapeSystemdPath;
7 inherit (systemdUtils.lib)
8 generateUnits
9 pathToUnit
10 serviceToUnit
11 sliceToUnit
12 socketToUnit
13 targetToUnit
14 timerToUnit
15 mountToUnit
16 automountToUnit;
17
18
19 cfg = config.boot.initrd.systemd;
20
21 upstreamUnits = [
22 "basic.target"
23 "ctrl-alt-del.target"
24 "debug-shell.service"
25 "emergency.service"
26 "emergency.target"
27 "final.target"
28 "halt.target"
29 "initrd-cleanup.service"
30 "initrd-fs.target"
31 "initrd-parse-etc.service"
32 "initrd-root-device.target"
33 "initrd-root-fs.target"
34 "initrd-switch-root.service"
35 "initrd-switch-root.target"
36 "initrd.target"
37 "kexec.target"
38 "kmod-static-nodes.service"
39 "local-fs-pre.target"
40 "local-fs.target"
41 "multi-user.target"
42 "paths.target"
43 "poweroff.target"
44 "reboot.target"
45 "rescue.service"
46 "rescue.target"
47 "rpcbind.target"
48 "shutdown.target"
49 "sigpwr.target"
50 "slices.target"
51 "sockets.target"
52 "swap.target"
53 "sysinit.target"
54 "sys-kernel-config.mount"
55 "syslog.socket"
56 "systemd-ask-password-console.path"
57 "systemd-ask-password-console.service"
58 "systemd-fsck@.service"
59 "systemd-halt.service"
60 "systemd-hibernate-resume.service"
61 "systemd-journald-audit.socket"
62 "systemd-journald-dev-log.socket"
63 "systemd-journald.service"
64 "systemd-journald.socket"
65 "systemd-kexec.service"
66 "systemd-modules-load.service"
67 "systemd-poweroff.service"
68 "systemd-reboot.service"
69 "systemd-sysctl.service"
70 "systemd-tmpfiles-setup-dev.service"
71 "systemd-tmpfiles-setup.service"
72 "timers.target"
73 "umount.target"
74 "systemd-bsod.service"
75 ] ++ cfg.additionalUpstreamUnits;
76
77 upstreamWants = [
78 "sysinit.target.wants"
79 ];
80
81 enabledUpstreamUnits = filter (n: ! elem n cfg.suppressedUnits) upstreamUnits;
82 enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedUnits) cfg.units;
83 jobScripts = concatLists (mapAttrsToList (_: unit: unit.jobScripts or []) (filterAttrs (_: v: v.enable) cfg.services));
84
85 stage1Units = generateUnits {
86 type = "initrd";
87 units = enabledUnits;
88 upstreamUnits = enabledUpstreamUnits;
89 inherit upstreamWants;
90 inherit (cfg) packages package;
91 };
92
93 kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
94 # Determine the set of modules that we need to mount the root FS.
95 modulesClosure = pkgs.makeModulesClosure {
96 rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules;
97 kernel = config.system.modulesTree;
98 firmware = config.hardware.firmware;
99 allowMissing = false;
100 };
101
102 initrdBinEnv = pkgs.buildEnv {
103 name = "initrd-bin-env";
104 paths = map getBin cfg.initrdBin;
105 pathsToLink = ["/bin" "/sbin"];
106 postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -sf '${v}' $out/bin/'${n}'") cfg.extraBin);
107 };
108
109 initialRamdisk = pkgs.makeInitrdNG {
110 name = "initrd-${kernel-name}";
111 inherit (config.boot.initrd) compressor compressorArgs prepend;
112 inherit (cfg) strip;
113
114 contents = map (path: { object = path; symlink = ""; }) (subtractLists cfg.suppressedStorePaths cfg.storePaths)
115 ++ mapAttrsToList (_: v: { object = v.source; symlink = v.target; }) (filterAttrs (_: v: v.enable) cfg.contents);
116 };
117
118in {
119 options.boot.initrd.systemd = {
120 enable = mkEnableOption "systemd in initrd" // {
121 description = ''
122 Whether to enable systemd in initrd. The unit options such as
123 {option}`boot.initrd.systemd.services` are the same as their
124 stage 2 counterparts such as {option}`systemd.services`,
125 except that `restartTriggers` and `reloadTriggers` are not
126 supported.
127 '';
128 };
129
130 package = lib.mkOption {
131 type = lib.types.package;
132 default = config.systemd.package;
133 defaultText = lib.literalExpression "config.systemd.package";
134 description = ''
135 The systemd package to use.
136 '';
137 };
138
139 extraConfig = mkOption {
140 default = "";
141 type = types.lines;
142 example = "DefaultLimitCORE=infinity";
143 description = ''
144 Extra config options for systemd. See systemd-system.conf(5) man page
145 for available options.
146 '';
147 };
148
149 managerEnvironment = mkOption {
150 type = with types; attrsOf (nullOr (oneOf [ str path package ]));
151 default = {};
152 example = { SYSTEMD_LOG_LEVEL = "debug"; };
153 description = ''
154 Environment variables of PID 1. These variables are
155 *not* passed to started units.
156 '';
157 };
158
159 contents = mkOption {
160 description = "Set of files that have to be linked into the initrd";
161 example = literalExpression ''
162 {
163 "/etc/hostname".text = "mymachine";
164 }
165 '';
166 default = {};
167 type = utils.systemdUtils.types.initrdContents;
168 };
169
170 storePaths = mkOption {
171 description = ''
172 Store paths to copy into the initrd as well.
173 '';
174 type = with types; listOf (oneOf [ singleLineStr package ]);
175 default = [];
176 };
177
178 strip = mkOption {
179 description = ''
180 Whether to completely strip executables and libraries copied to the initramfs.
181
182 Setting this to false may save on the order of 30MiB on the
183 machine building the system (by avoiding a binutils
184 reference), at the cost of ~1MiB of initramfs size. This puts
185 this option firmly in the territory of micro-optimisation.
186 '';
187 type = types.bool;
188 default = true;
189 };
190
191 extraBin = mkOption {
192 description = ''
193 Tools to add to /bin
194 '';
195 example = literalExpression ''
196 {
197 umount = ''${pkgs.util-linux}/bin/umount;
198 }
199 '';
200 type = types.attrsOf types.path;
201 default = {};
202 };
203
204 suppressedStorePaths = mkOption {
205 description = ''
206 Store paths specified in the storePaths option that
207 should not be copied.
208 '';
209 type = types.listOf types.singleLineStr;
210 default = [];
211 };
212
213 root = lib.mkOption {
214 type = lib.types.enum [ "fstab" "gpt-auto" ];
215 default = "fstab";
216 example = "gpt-auto";
217 description = ''
218 Controls how systemd will interpret the root FS in initrd. See
219 {manpage}`kernel-command-line(7)`. NixOS currently does not
220 allow specifying the root file system itself this
221 way. Instead, the `fstab` value is used in order to interpret
222 the root file system specified with the `fileSystems` option.
223 '';
224 };
225
226 emergencyAccess = mkOption {
227 type = with types; oneOf [ bool (nullOr (passwdEntry str)) ];
228 description = ''
229 Set to true for unauthenticated emergency access, and false for
230 no emergency access.
231
232 Can also be set to a hashed super user password to allow
233 authenticated access to the emergency mode.
234 '';
235 default = false;
236 };
237
238 initrdBin = mkOption {
239 type = types.listOf types.package;
240 default = [];
241 description = ''
242 Packages to include in /bin for the stage 1 emergency shell.
243 '';
244 };
245
246 additionalUpstreamUnits = mkOption {
247 default = [ ];
248 type = types.listOf types.str;
249 example = [ "debug-shell.service" "systemd-quotacheck.service" ];
250 description = ''
251 Additional units shipped with systemd that shall be enabled.
252 '';
253 };
254
255 suppressedUnits = mkOption {
256 default = [ ];
257 type = types.listOf types.str;
258 example = [ "systemd-backlight@.service" ];
259 description = ''
260 A list of units to skip when generating system systemd configuration directory. This has
261 priority over upstream units, {option}`boot.initrd.systemd.units`, and
262 {option}`boot.initrd.systemd.additionalUpstreamUnits`. The main purpose of this is to
263 prevent a upstream systemd unit from being added to the initrd with any modifications made to it
264 by other NixOS modules.
265 '';
266 };
267
268 units = mkOption {
269 description = "Definition of systemd units.";
270 default = {};
271 visible = "shallow";
272 type = systemdUtils.types.units;
273 };
274
275 packages = mkOption {
276 default = [];
277 type = types.listOf types.package;
278 example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
279 description = "Packages providing systemd units and hooks.";
280 };
281
282 targets = mkOption {
283 default = {};
284 visible = "shallow";
285 type = systemdUtils.types.initrdTargets;
286 description = "Definition of systemd target units.";
287 };
288
289 services = mkOption {
290 default = {};
291 type = systemdUtils.types.initrdServices;
292 visible = "shallow";
293 description = "Definition of systemd service units.";
294 };
295
296 sockets = mkOption {
297 default = {};
298 type = systemdUtils.types.initrdSockets;
299 visible = "shallow";
300 description = "Definition of systemd socket units.";
301 };
302
303 timers = mkOption {
304 default = {};
305 type = systemdUtils.types.initrdTimers;
306 visible = "shallow";
307 description = "Definition of systemd timer units.";
308 };
309
310 paths = mkOption {
311 default = {};
312 type = systemdUtils.types.initrdPaths;
313 visible = "shallow";
314 description = "Definition of systemd path units.";
315 };
316
317 mounts = mkOption {
318 default = [];
319 type = systemdUtils.types.initrdMounts;
320 visible = "shallow";
321 description = ''
322 Definition of systemd mount units.
323 This is a list instead of an attrSet, because systemd mandates the names to be derived from
324 the 'where' attribute.
325 '';
326 };
327
328 automounts = mkOption {
329 default = [];
330 type = systemdUtils.types.automounts;
331 visible = "shallow";
332 description = ''
333 Definition of systemd automount units.
334 This is a list instead of an attrSet, because systemd mandates the names to be derived from
335 the 'where' attribute.
336 '';
337 };
338
339 slices = mkOption {
340 default = {};
341 type = systemdUtils.types.slices;
342 visible = "shallow";
343 description = "Definition of slice configurations.";
344 };
345
346 enableTpm2 = mkOption {
347 default = true;
348 type = types.bool;
349 description = ''
350 Whether to enable TPM2 support in the initrd.
351 '';
352 };
353 };
354
355 config = mkIf (config.boot.initrd.enable && cfg.enable) {
356 assertions = [
357 {
358 assertion = cfg.root == "fstab" -> any (fs: fs.mountPoint == "/") (builtins.attrValues config.fileSystems);
359 message = "The ‘fileSystems’ option does not specify your root file system.";
360 }
361 ] ++ map (name: {
362 assertion = lib.attrByPath name (throw "impossible") config.boot.initrd == "";
363 message = ''
364 systemd stage 1 does not support 'boot.initrd.${lib.concatStringsSep "." name}'. Please
365 convert it to analogous systemd units in 'boot.initrd.systemd'.
366
367 Definitions:
368 ${lib.concatMapStringsSep "\n" ({ file, ... }: " - ${file}") (lib.attrByPath name (throw "impossible") options.boot.initrd).definitionsWithLocations}
369 '';
370 }) [
371 [ "preFailCommands" ]
372 [ "preDeviceCommands" ]
373 [ "preLVMCommands" ]
374 [ "postDeviceCommands" ]
375 [ "postResumeCommands" ]
376 [ "postMountCommands" ]
377 [ "extraUdevRulesCommands" ]
378 [ "extraUtilsCommands" ]
379 [ "extraUtilsCommandsTest" ]
380 [ "network" "postCommands" ]
381 ];
382
383 system.build = { inherit initialRamdisk; };
384
385 boot.initrd.availableKernelModules = [
386 # systemd needs this for some features
387 "autofs"
388 # systemd-cryptenroll
389 ] ++ lib.optional cfg.enableTpm2 "tpm-tis"
390 ++ lib.optional (cfg.enableTpm2 && !(pkgs.stdenv.hostPlatform.isRiscV64 || pkgs.stdenv.hostPlatform.isArmv7)) "tpm-crb"
391 ++ lib.optional cfg.package.withEfi "efivarfs";
392
393 boot.kernelParams = [
394 "root=${config.boot.initrd.systemd.root}"
395 ] ++ lib.optional (config.boot.resumeDevice != "") "resume=${config.boot.resumeDevice}"
396 # `systemd` mounts root in initrd as read-only unless "rw" is on the kernel command line.
397 # For NixOS activation to succeed, we need to have root writable in initrd.
398 ++ lib.optional (config.boot.initrd.systemd.root == "gpt-auto") "rw";
399
400 boot.initrd.systemd = {
401 # bashInteractive is easier to use and also required by debug-shell.service
402 initrdBin = [pkgs.bashInteractive pkgs.coreutils cfg.package.kmod cfg.package];
403 extraBin = {
404 less = "${pkgs.less}/bin/less";
405 mount = "${cfg.package.util-linux}/bin/mount";
406 umount = "${cfg.package.util-linux}/bin/umount";
407 fsck = "${cfg.package.util-linux}/bin/fsck";
408 };
409
410 managerEnvironment.PATH = "/bin:/sbin";
411
412 contents = {
413 "/tmp/.keep".text = "systemd requires the /tmp mount point in the initrd cpio archive";
414 "/init".source = "${cfg.package}/lib/systemd/systemd";
415 "/etc/systemd/system".source = stage1Units;
416
417 "/etc/systemd/system.conf".text = ''
418 [Manager]
419 DefaultEnvironment=PATH=/bin:/sbin
420 ${cfg.extraConfig}
421 ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
422 '';
423
424 "/lib".source = "${modulesClosure}/lib";
425
426 "/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules;
427
428 # We can use either ! or * to lock the root account in the
429 # console, but some software like OpenSSH won't even allow you
430 # to log in with an SSH key if you use ! so we use * instead
431 "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then optionalString (!cfg.emergencyAccess) "*" else cfg.emergencyAccess}:::::::";
432
433 "/bin".source = "${initrdBinEnv}/bin";
434 "/sbin".source = "${initrdBinEnv}/sbin";
435
436 "/etc/sysctl.d/nixos.conf".text = "kernel.modprobe = /sbin/modprobe";
437 "/etc/modprobe.d/systemd.conf".source = "${cfg.package}/lib/modprobe.d/systemd.conf";
438 "/etc/modprobe.d/ubuntu.conf".source = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" { } ''
439 ${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
440 '';
441 "/etc/modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
442
443 "/etc/os-release".source = config.boot.initrd.osRelease;
444 "/etc/initrd-release".source = config.boot.initrd.osRelease;
445
446 } // optionalAttrs (config.environment.etc ? "modprobe.d/nixos.conf") {
447 "/etc/modprobe.d/nixos.conf".source = config.environment.etc."modprobe.d/nixos.conf".source;
448 };
449
450 storePaths = [
451 # systemd tooling
452 "${cfg.package}/lib/systemd/systemd-executor"
453 "${cfg.package}/lib/systemd/systemd-fsck"
454 "${cfg.package}/lib/systemd/systemd-hibernate-resume"
455 "${cfg.package}/lib/systemd/systemd-journald"
456 "${cfg.package}/lib/systemd/systemd-makefs"
457 "${cfg.package}/lib/systemd/systemd-modules-load"
458 "${cfg.package}/lib/systemd/systemd-remount-fs"
459 "${cfg.package}/lib/systemd/systemd-shutdown"
460 "${cfg.package}/lib/systemd/systemd-sulogin-shell"
461 "${cfg.package}/lib/systemd/systemd-sysctl"
462 "${cfg.package}/lib/systemd/systemd-bsod"
463
464 # generators
465 "${cfg.package}/lib/systemd/system-generators/systemd-debug-generator"
466 "${cfg.package}/lib/systemd/system-generators/systemd-fstab-generator"
467 "${cfg.package}/lib/systemd/system-generators/systemd-gpt-auto-generator"
468 "${cfg.package}/lib/systemd/system-generators/systemd-hibernate-resume-generator"
469 "${cfg.package}/lib/systemd/system-generators/systemd-run-generator"
470
471 # utilities needed by systemd
472 "${cfg.package.util-linux}/bin/mount"
473 "${cfg.package.util-linux}/bin/umount"
474 "${cfg.package.util-linux}/bin/sulogin"
475
476 # required for script services, and some tools like xfs still want the sh symlink
477 "${pkgs.bash}/bin"
478
479 # so NSS can look up usernames
480 "${pkgs.glibc}/lib/libnss_files.so.2"
481 ] ++ optionals (cfg.package.withCryptsetup && cfg.enableTpm2) [
482 # tpm2 support
483 "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so"
484 pkgs.tpm2-tss
485 ] ++ optionals cfg.package.withCryptsetup [
486 # fido2 support
487 "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
488 "${pkgs.libfido2}/lib/libfido2.so.1"
489 ] ++ jobScripts;
490
491 targets.initrd.aliases = ["default.target"];
492 units =
493 mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit v)) cfg.paths
494 // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit v)) cfg.services
495 // mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit v)) cfg.slices
496 // mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit v)) cfg.sockets
497 // mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit v)) cfg.targets
498 // mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit v)) cfg.timers
499 // listToAttrs (map
500 (v: let n = escapeSystemdPath v.where;
501 in nameValuePair "${n}.mount" (mountToUnit v)) cfg.mounts)
502 // listToAttrs (map
503 (v: let n = escapeSystemdPath v.where;
504 in nameValuePair "${n}.automount" (automountToUnit v)) cfg.automounts);
505
506 # make sure all the /dev nodes are set up
507 services.systemd-tmpfiles-setup-dev.wantedBy = ["sysinit.target"];
508
509 services.initrd-nixos-activation = {
510 after = [ "initrd-fs.target" ];
511 requiredBy = [ "initrd.target" ];
512 unitConfig.AssertPathExists = "/etc/initrd-release";
513 serviceConfig.Type = "oneshot";
514 description = "NixOS Activation";
515
516 script = /* bash */ ''
517 set -uo pipefail
518 export PATH="/bin:${cfg.package.util-linux}/bin"
519
520 # Figure out what closure to boot
521 closure=
522 for o in $(< /proc/cmdline); do
523 case $o in
524 init=*)
525 IFS== read -r -a initParam <<< "$o"
526 closure="''${initParam[1]}"
527 ;;
528 esac
529 done
530
531 # Sanity check
532 if [ -z "''${closure:-}" ]; then
533 echo 'No init= parameter on the kernel command line' >&2
534 exit 1
535 fi
536
537 # Resolve symlinks in the init parameter. We need this for some boot loaders
538 # (e.g. boot.loader.generationsDir).
539 closure="$(chroot /sysroot ${pkgs.coreutils}/bin/realpath "$closure")"
540
541 # Assume the directory containing the init script is the closure.
542 closure="$(dirname "$closure")"
543
544 # If we are not booting a NixOS closure (e.g. init=/bin/sh),
545 # we don't know what root to prepare so we don't do anything
546 if ! [ -x "/sysroot$(readlink "/sysroot$closure/prepare-root" || echo "$closure/prepare-root")" ]; then
547 echo "NEW_INIT=''${initParam[1]}" > /etc/switch-root.conf
548 echo "$closure does not look like a NixOS installation - not activating"
549 exit 0
550 fi
551 echo 'NEW_INIT=' > /etc/switch-root.conf
552
553
554 # We need to propagate /run for things like /run/booted-system
555 # and /run/current-system.
556 mkdir -p /sysroot/run
557 mount --bind /run /sysroot/run
558
559 # Initialize the system
560 export IN_NIXOS_SYSTEMD_STAGE1=true
561 exec chroot /sysroot $closure/prepare-root
562 '';
563 };
564
565 # This will either call systemctl with the new init as the last parameter (which
566 # is the case when not booting a NixOS system) or with an empty string, causing
567 # systemd to bypass its verification code that checks whether the next file is a systemd
568 # and using its compiled-in value
569 services.initrd-switch-root.serviceConfig = {
570 EnvironmentFile = "-/etc/switch-root.conf";
571 ExecStart = [
572 ""
573 ''systemctl --no-block switch-root /sysroot "''${NEW_INIT}"''
574 ];
575 };
576
577 services.panic-on-fail = {
578 wantedBy = ["emergency.target"];
579 unitConfig = {
580 DefaultDependencies = false;
581 ConditionKernelCommandLine = [
582 "|boot.panic_on_fail"
583 "|stage1panic"
584 ];
585 };
586 script = ''
587 echo c > /proc/sysrq-trigger
588 '';
589 serviceConfig.Type = "oneshot";
590 };
591 };
592 };
593}