Merge pull request #164943 from ElvishJerricco/systemd-initrd-reuse-systemd-module

initrd: Opt-in bare bones systemd-based initrd
main
Janne Heß 2 years ago committed by GitHub
commit 7cdc4dd5d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 84
      nixos/lib/systemd-lib.nix
  2. 37
      nixos/lib/systemd-types.nix
  3. 415
      nixos/lib/systemd-unit-options.nix
  4. 1
      nixos/lib/utils.nix
  5. 40
      nixos/modules/misc/version.nix
  6. 1
      nixos/modules/module-list.nix
  7. 8
      nixos/modules/system/boot/stage-1.nix
  8. 43
      nixos/modules/system/boot/systemd.nix
  9. 417
      nixos/modules/system/boot/systemd/initrd.nix
  10. 8
      nixos/modules/system/boot/systemd/nspawn.nix
  11. 31
      nixos/modules/system/boot/systemd/user.nix
  12. 2
      nixos/modules/tasks/filesystems.nix
  13. 24
      nixos/modules/virtualisation/qemu-vm.nix
  14. 1
      nixos/tests/all-tests.nix
  15. 27
      nixos/tests/systemd-initrd-simple.nix
  16. 21
      pkgs/build-support/docker/default.nix
  17. 24
      pkgs/build-support/fake-nss/default.nix
  18. 9
      pkgs/build-support/kernel/make-initrd-ng-tool.nix
  19. 79
      pkgs/build-support/kernel/make-initrd-ng.nix
  20. 5
      pkgs/build-support/kernel/make-initrd-ng/Cargo.lock
  21. 9
      pkgs/build-support/kernel/make-initrd-ng/Cargo.toml
  22. 79
      pkgs/build-support/kernel/make-initrd-ng/README.md
  23. 208
      pkgs/build-support/kernel/make-initrd-ng/src/main.rs
  24. 2
      pkgs/os-specific/linux/systemd/default.nix
  25. 6
      pkgs/top-level/all-packages.nix

@ -122,10 +122,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;
} ''
@ -133,7 +138,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")"
@ -150,7 +155,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
@ -162,14 +167,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")"
@ -270,9 +275,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; }
@ -288,45 +293,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 =
@ -374,12 +358,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}
'';
};

@ -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 ]);
}

@ -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 <option>restartIfChanged</option> is
ignored.
This option should not be used anymore in favor of
<option>reloadTriggers</option> 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
<command>systemctl stop</command> in the old configuration,
then <command>systemctl start</command> in the new one.
Otherwise, it is restarted in a single step using
<command>systemctl restart</command> in the new configuration.
The latter is less correct because it runs the
<literal>ExecStop</literal> 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
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>. This is equivalent
to adding a corresponding timer unit with
<option>OnCalendar</option> 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 <option>restartIfChanged</option> is
ignored.
This option should not be used anymore in favor of
<option>reloadTriggers</option> 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
<command>systemctl stop</command> in the old configuration,
then <command>systemctl start</command> in the new one.
Otherwise, it is restarted in a single step using
<command>systemctl restart</command> in the new configuration.
The latter is less correct because it runs the
<literal>ExecStop</literal> 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
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>. This is equivalent
to adding a corresponding timer unit with
<option>OnCalendar</option> 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 {
<manvolnum>5</manvolnum></citerefentry> for details.
'';
};
}; };
stage2MountOptions = {
imports = [
stage2CommonUnitOptions
mountOptions
];
};
stage1MountOptions = {
imports = [
stage1CommonUnitOptions
mountOptions
];
};
automountOptions = commonUnitOptions // {
automountOptions = { options = {
where = mkOption {
example = "/mnt";
@ -529,11 +636,23 @@ in rec {
<manvolnum>5</manvolnum></citerefentry> 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
];
};
}

@ -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; };
};
}

@ -15,6 +15,26 @@ let
mapAttrsToList (n: v: ''${n}=${escapeIfNeccessary (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 = [
@ -119,20 +139,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;
};
};

@ -1181,6 +1181,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

@ -703,8 +703,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")

@ -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.";
};
@ -362,10 +349,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, <option>systemd.units</option>, and
<option>systemd.additionalUpstreamSystemUnits</option>. 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.
'';
};
@ -482,7 +470,12 @@ in
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]

@ -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, <option>boot.initrd.systemd.units</option>, and
<option>boot.initrd.systemd.additionalUpstreamUnits</option>. 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;
};
};
}

@ -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" ];

@ -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]

@ -352,7 +352,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" = {

@ -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 {};

@ -514,6 +514,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 {};

@ -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"
'';
})

@ -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.

@ -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
'')
];
}

@ -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;
}

@ -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")
''

@ -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"

@ -0,0 +1,9 @@
[package]
name = "make-initrd-ng"
version = "0.1.0"
authors = ["Will Fancher <elvishjerricco@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

@ -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*?

@ -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<T> {
queue: VecDeque<T>,
seen: HashSet<T>,
}
impl<T> NonRepeatingQueue<T> {
fn new() -> NonRepeatingQueue<T> {
NonRepeatingQueue {
queue: VecDeque::new(),
seen: HashSet::new(),
}
}
}
impl<T: Clone + Eq + Hash> NonRepeatingQueue<T> {
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<T> {
self.queue.pop_front()
}
}
fn patch_elf<S: AsRef<OsStr>, P: AsRef<OsStr>>(mode: S, path: P) -> Result<String, Error> {
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<P: AsRef<Path> + AsRef<OsStr>, S: AsRef<Path>>(
source: P,
target: S,
queue: &mut NonRepeatingQueue<Box<Path>>,
) -> 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::<Path>::from(Path::new(p))).collect::<Vec<_>>();
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<P: AsRef<Path>>(
source: P,
queue: &mut NonRepeatingQueue<Box<Path>>,
) -> 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<Box<Path>>,
) -> 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<String> = env::args().collect();
let input = fs::File::open(&args[1])?;
let output = &args[2];
let out_path = Path::new(output);
let mut queue = NonRepeatingQueue::<Box<Path>>::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(())
}

@ -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;

@ -451,6 +451,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 { };
@ -747,6 +750,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 = {

Loading…
Cancel
Save