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