diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix index a472d97f5cc748..589f1e2c718546 100644 --- a/nixos/lib/systemd-lib.nix +++ b/nixos/lib/systemd-lib.nix @@ -120,10 +120,15 @@ in rec { (if isList value then value else [value])) as)); - generateUnits = generateUnits' true; - - generateUnits' = allowCollisions: type: units: upstreamUnits: upstreamWants: - pkgs.runCommand "${type}-units" + generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }: + let + typeDir = ({ + system = "system"; + initrd = "system"; + user = "user"; + nspawn = "nspawn"; + }).${type}; + in pkgs.runCommand "${type}-units" { preferLocalBuild = true; allowSubstitutes = false; } '' @@ -131,7 +136,7 @@ in rec { # Copy the upstream systemd units we're interested in. for i in ${toString upstreamUnits}; do - fn=${cfg.package}/example/systemd/${type}/$i + fn=${package}/example/systemd/${typeDir}/$i if ! [ -e $fn ]; then echo "missing $fn"; false; fi if [ -L $fn ]; then target="$(readlink "$fn")" @@ -148,7 +153,7 @@ in rec { # Copy .wants links, but only those that point to units that # we're interested in. for i in ${toString upstreamWants}; do - fn=${cfg.package}/example/systemd/${type}/$i + fn=${package}/example/systemd/${typeDir}/$i if ! [ -e $fn ]; then echo "missing $fn"; false; fi x=$out/$(basename $fn) mkdir $x @@ -160,14 +165,14 @@ in rec { done # Symlink all units provided listed in systemd.packages. - packages="${toString cfg.packages}" + packages="${toString packages}" # Filter duplicate directories declare -A unique_packages for k in $packages ; do unique_packages[$k]=1 ; done for i in ''${!unique_packages[@]}; do - for fn in $i/etc/systemd/${type}/* $i/lib/systemd/${type}/*; do + for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do if ! [[ "$fn" =~ .wants$ ]]; then if [[ -d "$fn" ]]; then targetDir="$out/$(basename "$fn")" @@ -268,9 +273,9 @@ in rec { { Conflicts = toString config.conflicts; } // optionalAttrs (config.requisite != []) { Requisite = toString config.requisite; } - // optionalAttrs (config.restartTriggers != []) + // optionalAttrs (config ? restartTriggers && config.restartTriggers != []) { X-Restart-Triggers = toString config.restartTriggers; } - // optionalAttrs (config.reloadTriggers != []) + // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != []) { X-Reload-Triggers = toString config.reloadTriggers; } // optionalAttrs (config.description != "") { Description = config.description; } @@ -286,45 +291,24 @@ in rec { }; }; - serviceConfig = { name, config, ... }: { - config = mkMerge - [ { # Default path for systemd services. Should be quite minimal. - path = mkAfter - [ pkgs.coreutils - pkgs.findutils - pkgs.gnugrep - pkgs.gnused - systemd - ]; - environment.PATH = "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}"; - } - (mkIf (config.preStart != "") - { serviceConfig.ExecStartPre = - [ (makeJobScript "${name}-pre-start" config.preStart) ]; - }) - (mkIf (config.script != "") - { serviceConfig.ExecStart = - makeJobScript "${name}-start" config.script + " " + config.scriptArgs; - }) - (mkIf (config.postStart != "") - { serviceConfig.ExecStartPost = - [ (makeJobScript "${name}-post-start" config.postStart) ]; - }) - (mkIf (config.reload != "") - { serviceConfig.ExecReload = - makeJobScript "${name}-reload" config.reload; - }) - (mkIf (config.preStop != "") - { serviceConfig.ExecStop = - makeJobScript "${name}-pre-stop" config.preStop; - }) - (mkIf (config.postStop != "") - { serviceConfig.ExecStopPost = - makeJobScript "${name}-post-stop" config.postStop; - }) - ]; + serviceConfig = { config, ... }: { + config.environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}"; }; + stage2ServiceConfig = { + imports = [ serviceConfig ]; + # Default path for systemd services. Should be quite minimal. + config.path = mkAfter [ + pkgs.coreutils + pkgs.findutils + pkgs.gnugrep + pkgs.gnused + systemd + ]; + }; + + stage1ServiceConfig = serviceConfig; + mountConfig = { config, ... }: { config = { mountConfig = @@ -372,12 +356,12 @@ in rec { # systemd max line length is now 1MiB # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)} - ${if def.reloadIfChanged then '' + ${if def ? reloadIfChanged && def.reloadIfChanged then '' X-ReloadIfChanged=true - '' else if !def.restartIfChanged then '' + '' else if (def ? restartIfChanged && !def.restartIfChanged) then '' X-RestartIfChanged=false '' else ""} - ${optionalString (!def.stopIfChanged) "X-StopIfChanged=false"} + ${optionalString (def ? stopIfChanged && !def.stopIfChanged) "X-StopIfChanged=false"} ${attrsToSection def.serviceConfig} ''; }; diff --git a/nixos/lib/systemd-types.nix b/nixos/lib/systemd-types.nix new file mode 100644 index 00000000000000..71962fab2e1779 --- /dev/null +++ b/nixos/lib/systemd-types.nix @@ -0,0 +1,37 @@ +{ lib, systemdUtils }: + +with systemdUtils.lib; +with systemdUtils.unitOptions; +with lib; + +rec { + units = with types; + attrsOf (submodule ({ name, config, ... }: { + options = concreteUnitOptions; + config = { unit = mkDefault (systemdUtils.lib.makeUnit name config); }; + })); + + services = with types; attrsOf (submodule [ stage2ServiceOptions unitConfig stage2ServiceConfig ]); + initrdServices = with types; attrsOf (submodule [ stage1ServiceOptions unitConfig stage1ServiceConfig ]); + + targets = with types; attrsOf (submodule [ stage2CommonUnitOptions unitConfig ]); + initrdTargets = with types; attrsOf (submodule [ stage1CommonUnitOptions unitConfig ]); + + sockets = with types; attrsOf (submodule [ stage2SocketOptions unitConfig ]); + initrdSockets = with types; attrsOf (submodule [ stage1SocketOptions unitConfig ]); + + timers = with types; attrsOf (submodule [ stage2TimerOptions unitConfig ]); + initrdTimers = with types; attrsOf (submodule [ stage1TimerOptions unitConfig ]); + + paths = with types; attrsOf (submodule [ stage2PathOptions unitConfig ]); + initrdPaths = with types; attrsOf (submodule [ stage1PathOptions unitConfig ]); + + slices = with types; attrsOf (submodule [ stage2SliceOptions unitConfig ]); + initrdSlices = with types; attrsOf (submodule [ stage1SliceOptions unitConfig ]); + + mounts = with types; listOf (submodule [ stage2MountOptions unitConfig mountConfig ]); + initrdMounts = with types; listOf (submodule [ stage1MountOptions unitConfig mountConfig ]); + + automounts = with types; listOf (submodule [ stage2AutomountOptions unitConfig automountConfig ]); + initrdAutomounts = with types; attrsOf (submodule [ stage1AutomountOptions unitConfig automountConfig ]); +} diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix index 8029ba0e3f6cf5..c9d424d3911966 100644 --- a/nixos/lib/systemd-unit-options.nix +++ b/nixos/lib/systemd-unit-options.nix @@ -94,7 +94,7 @@ in rec { }; - commonUnitOptions = sharedOptions // { + commonUnitOptions = { options = (sharedOptions // { description = mkOption { default = ""; @@ -191,27 +191,6 @@ in rec { ''; }; - restartTriggers = mkOption { - default = []; - type = types.listOf types.unspecified; - description = '' - An arbitrary list of items such as derivations. If any item - in the list changes between reconfigurations, the service will - be restarted. - ''; - }; - - reloadTriggers = mkOption { - default = []; - type = types.listOf unitOption; - description = '' - An arbitrary list of items such as derivations. If any item - in the list changes between reconfigurations, the service will - be reloaded. If anything but a reload trigger changes in the - unit file, the unit will be restarted instead. - ''; - }; - onFailure = mkOption { default = []; type = types.listOf unitNameType; @@ -239,10 +218,39 @@ in rec { ''; }; + }); }; + + stage2CommonUnitOptions = { + imports = [ + commonUnitOptions + ]; + + options = { + restartTriggers = mkOption { + default = []; + type = types.listOf types.unspecified; + description = '' + An arbitrary list of items such as derivations. If any item + in the list changes between reconfigurations, the service will + be restarted. + ''; + }; + + reloadTriggers = mkOption { + default = []; + type = types.listOf unitOption; + description = '' + An arbitrary list of items such as derivations. If any item + in the list changes between reconfigurations, the service will + be reloaded. If anything but a reload trigger changes in the + unit file, the unit will be restarted instead. + ''; + }; + }; }; + stage1CommonUnitOptions = commonUnitOptions; - - serviceOptions = commonUnitOptions // { + serviceOptions = { options = { environment = mkOption { default = {}; @@ -276,121 +284,164 @@ in rec { ''; }; - script = mkOption { - type = types.lines; - default = ""; - description = "Shell commands executed as the service's main process."; - }; - - scriptArgs = mkOption { - type = types.str; - default = ""; - description = "Arguments passed to the main process script."; - }; - - preStart = mkOption { - type = types.lines; - default = ""; - description = '' - Shell commands executed before the service's main process - is started. - ''; - }; - - postStart = mkOption { - type = types.lines; - default = ""; - description = '' - Shell commands executed after the service's main process - is started. - ''; - }; - - reload = mkOption { - type = types.lines; - default = ""; - description = '' - Shell commands executed when the service's main process - is reloaded. - ''; - }; - - preStop = mkOption { - type = types.lines; - default = ""; - description = '' - Shell commands executed to stop the service. - ''; - }; - - postStop = mkOption { - type = types.lines; - default = ""; - description = '' - Shell commands executed after the service's main process - has exited. - ''; - }; - - restartIfChanged = mkOption { - type = types.bool; - default = true; - description = '' - Whether the service should be restarted during a NixOS - configuration switch if its definition has changed. - ''; - }; - - reloadIfChanged = mkOption { - type = types.bool; - default = false; - description = '' - Whether the service should be reloaded during a NixOS - configuration switch if its definition has changed. If - enabled, the value of is - ignored. - - This option should not be used anymore in favor of - which allows more granular - control of when a service is reloaded and when a service - is restarted. - ''; - }; - - stopIfChanged = mkOption { - type = types.bool; - default = true; - description = '' - If set, a changed unit is restarted by calling - systemctl stop in the old configuration, - then systemctl start in the new one. - Otherwise, it is restarted in a single step using - systemctl restart in the new configuration. - The latter is less correct because it runs the - ExecStop commands from the new - configuration. - ''; - }; - - startAt = mkOption { - type = with types; either str (listOf str); - default = []; - example = "Sun 14:00:00"; - description = '' - Automatically start this unit at the given date/time, which - must be in the format described in - systemd.time - 7. This is equivalent - to adding a corresponding timer unit with - set to the value given here. - ''; - apply = v: if isList v then v else [ v ]; - }; + }; }; + + stage2ServiceOptions = { name, config, ... }: { + imports = [ + stage2CommonUnitOptions + serviceOptions + ]; + + options = { + script = mkOption { + type = types.lines; + default = ""; + description = "Shell commands executed as the service's main process."; + }; + + scriptArgs = mkOption { + type = types.str; + default = ""; + description = "Arguments passed to the main process script."; + }; + + preStart = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed before the service's main process + is started. + ''; + }; + + postStart = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed after the service's main process + is started. + ''; + }; + + reload = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed when the service's main process + is reloaded. + ''; + }; + + preStop = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed to stop the service. + ''; + }; + + postStop = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed after the service's main process + has exited. + ''; + }; + + restartIfChanged = mkOption { + type = types.bool; + default = true; + description = '' + Whether the service should be restarted during a NixOS + configuration switch if its definition has changed. + ''; + }; + + reloadIfChanged = mkOption { + type = types.bool; + default = false; + description = '' + Whether the service should be reloaded during a NixOS + configuration switch if its definition has changed. If + enabled, the value of is + ignored. + + This option should not be used anymore in favor of + which allows more granular + control of when a service is reloaded and when a service + is restarted. + ''; + }; + + stopIfChanged = mkOption { + type = types.bool; + default = true; + description = '' + If set, a changed unit is restarted by calling + systemctl stop in the old configuration, + then systemctl start in the new one. + Otherwise, it is restarted in a single step using + systemctl restart in the new configuration. + The latter is less correct because it runs the + ExecStop commands from the new + configuration. + ''; + }; + + startAt = mkOption { + type = with types; either str (listOf str); + default = []; + example = "Sun 14:00:00"; + description = '' + Automatically start this unit at the given date/time, which + must be in the format described in + systemd.time + 7. This is equivalent + to adding a corresponding timer unit with + set to the value given here. + ''; + apply = v: if isList v then v else [ v ]; + }; + }; + + config = mkMerge + [ (mkIf (config.preStart != "") + { serviceConfig.ExecStartPre = + [ (makeJobScript "${name}-pre-start" config.preStart) ]; + }) + (mkIf (config.script != "") + { serviceConfig.ExecStart = + makeJobScript "${name}-start" config.script + " " + config.scriptArgs; + }) + (mkIf (config.postStart != "") + { serviceConfig.ExecStartPost = + [ (makeJobScript "${name}-post-start" config.postStart) ]; + }) + (mkIf (config.reload != "") + { serviceConfig.ExecReload = + makeJobScript "${name}-reload" config.reload; + }) + (mkIf (config.preStop != "") + { serviceConfig.ExecStop = + makeJobScript "${name}-pre-stop" config.preStop; + }) + (mkIf (config.postStop != "") + { serviceConfig.ExecStopPost = + makeJobScript "${name}-post-stop" config.postStop; + }) + ]; + }; + stage1ServiceOptions = { + imports = [ + stage1CommonUnitOptions + serviceOptions + ]; }; - socketOptions = commonUnitOptions // { + socketOptions = { options = { listenStreams = mkOption { default = []; @@ -424,10 +475,24 @@ in rec { ''; }; + }; }; + + stage2SocketOptions = { + imports = [ + stage2CommonUnitOptions + socketOptions + ]; }; + stage1SocketOptions = { + imports = [ + stage1CommonUnitOptions + socketOptions + ]; + }; - timerOptions = commonUnitOptions // { + + timerOptions = { options = { timerConfig = mkOption { default = {}; @@ -443,10 +508,24 @@ in rec { ''; }; + }; }; + + stage2TimerOptions = { + imports = [ + stage2CommonUnitOptions + timerOptions + ]; }; + stage1TimerOptions = { + imports = [ + stage1CommonUnitOptions + timerOptions + ]; + }; - pathOptions = commonUnitOptions // { + + pathOptions = { options = { pathConfig = mkOption { default = {}; @@ -460,10 +539,24 @@ in rec { ''; }; + }; }; + + stage2PathOptions = { + imports = [ + stage2CommonUnitOptions + pathOptions + ]; + }; + + stage1PathOptions = { + imports = [ + stage1CommonUnitOptions + pathOptions + ]; }; - mountOptions = commonUnitOptions // { + mountOptions = { options = { what = mkOption { example = "/dev/sda1"; @@ -505,9 +598,23 @@ in rec { 5 for details. ''; }; + }; }; + + stage2MountOptions = { + imports = [ + stage2CommonUnitOptions + mountOptions + ]; + }; + + stage1MountOptions = { + imports = [ + stage1CommonUnitOptions + mountOptions + ]; }; - automountOptions = commonUnitOptions // { + automountOptions = { options = { where = mkOption { example = "/mnt"; @@ -529,11 +636,23 @@ in rec { 5 for details. ''; }; + }; }; + + stage2AutomountOptions = { + imports = [ + stage2CommonUnitOptions + automountOptions + ]; }; - targetOptions = commonUnitOptions; + stage1AutomountOptions = { + imports = [ + stage1CommonUnitOptions + automountOptions + ]; + }; - sliceOptions = commonUnitOptions // { + sliceOptions = { options = { sliceConfig = mkOption { default = {}; @@ -547,6 +666,20 @@ in rec { ''; }; + }; }; + + stage2SliceOptions = { + imports = [ + stage2CommonUnitOptions + sliceOptions + ]; + }; + + stage1SliceOptions = { + imports = [ + stage1CommonUnitOptions + sliceOptions + ]; }; } diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix index ae68c3920c5bb4..80341dd48fcd57 100644 --- a/nixos/lib/utils.nix +++ b/nixos/lib/utils.nix @@ -197,5 +197,6 @@ rec { systemdUtils = { lib = import ./systemd-lib.nix { inherit lib config pkgs; }; unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; }; + types = import ./systemd-types.nix { inherit lib systemdUtils; }; }; } diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix index 6c072021ed8340..5411bf3bc55e1c 100644 --- a/nixos/modules/misc/version.nix +++ b/nixos/modules/misc/version.nix @@ -11,6 +11,26 @@ let attrsToText = attrs: concatStringsSep "\n" (mapAttrsToList (n: v: ''${n}="${toString v}"'') attrs); + osReleaseContents = { + NAME = "NixOS"; + ID = "nixos"; + VERSION = "${cfg.release} (${cfg.codeName})"; + VERSION_CODENAME = toLower cfg.codeName; + VERSION_ID = cfg.release; + BUILD_ID = cfg.version; + PRETTY_NAME = "NixOS ${cfg.release} (${cfg.codeName})"; + LOGO = "nix-snowflake"; + HOME_URL = "https://nixos.org/"; + DOCUMENTATION_URL = "https://nixos.org/learn.html"; + SUPPORT_URL = "https://nixos.org/community.html"; + BUG_REPORT_URL = "https://github.com/NixOS/nixpkgs/issues"; + }; + + initrdReleaseContents = osReleaseContents // { + PRETTY_NAME = "${osReleaseContents.PRETTY_NAME} (Initrd)"; + }; + initrdRelease = pkgs.writeText "initrd-release" (attrsToText initrdReleaseContents); + in { imports = [ @@ -115,20 +135,12 @@ in DISTRIB_DESCRIPTION = "NixOS ${cfg.release} (${cfg.codeName})"; }; - "os-release".text = attrsToText { - NAME = "NixOS"; - ID = "nixos"; - VERSION = "${cfg.release} (${cfg.codeName})"; - VERSION_CODENAME = toLower cfg.codeName; - VERSION_ID = cfg.release; - BUILD_ID = cfg.version; - PRETTY_NAME = "NixOS ${cfg.release} (${cfg.codeName})"; - LOGO = "nix-snowflake"; - HOME_URL = "https://nixos.org/"; - DOCUMENTATION_URL = "https://nixos.org/learn.html"; - SUPPORT_URL = "https://nixos.org/community.html"; - BUG_REPORT_URL = "https://github.com/NixOS/nixpkgs/issues"; - }; + "os-release".text = attrsToText osReleaseContents; + }; + + boot.initrd.systemd.contents = { + "/etc/os-release".source = initrdRelease; + "/etc/initrd-release".source = initrdRelease; }; }; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 49d1105247abfb..2c1272ecd5a51e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1172,6 +1172,7 @@ ./system/boot/systemd/nspawn.nix ./system/boot/systemd/tmpfiles.nix ./system/boot/systemd/user.nix + ./system/boot/systemd/initrd.nix ./system/boot/timesyncd.nix ./system/boot/tmp.nix ./system/etc/etc-activation.nix diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix index 1575c0257d1c6b..aa2f4fb860b704 100644 --- a/nixos/modules/system/boot/stage-1.nix +++ b/nixos/modules/system/boot/stage-1.nix @@ -706,8 +706,12 @@ in } ]; - system.build = - { inherit bootStage1 initialRamdisk initialRamdiskSecretAppender extraUtils; }; + system.build = mkMerge [ + { inherit bootStage1 initialRamdiskSecretAppender extraUtils; } + + # generated in nixos/modules/system/boot/systemd/initrd.nix + (mkIf (!config.boot.initrd.systemd.enable) { inherit initialRamdisk; }) + ]; system.requiredKernelConfig = with config.lib.kernelConfig; [ (isYes "TMPFS") diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 057474c607ac85..a21b2bab91587a 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -11,14 +11,7 @@ let systemd = cfg.package; inherit (systemdUtils.lib) - makeUnit generateUnits - makeJobScript - unitConfig - serviceConfig - mountConfig - automountConfig - commonUnitText targetToUnit serviceToUnit socketToUnit @@ -185,13 +178,7 @@ in systemd.units = mkOption { description = "Definition of systemd units."; default = {}; - type = with types; attrsOf (submodule ( - { name, config, ... }: - { options = concreteUnitOptions; - config = { - unit = mkDefault (makeUnit name config); - }; - })); + type = systemdUtils.types.units; }; systemd.packages = mkOption { @@ -203,37 +190,37 @@ in systemd.targets = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = targetOptions; } unitConfig] ); + type = systemdUtils.types.targets; description = "Definition of systemd target units."; }; systemd.services = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = serviceOptions; } unitConfig serviceConfig ]); + type = systemdUtils.types.services; description = "Definition of systemd service units."; }; systemd.sockets = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = socketOptions; } unitConfig ]); + type = systemdUtils.types.sockets; description = "Definition of systemd socket units."; }; systemd.timers = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = timerOptions; } unitConfig ]); + type = systemdUtils.types.timers; description = "Definition of systemd timer units."; }; systemd.paths = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = pathOptions; } unitConfig ]); + type = systemdUtils.types.paths; description = "Definition of systemd path units."; }; systemd.mounts = mkOption { default = []; - type = with types; listOf (submodule [ { options = mountOptions; } unitConfig mountConfig ]); + type = systemdUtils.types.mounts; description = '' Definition of systemd mount units. This is a list instead of an attrSet, because systemd mandates the names to be derived from @@ -243,7 +230,7 @@ in systemd.automounts = mkOption { default = []; - type = with types; listOf (submodule [ { options = automountOptions; } unitConfig automountConfig ]); + type = systemdUtils.types.automounts; description = '' Definition of systemd automount units. This is a list instead of an attrSet, because systemd mandates the names to be derived from @@ -253,7 +240,7 @@ in systemd.slices = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = sliceOptions; } unitConfig] ); + type = systemdUtils.types.slices; description = "Definition of slice configurations."; }; @@ -352,10 +339,11 @@ in type = types.listOf types.str; example = [ "systemd-backlight@.service" ]; description = '' - A list of units to suppress when generating system systemd configuration directory. This has + A list of units to skip when generating system systemd configuration directory. This has priority over upstream units, , and . The main purpose of this is to - suppress a upstream systemd unit with any modifications made to it by other NixOS modules. + prevent a upstream systemd unit from being added to the initrd with any modifications made to it + by other NixOS modules. ''; }; @@ -471,7 +459,12 @@ in enabledUpstreamSystemUnits = filter (n: ! elem n cfg.suppressedSystemUnits) upstreamSystemUnits; enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedSystemUnits) cfg.units; in ({ - "systemd/system".source = generateUnits "system" enabledUnits enabledUpstreamSystemUnits upstreamSystemWants; + "systemd/system".source = generateUnits { + type = "system"; + units = enabledUnits; + upstreamUnits = enabledUpstreamSystemUnits; + upstreamWants = upstreamSystemWants; + }; "systemd/system.conf".text = '' [Manager] diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix new file mode 100644 index 00000000000000..30bdc9a3422c7f --- /dev/null +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -0,0 +1,417 @@ +{ lib, config, utils, pkgs, ... }: + +with lib; + +let + inherit (utils) systemdUtils escapeSystemdPath; + inherit (systemdUtils.lib) + generateUnits + pathToUnit + serviceToUnit + sliceToUnit + socketToUnit + targetToUnit + timerToUnit + mountToUnit + automountToUnit; + + + cfg = config.boot.initrd.systemd; + + # Copied from fedora + upstreamUnits = [ + "basic.target" + "ctrl-alt-del.target" + "emergency.service" + "emergency.target" + "final.target" + "halt.target" + "initrd-cleanup.service" + "initrd-fs.target" + "initrd-parse-etc.service" + "initrd-root-device.target" + "initrd-root-fs.target" + "initrd-switch-root.service" + "initrd-switch-root.target" + "initrd.target" + "initrd-udevadm-cleanup-db.service" + "kexec.target" + "kmod-static-nodes.service" + "local-fs-pre.target" + "local-fs.target" + "multi-user.target" + "paths.target" + "poweroff.target" + "reboot.target" + "rescue.service" + "rescue.target" + "rpcbind.target" + "shutdown.target" + "sigpwr.target" + "slices.target" + "sockets.target" + "swap.target" + "sysinit.target" + "sys-kernel-config.mount" + "syslog.socket" + "systemd-ask-password-console.path" + "systemd-ask-password-console.service" + "systemd-fsck@.service" + "systemd-halt.service" + "systemd-hibernate-resume@.service" + "systemd-journald-audit.socket" + "systemd-journald-dev-log.socket" + "systemd-journald.service" + "systemd-journald.socket" + "systemd-kexec.service" + "systemd-modules-load.service" + "systemd-poweroff.service" + "systemd-random-seed.service" + "systemd-reboot.service" + "systemd-sysctl.service" + "systemd-tmpfiles-setup-dev.service" + "systemd-tmpfiles-setup.service" + "systemd-udevd-control.socket" + "systemd-udevd-kernel.socket" + "systemd-udevd.service" + "systemd-udev-settle.service" + "systemd-udev-trigger.service" + "systemd-vconsole-setup.service" + "timers.target" + "umount.target" + + # TODO: Networking + # "network-online.target" + # "network-pre.target" + # "network.target" + # "nss-lookup.target" + # "nss-user-lookup.target" + # "remote-fs-pre.target" + # "remote-fs.target" + ] ++ cfg.additionalUpstreamUnits; + + upstreamWants = [ + "sysinit.target.wants" + ]; + + enabledUpstreamUnits = filter (n: ! elem n cfg.suppressedUnits) upstreamUnits; + enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedUnits) cfg.units; + + stage1Units = generateUnits { + type = "initrd"; + units = enabledUnits; + upstreamUnits = enabledUpstreamUnits; + inherit upstreamWants; + inherit (cfg) packages package; + }; + + fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems; + + fstab = pkgs.writeText "fstab" (lib.concatMapStringsSep "\n" + ({ fsType, mountPoint, device, options, autoFormat, autoResize, ... }@fs: let + opts = options ++ optional autoFormat "x-systemd.makefs" ++ optional autoResize "x-systemd.growfs"; + in "${device} /sysroot${mountPoint} ${fsType} ${lib.concatStringsSep "," opts}") fileSystems); + + kernel-name = config.boot.kernelPackages.kernel.name or "kernel"; + modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; }; + firmware = config.hardware.firmware; + # Determine the set of modules that we need to mount the root FS. + modulesClosure = pkgs.makeModulesClosure { + rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules; + kernel = modulesTree; + firmware = firmware; + allowMissing = false; + }; + + initrdBinEnv = pkgs.buildEnv { + name = "initrd-emergency-env"; + paths = map getBin cfg.initrdBin; + pathsToLink = ["/bin" "/sbin"]; + # Make recovery easier + postBuild = '' + ln -s ${cfg.package.util-linux}/bin/mount $out/bin/ + ln -s ${cfg.package.util-linux}/bin/umount $out/bin/ + ''; + }; + + initialRamdisk = pkgs.makeInitrdNG { + contents = map (path: { object = path; symlink = ""; }) (subtractLists cfg.suppressedStorePaths cfg.storePaths) + ++ mapAttrsToList (_: v: { object = v.source; symlink = v.target; }) (filterAttrs (_: v: v.enable) cfg.contents); + }; + +in { + options.boot.initrd.systemd = { + enable = mkEnableOption ''systemd in initrd. + + Note: This is in very early development and is highly + experimental. Most of the features NixOS supports in initrd are + not yet supported by the intrd generated with this option. + ''; + + package = (mkPackageOption pkgs "systemd" { + default = "systemdMinimal"; + }) // { + visible = false; + }; + + contents = mkOption { + description = "Set of files that have to be linked into the initrd"; + example = literalExpression '' + { + "/etc/hostname".text = "mymachine"; + } + ''; + visible = false; + default = {}; + type = types.attrsOf (types.submodule ({ config, options, name, ... }: { + options = { + enable = mkEnableOption "copying of this file to initrd and symlinking it" // { default = true; }; + + target = mkOption { + type = types.path; + description = '' + Path of the symlink. + ''; + default = name; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Text of the file."; + }; + + source = mkOption { + type = types.path; + description = "Path of the source file."; + }; + }; + + config = { + source = mkIf (config.text != null) ( + let name' = "initrd-" + baseNameOf name; + in mkDerivedConfig options.text (pkgs.writeText name') + ); + }; + })); + }; + + storePaths = mkOption { + description = '' + Store paths to copy into the initrd as well. + ''; + type = types.listOf types.singleLineStr; + default = []; + }; + + suppressedStorePaths = mkOption { + description = '' + Store paths specified in the storePaths option that + should not be copied. + ''; + type = types.listOf types.singleLineStr; + default = []; + }; + + emergencyAccess = mkOption { + type = with types; oneOf [ bool singleLineStr ]; + visible = false; + description = '' + Set to true for unauthenticated emergency access, and false for + no emergency access. + + Can also be set to a hashed super user password to allow + authenticated access to the emergency mode. + ''; + default = false; + }; + + initrdBin = mkOption { + type = types.listOf types.package; + default = []; + visible = false; + description = '' + Packages to include in /bin for the stage 1 emergency shell. + ''; + }; + + additionalUpstreamUnits = mkOption { + default = [ ]; + type = types.listOf types.str; + visible = false; + example = [ "debug-shell.service" "systemd-quotacheck.service" ]; + description = '' + Additional units shipped with systemd that shall be enabled. + ''; + }; + + suppressedUnits = mkOption { + default = [ ]; + type = types.listOf types.str; + example = [ "systemd-backlight@.service" ]; + visible = false; + description = '' + A list of units to skip when generating system systemd configuration directory. This has + priority over upstream units, , and + . The main purpose of this is to + prevent a upstream systemd unit from being added to the initrd with any modifications made to it + by other NixOS modules. + ''; + }; + + units = mkOption { + description = "Definition of systemd units."; + default = {}; + visible = false; + type = systemdUtils.types.units; + }; + + packages = mkOption { + default = []; + visible = false; + type = types.listOf types.package; + example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]"; + description = "Packages providing systemd units and hooks."; + }; + + targets = mkOption { + default = {}; + visible = false; + type = systemdUtils.types.initrdTargets; + description = "Definition of systemd target units."; + }; + + services = mkOption { + default = {}; + type = systemdUtils.types.initrdServices; + visible = false; + description = "Definition of systemd service units."; + }; + + sockets = mkOption { + default = {}; + type = systemdUtils.types.initrdSockets; + visible = false; + description = "Definition of systemd socket units."; + }; + + timers = mkOption { + default = {}; + type = systemdUtils.types.initrdTimers; + visible = false; + description = "Definition of systemd timer units."; + }; + + paths = mkOption { + default = {}; + type = systemdUtils.types.initrdPaths; + visible = false; + description = "Definition of systemd path units."; + }; + + mounts = mkOption { + default = []; + type = systemdUtils.types.initrdMounts; + visible = false; + description = '' + Definition of systemd mount units. + This is a list instead of an attrSet, because systemd mandates the names to be derived from + the 'where' attribute. + ''; + }; + + automounts = mkOption { + default = []; + type = systemdUtils.types.automounts; + visible = false; + description = '' + Definition of systemd automount units. + This is a list instead of an attrSet, because systemd mandates the names to be derived from + the 'where' attribute. + ''; + }; + + slices = mkOption { + default = {}; + type = systemdUtils.types.slices; + visible = false; + description = "Definition of slice configurations."; + }; + }; + + config = mkIf (config.boot.initrd.enable && cfg.enable) { + system.build = { inherit initialRamdisk; }; + boot.initrd.systemd = { + initrdBin = [pkgs.bash pkgs.coreutils pkgs.kmod cfg.package] ++ config.system.fsPackages; + + contents = { + "/init".source = "${cfg.package}/lib/systemd/systemd"; + "/etc/systemd/system".source = stage1Units; + + "/etc/systemd/system.conf".text = '' + [Manager] + DefaultEnvironment=PATH=/bin:/sbin + ''; + + "/etc/fstab".source = fstab; + + "/lib/modules".source = "${modulesClosure}/lib/modules"; + + "/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules; + + "/etc/passwd".source = "${pkgs.fakeNss}/etc/passwd"; + "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then "!" else cfg.emergencyAccess}:::::::"; + + "/bin".source = "${initrdBinEnv}/bin"; + "/sbin".source = "${initrdBinEnv}/sbin"; + + "/etc/sysctl.d/nixos.conf".text = "kernel.modprobe = /sbin/modprobe"; + }; + + storePaths = [ + # TODO: Limit this to the bare necessities + "${cfg.package}/lib" + + "${cfg.package.util-linux}/bin/mount" + "${cfg.package.util-linux}/bin/umount" + "${cfg.package.util-linux}/bin/sulogin" + + # so NSS can look up usernames + "${pkgs.glibc}/lib/libnss_files.so" + ]; + + targets.initrd.aliases = ["default.target"]; + units = + mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths + // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services + // mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices + // mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets + // mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets + // mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers + // listToAttrs (map + (v: let n = escapeSystemdPath v.where; + in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts) + // listToAttrs (map + (v: let n = escapeSystemdPath v.where; + in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts); + + services.emergency = mkIf (isBool cfg.emergencyAccess && cfg.emergencyAccess) { + environment.SYSTEMD_SULOGIN_FORCE = "1"; + }; + # The unit in /run/systemd/generator shadows the unit in + # /etc/systemd/system, but will still apply drop-ins from + # /etc/systemd/system/foo.service.d/ + # + # We need IgnoreOnIsolate, otherwise the Requires dependency of + # a mount unit on its makefs unit causes it to be unmounted when + # we isolate for switch-root. Use a dummy package so that + # generateUnits will generate drop-ins instead of unit files. + packages = [(pkgs.runCommand "dummy" {} '' + mkdir -p $out/etc/systemd/system + touch $out/etc/systemd/system/systemd-{makefs,growfs}@.service + '')]; + services."systemd-makefs@".unitConfig.IgnoreOnIsolate = true; + services."systemd-growfs@".unitConfig.IgnoreOnIsolate = true; + }; + }; +} diff --git a/nixos/modules/system/boot/systemd/nspawn.nix b/nixos/modules/system/boot/systemd/nspawn.nix index 0c6822319a5b08..bf9995d03cc183 100644 --- a/nixos/modules/system/boot/systemd/nspawn.nix +++ b/nixos/modules/system/boot/systemd/nspawn.nix @@ -116,7 +116,13 @@ in { in mkMerge [ (mkIf (cfg != {}) { - environment.etc."systemd/nspawn".source = mkIf (cfg != {}) (generateUnits' false "nspawn" units [] []); + environment.etc."systemd/nspawn".source = mkIf (cfg != {}) (generateUnits { + allowCollisions = false; + type = "nspawn"; + inherit units; + upstreamUnits = []; + upstreamWants = []; + }); }) { systemd.targets.multi-user.wants = [ "machines.target" ]; diff --git a/nixos/modules/system/boot/systemd/user.nix b/nixos/modules/system/boot/systemd/user.nix index e30f83f3457f8e..4951aef95584bb 100644 --- a/nixos/modules/system/boot/systemd/user.nix +++ b/nixos/modules/system/boot/systemd/user.nix @@ -12,10 +12,6 @@ let (systemdUtils.lib) makeUnit generateUnits - makeJobScript - unitConfig - serviceConfig - commonUnitText targetToUnit serviceToUnit socketToUnit @@ -57,48 +53,42 @@ in { systemd.user.units = mkOption { description = "Definition of systemd per-user units."; default = {}; - type = with types; attrsOf (submodule ( - { name, config, ... }: - { options = concreteUnitOptions; - config = { - unit = mkDefault (makeUnit name config); - }; - })); + type = systemdUtils.types.units; }; systemd.user.paths = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = pathOptions; } unitConfig ]); + type = systemdUtils.types.paths; description = "Definition of systemd per-user path units."; }; systemd.user.services = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = serviceOptions; } unitConfig serviceConfig ] ); + type = systemdUtils.types.services; description = "Definition of systemd per-user service units."; }; systemd.user.slices = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = sliceOptions; } unitConfig ] ); + type = systemdUtils.types.slices; description = "Definition of systemd per-user slice units."; }; systemd.user.sockets = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = socketOptions; } unitConfig ] ); + type = systemdUtils.types.sockets; description = "Definition of systemd per-user socket units."; }; systemd.user.targets = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = targetOptions; } unitConfig] ); + type = systemdUtils.types.targets; description = "Definition of systemd per-user target units."; }; systemd.user.timers = mkOption { default = {}; - type = with types; attrsOf (submodule [ { options = timerOptions; } unitConfig ] ); + type = systemdUtils.types.timers; description = "Definition of systemd per-user timer units."; }; @@ -119,7 +109,12 @@ in { ]; environment.etc = { - "systemd/user".source = generateUnits "user" cfg.units upstreamUserUnits []; + "systemd/user".source = generateUnits { + type = "user"; + inherit (cfg) units; + upstreamUnits = upstreamUserUnits; + upstreamWants = []; + }; "systemd/user.conf".text = '' [Manager] diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix index f3da6771197e6c..6fcc762ca88ca7 100644 --- a/nixos/modules/tasks/filesystems.nix +++ b/nixos/modules/tasks/filesystems.nix @@ -323,7 +323,7 @@ in unitConfig.DefaultDependencies = false; # needed to prevent a cycle serviceConfig.Type = "oneshot"; }; - in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // { + in listToAttrs (map formatDevice (filter (fs: fs.autoFormat && !(utils.fsNeededForBoot fs)) fileSystems)) // { # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. "mount-pstore" = { diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 51438935894709..05738b9cfc1b88 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -923,6 +923,8 @@ in mkVMOverride (cfg.fileSystems // { "/".device = cfg.bootDevice; + "/".fsType = "ext4"; + "/".autoFormat = true; "/tmp" = mkIf config.boot.tmpOnTmpfs { device = "tmpfs"; @@ -953,6 +955,28 @@ in }; } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories); + boot.initrd.systemd = lib.mkIf (config.boot.initrd.systemd.enable && cfg.writableStore) { + mounts = [{ + where = "/sysroot/nix/store"; + what = "overlay"; + type = "overlay"; + options = "lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/store,workdir=/sysroot/nix/.rw-store/work"; + wantedBy = ["local-fs.target"]; + before = ["local-fs.target"]; + requires = ["sysroot-nix-.ro\\x2dstore.mount" "sysroot-nix-.rw\\x2dstore.mount" "rw-store.service"]; + after = ["sysroot-nix-.ro\\x2dstore.mount" "sysroot-nix-.rw\\x2dstore.mount" "rw-store.service"]; + unitConfig.IgnoreOnIsolate = true; + }]; + services.rw-store = { + after = ["sysroot-nix-.rw\\x2dstore.mount"]; + unitConfig.DefaultDependencies = false; + serviceConfig = { + Type = "oneshot"; + ExecStart = "/bin/mkdir -p 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store"; + }; + }; + }; + swapDevices = mkVMOverride [ ]; boot.initrd.luks.devices = mkVMOverride {}; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 98ca2e081086f5..8d8596873b8c52 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -505,6 +505,7 @@ in systemd-confinement = handleTest ./systemd-confinement.nix {}; systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {}; systemd-escaping = handleTest ./systemd-escaping.nix {}; + systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {}; systemd-journal = handleTest ./systemd-journal.nix {}; systemd-machinectl = handleTest ./systemd-machinectl.nix {}; systemd-networkd = handleTest ./systemd-networkd.nix {}; diff --git a/nixos/tests/systemd-initrd-simple.nix b/nixos/tests/systemd-initrd-simple.nix new file mode 100644 index 00000000000000..ba62cdf3bbc772 --- /dev/null +++ b/nixos/tests/systemd-initrd-simple.nix @@ -0,0 +1,27 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: { + name = "systemd-initrd-simple"; + + machine = { pkgs, ... }: { + boot.initrd.systemd = { + enable = true; + emergencyAccess = true; + }; + fileSystems = lib.mkVMOverride { + "/".autoResize = true; + }; + }; + + testScript = '' + import subprocess + + oldAvail = machine.succeed("df --output=avail / | sed 1d") + machine.shutdown() + + subprocess.check_call(["qemu-img", "resize", "vm-state-machine/machine.qcow2", "+1G"]) + + machine.start() + newAvail = machine.succeed("df --output=avail / | sed 1d") + + assert int(oldAvail) < int(newAvail), "File system did not grow" + ''; +}) diff --git a/pkgs/build-support/docker/default.nix b/pkgs/build-support/docker/default.nix index 5718cadd4ffa40..96ea363c811e0c 100644 --- a/pkgs/build-support/docker/default.nix +++ b/pkgs/build-support/docker/default.nix @@ -6,6 +6,7 @@ , coreutils , e2fsprogs , fakechroot +, fakeNss , fakeroot , findutils , go @@ -747,25 +748,7 @@ rec { # Useful when packaging binaries that insist on using nss to look up # username/groups (like nginx). # /bin/sh is fine to not exist, and provided by another shim. - fakeNss = symlinkJoin { - name = "fake-nss"; - paths = [ - (writeTextDir "etc/passwd" '' - root:x:0:0:root user:/var/empty:/bin/sh - nobody:x:65534:65534:nobody:/var/empty:/bin/sh - '') - (writeTextDir "etc/group" '' - root:x:0: - nobody:x:65534: - '') - (writeTextDir "etc/nsswitch.conf" '' - hosts: files dns - '') - (runCommand "var-empty" { } '' - mkdir -p $out/var/empty - '') - ]; - }; + inherit fakeNss; # alias # This provides a /usr/bin/env, for shell scripts using the # "#!/usr/bin/env executable" shebang. diff --git a/pkgs/build-support/fake-nss/default.nix b/pkgs/build-support/fake-nss/default.nix new file mode 100644 index 00000000000000..9e0b60133e00fb --- /dev/null +++ b/pkgs/build-support/fake-nss/default.nix @@ -0,0 +1,24 @@ +# Provide a /etc/passwd and /etc/group that contain root and nobody. +# Useful when packaging binaries that insist on using nss to look up +# username/groups (like nginx). +# /bin/sh is fine to not exist, and provided by another shim. +{ symlinkJoin, writeTextDir, runCommand }: +symlinkJoin { + name = "fake-nss"; + paths = [ + (writeTextDir "etc/passwd" '' + root:x:0:0:root user:/var/empty:/bin/sh + nobody:x:65534:65534:nobody:/var/empty:/bin/sh + '') + (writeTextDir "etc/group" '' + root:x:0: + nobody:x:65534: + '') + (writeTextDir "etc/nsswitch.conf" '' + hosts: files dns + '') + (runCommand "var-empty" { } '' + mkdir -p $out/var/empty + '') + ]; +} diff --git a/pkgs/build-support/kernel/make-initrd-ng-tool.nix b/pkgs/build-support/kernel/make-initrd-ng-tool.nix new file mode 100644 index 00000000000000..66ffc09d43cf86 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng-tool.nix @@ -0,0 +1,9 @@ +{ rustPlatform }: + +rustPlatform.buildRustPackage { + pname = "make-initrd-ng"; + version = "0.1.0"; + + src = ./make-initrd-ng; + cargoLock.lockFile = ./make-initrd-ng/Cargo.lock; +} diff --git a/pkgs/build-support/kernel/make-initrd-ng.nix b/pkgs/build-support/kernel/make-initrd-ng.nix new file mode 100644 index 00000000000000..9fd202c44847a0 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng.nix @@ -0,0 +1,79 @@ +let + # Some metadata on various compression programs, relevant to naming + # the initramfs file and, if applicable, generating a u-boot image + # from it. + compressors = import ./initrd-compressor-meta.nix; + # Get the basename of the actual compression program from the whole + # compression command, for the purpose of guessing the u-boot + # compression type and filename extension. + compressorName = fullCommand: builtins.elemAt (builtins.match "([^ ]*/)?([^ ]+).*" fullCommand) 1; +in +{ stdenvNoCC, perl, cpio, ubootTools, lib, pkgsBuildHost, makeInitrdNGTool, patchelf, runCommand, glibc +# Name of the derivation (not of the resulting file!) +, name ? "initrd" + +# Program used to compress the cpio archive; use "cat" for no compression. +# This can also be a function which takes a package set and returns the path to the compressor, +# such as `pkgs: "${pkgs.lzop}/bin/lzop"`. +, compressor ? "gzip" +, _compressorFunction ? + if lib.isFunction compressor then compressor + else if ! builtins.hasContext compressor && builtins.hasAttr compressor compressors then compressors.${compressor}.executable + else _: compressor +, _compressorExecutable ? _compressorFunction pkgsBuildHost +, _compressorName ? compressorName _compressorExecutable +, _compressorMeta ? compressors.${_compressorName} or {} + +# List of arguments to pass to the compressor program, or null to use its defaults +, compressorArgs ? null +, _compressorArgsReal ? if compressorArgs == null then _compressorMeta.defaultArgs or [] else compressorArgs + +# Filename extension to use for the compressed initramfs. This is +# included for clarity, but $out/initrd will always be a symlink to +# the final image. +# If this isn't guessed, you may want to complete the metadata above and send a PR :) +, extension ? _compressorMeta.extension or + (throw "Unrecognised compressor ${_compressorName}, please specify filename extension") + +# List of { object = path_or_derivation; symlink = "/path"; } +# The paths are copied into the initramfs in their nix store path +# form, then linked at the root according to `symlink`. +, contents + +# List of uncompressed cpio files to prepend to the initramfs. This +# can be used to add files in specified paths without them becoming +# symlinks to store paths. +, prepend ? [] + +# Whether to wrap the initramfs in a u-boot image. +, makeUInitrd ? stdenvNoCC.hostPlatform.linux-kernel.target == "uImage" + +# If generating a u-boot image, the architecture to use. The default +# guess may not align with u-boot's nomenclature correctly, so it can +# be overridden. +# See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L81-106 for a list. +, uInitrdArch ? stdenvNoCC.hostPlatform.linuxArch + +# The name of the compression, as recognised by u-boot. +# See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L195-204 for a list. +# If this isn't guessed, you may want to complete the metadata above and send a PR :) +, uInitrdCompression ? _compressorMeta.ubootName or + (throw "Unrecognised compressor ${_compressorName}, please specify uInitrdCompression") +}: runCommand name { + compress = "${_compressorExecutable} ${lib.escapeShellArgs _compressorArgsReal}"; + passthru = { + compressorExecutableFunction = _compressorFunction; + compressorArgs = _compressorArgsReal; + }; + + passAsFile = ["contents"]; + contents = lib.concatMapStringsSep "\n" ({ object, symlink, ... }: "${object}\n${if symlink == null then "" else symlink}") contents + "\n"; + + nativeBuildInputs = [makeInitrdNGTool patchelf glibc cpio]; +} '' + mkdir ./root + make-initrd-ng "$contentsPath" ./root + mkdir "$out" + (cd root && find * .[^.*] -exec touch -h -d '@1' '{}' +) + (cd root && find * .[^.*] -print0 | sort -z | cpio -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd") +'' diff --git a/pkgs/build-support/kernel/make-initrd-ng/Cargo.lock b/pkgs/build-support/kernel/make-initrd-ng/Cargo.lock new file mode 100644 index 00000000000000..75e732029b5112 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "make-initrd-ng" +version = "0.1.0" diff --git a/pkgs/build-support/kernel/make-initrd-ng/Cargo.toml b/pkgs/build-support/kernel/make-initrd-ng/Cargo.toml new file mode 100644 index 00000000000000..9076f6b1561768 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "make-initrd-ng" +version = "0.1.0" +authors = ["Will Fancher "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/pkgs/build-support/kernel/make-initrd-ng/README.md b/pkgs/build-support/kernel/make-initrd-ng/README.md new file mode 100644 index 00000000000000..741eba67e43f06 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng/README.md @@ -0,0 +1,79 @@ +# What is this for? + +NixOS's traditional initrd is generated by listing the paths that +should be included in initrd and copying the full runtime closure of +those paths into the archive. For most things, like almost any +executable, this involves copying the entirety of huge packages like +glibc, when only things like the shared library files are needed. To +solve this, NixOS does a variety of patchwork to edit the files being +copied in so they only refer to small, patched up paths. For instance, +executables and their shared library dependencies are copied into an +`extraUtils` derivation, and every ELF file is patched to refer to +files in that output. + +The problem with this is that it is often difficult to correctly patch +some things. For instance, systemd bakes the path to the `mount` +command into the binary, so patchelf is no help. Instead, it's very +often easier to simply copy the desired files to their original store +locations in initrd and not copy their entire runtime closure. This +does mean that it is the burden of the developer to ensure that all +necessary dependencies are copied in, as closures won't be +consulted. However, it is rare that full closures are actually +desirable, so in the traditional initrd, the developer was likely to +do manual work on patching the dependencies explicitly anyway. + +# How it works + +This program is similar to its inspiration (`find-libs` from the +traditional initrd), except that it also handles symlinks and +directories according to certain rules. As input, it receives a +sequence of pairs of paths. The first path is an object to copy into +initrd. The second path (if not empty) is the path to a symlink that +should be placed in the initrd, pointing to that object. How that +object is copied depends on its type. + +1. A regular file is copied directly to the same absolute path in the + initrd. + + - If it is *also* an ELF file, then all of its direct shared + library dependencies are also listed as objects to be copied. + +2. A directory's direct children are listed as objects to be copied, + and a directory at the same absolute path in the initrd is created. + +3. A symlink's target is listed as an object to be copied. + +There are a couple of quirks to mention here. First, the term "object" +refers to the final file path that the developer intends to have +copied into initrd. This means any parent directory is not considered +an object just because its child was listed as an object in the +program input; instead those intermediate directories are simply +created in support of the target object. Second, shared libraries, +directory children, and symlink targets aren't immediately recursed, +because they simply get listed as objects themselves, and are +therefore traversed when they themselves are processed. Finally, +symlinks in the intermediate directories leading to an object are +preserved, meaning an input object `/a/symlink/b` will just result in +initrd containing `/a/symlink -> /target/b` and `/target/b`, even if +`/target` has other children. Preserving symlinks in this manner is +important for things like systemd. + +These rules automate the most important and obviously necessary +copying that needs to be done in most cases, allowing programs and +configuration files to go unpatched, while keeping the content of the +initrd to a minimum. + +# Why Rust? + +- A prototype of this logic was written in Bash, in an attempt to keep + with its `find-libs` ancestor, but that program was difficult to + write, and ended up taking several minutes to run. This program runs + in less than a second, and the code is substantially easier to work + with. + +- This will not require end users to install a rust toolchain to use + NixOS, as long as this tool is cached by Hydra. And if you're + bootstrapping NixOS from source, rustc is already required anyway. + +- Rust was favored over Python for its type system, and because if you + want to go fast, why not go *really fast*? diff --git a/pkgs/build-support/kernel/make-initrd-ng/src/main.rs b/pkgs/build-support/kernel/make-initrd-ng/src/main.rs new file mode 100644 index 00000000000000..1342734590f725 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng/src/main.rs @@ -0,0 +1,208 @@ +use std::collections::{HashSet, VecDeque}; +use std::env; +use std::ffi::OsStr; +use std::fs; +use std::hash::Hash; +use std::io::{BufReader, BufRead, Error, ErrorKind}; +use std::os::unix; +use std::path::{Component, Path, PathBuf}; +use std::process::{Command, Stdio}; + +struct NonRepeatingQueue { + queue: VecDeque, + seen: HashSet, +} + +impl NonRepeatingQueue { + fn new() -> NonRepeatingQueue { + NonRepeatingQueue { + queue: VecDeque::new(), + seen: HashSet::new(), + } + } +} + +impl NonRepeatingQueue { + fn push_back(&mut self, value: T) -> bool { + if self.seen.contains(&value) { + false + } else { + self.seen.insert(value.clone()); + self.queue.push_back(value); + true + } + } + + fn pop_front(&mut self) -> Option { + self.queue.pop_front() + } +} + +fn patch_elf, P: AsRef>(mode: S, path: P) -> Result { + let output = Command::new("patchelf") + .arg(&mode) + .arg(&path) + .stderr(Stdio::inherit()) + .output()?; + if output.status.success() { + Ok(String::from_utf8(output.stdout).expect("Failed to parse output")) + } else { + Err(Error::new(ErrorKind::Other, format!("failed: patchelf {:?} {:?}", OsStr::new(&mode), OsStr::new(&path)))) + } +} + +fn copy_file + AsRef, S: AsRef>( + source: P, + target: S, + queue: &mut NonRepeatingQueue>, +) -> Result<(), Error> { + fs::copy(&source, target)?; + + if !Command::new("ldd").arg(&source).output()?.status.success() { + //stdout(Stdio::inherit()).stderr(Stdio::inherit()). + println!("{:?} is not dynamically linked. Not recursing.", OsStr::new(&source)); + return Ok(()); + } + + let rpath_string = patch_elf("--print-rpath", &source)?; + let needed_string = patch_elf("--print-needed", &source)?; + // Shared libraries don't have an interpreter + if let Ok(interpreter_string) = patch_elf("--print-interpreter", &source) { + queue.push_back(Box::from(Path::new(&interpreter_string.trim()))); + } + + let rpath = rpath_string.trim().split(":").map(|p| Box::::from(Path::new(p))).collect::>(); + + for line in needed_string.lines() { + let mut found = false; + for path in &rpath { + let lib = path.join(line); + if lib.exists() { + // No need to recurse. The queue will bring it back round. + queue.push_back(Box::from(lib.as_path())); + found = true; + break; + } + } + if !found { + // glibc makes it tricky to make this an error because + // none of the files have a useful rpath. + println!("Warning: Couldn't satisfy dependency {} for {:?}", line, OsStr::new(&source)); + } + } + + Ok(()) +} + +fn queue_dir>( + source: P, + queue: &mut NonRepeatingQueue>, +) -> Result<(), Error> { + for entry in fs::read_dir(source)? { + let entry = entry?; + // No need to recurse. The queue will bring us back round here on its own. + queue.push_back(Box::from(entry.path().as_path())); + } + + Ok(()) +} + +fn handle_path( + root: &Path, + p: &Path, + queue: &mut NonRepeatingQueue>, +) -> Result<(), Error> { + let mut source = PathBuf::new(); + let mut target = Path::new(root).to_path_buf(); + let mut iter = p.components().peekable(); + while let Some(comp) = iter.next() { + match comp { + Component::Prefix(_) => panic!("This tool is not meant for Windows"), + Component::RootDir => { + target.clear(); + target.push(root); + source.clear(); + source.push("/"); + } + Component::CurDir => {} + Component::ParentDir => { + // Don't over-pop the target if the path has too many ParentDirs + if source.pop() { + target.pop(); + } + } + Component::Normal(name) => { + target.push(name); + source.push(name); + let typ = fs::symlink_metadata(&source)?.file_type(); + if typ.is_file() && !target.exists() { + copy_file(&source, &target, queue)?; + } else if typ.is_symlink() { + let link_target = fs::read_link(&source)?; + + // Create the link, then push its target to the queue + if !target.exists() { + unix::fs::symlink(&link_target, &target)?; + } + source.pop(); + source.push(link_target); + while let Some(c) = iter.next() { + source.push(c); + } + let link_target_path = source.as_path(); + if link_target_path.exists() { + queue.push_back(Box::from(link_target_path)); + } + break; + } else if typ.is_dir() { + if !target.exists() { + fs::create_dir(&target)?; + } + + // Only recursively copy if the directory is the target object + if iter.peek().is_none() { + queue_dir(&source, queue)?; + } + } + } + } + } + + Ok(()) +} + +fn main() -> Result<(), Error> { + let args: Vec = env::args().collect(); + let input = fs::File::open(&args[1])?; + let output = &args[2]; + let out_path = Path::new(output); + + let mut queue = NonRepeatingQueue::>::new(); + + let mut lines = BufReader::new(input).lines(); + while let Some(obj) = lines.next() { + // Lines should always come in pairs + let obj = obj?; + let sym = lines.next().unwrap()?; + + let obj_path = Path::new(&obj); + queue.push_back(Box::from(obj_path)); + if !sym.is_empty() { + println!("{} -> {}", &sym, &obj); + // We don't care about preserving symlink structure here + // nearly as much as for the actual objects. + let link_string = format!("{}/{}", output, sym); + let link_path = Path::new(&link_string); + let mut link_parent = link_path.to_path_buf(); + link_parent.pop(); + fs::create_dir_all(link_parent)?; + unix::fs::symlink(obj_path, link_path)?; + } + } + while let Some(obj) = queue.pop_front() { + println!("{:?}", obj); + handle_path(out_path, &*obj, &mut queue)?; + } + + Ok(()) +} diff --git a/pkgs/os-specific/linux/systemd/default.nix b/pkgs/os-specific/linux/systemd/default.nix index 3a3a419093b753..4cbed9b7cbf103 100644 --- a/pkgs/os-specific/linux/systemd/default.nix +++ b/pkgs/os-specific/linux/systemd/default.nix @@ -603,7 +603,7 @@ stdenv.mkDerivation { # runtime; otherwise we can't and we need to reboot. interfaceVersion = 2; - inherit withCryptsetup; + inherit withCryptsetup util-linux; tests = { inherit (nixosTests) switchTest; diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 0d71f7594c985b..7b013701d97fe2 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -436,6 +436,9 @@ with pkgs; dockerTools = callPackage ../build-support/docker { writePython3 = buildPackages.writers.writePython3; }; + + fakeNss = callPackage ../build-support/fake-nss { }; + tarsum = callPackage ../build-support/docker/tarsum.nix { }; snapTools = callPackage ../build-support/snap { }; @@ -732,6 +735,9 @@ with pkgs; makeInitrd = callPackage ../build-support/kernel/make-initrd.nix; # Args intentionally left out + makeInitrdNG = callPackage ../build-support/kernel/make-initrd-ng.nix; + makeInitrdNGTool = callPackage ../build-support/kernel/make-initrd-ng-tool.nix {}; + makeWrapper = makeSetupHook { deps = [ dieHook ]; substitutions = {