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