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