Merge pull request #164943 from ElvishJerricco/systemd-initrd-reuse-systemd-module
initrd: Opt-in bare bones systemd-based initrdmain
commit
7cdc4dd5d1
@ -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 ]); |
||||
} |
@ -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; |
||||
}; |
||||
}; |
||||
} |
@ -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" |
||||
''; |
||||
}) |
@ -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(()) |
||||
} |
Loading…
Reference in new issue