diff --git a/nixos/default.nix b/nixos/default.nix index c11872f1441ab..99de5ac9ccc3b 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -34,6 +34,7 @@ in { inherit (eval) pkgs config options; + inherit vmConfig; system = eval.config.system.build.toplevel; diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index fa14d86e253df..000f0f97a2831 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -917,5 +917,17 @@ in boot.initrd.postDeviceCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands; environment.systemPackages = [ pkgs.cryptsetup ]; + + boot.initrd.emergencyPackages = [ pkgs.cryptsetup ]; + boot.initrd.objects = [ + { + object = pkgs.writeText "crypttab" + (lib.concatMapStringsSep "\n" ({ device, name, ... }: "${name} ${device}") + (builtins.attrValues config.boot.initrd.luks.devices)); + symlink = "/etc/crypttab"; + } + { object = "${config.systemd.package}/lib/systemd/systemd-cryptsetup"; executable = true; } + ]; + }; } diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix index d606d473d91e0..f3f0bb6ec8fbc 100644 --- a/nixos/modules/system/boot/stage-1.nix +++ b/nixos/modules/system/boot/stage-1.nix @@ -8,8 +8,7 @@ with lib; let - - udev = config.systemd.package; + systemd = config.systemd.package; kernel-name = config.boot.kernelPackages.kernel.name or "kernel"; @@ -31,315 +30,48 @@ let # mounting `/`, like `/` on a loopback). fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems; - # A utility for enumerating the shared-library dependencies of a program - findLibs = pkgs.buildPackages.writeShellScriptBin "find-libs" '' - set -euo pipefail - - declare -A seen - left=() - - patchelf="${pkgs.buildPackages.patchelf}/bin/patchelf" - - function add_needed { - rpath="$($patchelf --print-rpath $1)" - dir="$(dirname $1)" - for lib in $($patchelf --print-needed $1); do - left+=("$lib" "$rpath" "$dir") - done - } - - add_needed "$1" - - while [ ''${#left[@]} -ne 0 ]; do - next=''${left[0]} - rpath=''${left[1]} - ORIGIN=''${left[2]} - left=("''${left[@]:3}") - if [ -z ''${seen[$next]+x} ]; then - seen[$next]=1 - - # Ignore the dynamic linker which for some reason appears as a DT_NEEDED of glibc but isn't in glibc's RPATH. - case "$next" in - ld*.so.?) continue;; - esac - - IFS=: read -ra paths <<< $rpath - res= - for path in "''${paths[@]}"; do - path=$(eval "echo $path") - if [ -f "$path/$next" ]; then - res="$path/$next" - echo "$res" - add_needed "$res" - break - fi - done - if [ -z "$res" ]; then - echo "Couldn't satisfy dependency $next" >&2 - exit 1 - fi - fi + extraUnitsObjects = map (u: { object = u; }) (builtins.attrValues config.boot.initrd.extraUnits + ++ lib.concatLists (lib.mapAttrsToList (_: builtins.attrValues) config.boot.initrd.unitOverrides)); + systemdUnits = pkgs.callPackage ./systemd-units.nix { + inherit systemd; + inherit (config.boot.initrd) extraUnits unitOverrides; + systemdPackages = config.boot.initrd.systemd.packages; + }; + + fstab = pkgs.writeText "fstab" (lib.concatMapStringsSep "\n" + ({ fsType, mountPoint, device, options, ... }: + "${device} /sysroot${mountPoint} ${fsType} ${lib.concatStringsSep "," options}") fileSystems); + + groups = [ + "root" + "tty" + "dialout" + "kmem" + "input" + "video" + "render" + "audio" + "lp" + "disk" + "cdrom" + "tape" + "kvm" + ]; + + initrdUdevRules = pkgs.runCommandNoCC "udev-rules" { udevPackages = [ systemd pkgs.lvm2 ]; } '' + mkdir -p $out/lib/udev + for p in $udevPackages; do + cp -r --preserve=all --no-preserve=mode $p/lib/udev $out/lib done ''; - # Some additional utilities needed in stage 1, like mount, lvm, fsck - # etc. We don't want to bring in all of those packages, so we just - # copy what we need. Instead of using statically linked binaries, - # we just copy what we need from Glibc and use patchelf to make it - # work. - extraUtils = pkgs.runCommandCC "extra-utils" - { nativeBuildInputs = [pkgs.buildPackages.nukeReferences]; - allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd - } - '' - set +o pipefail - - mkdir -p $out/bin $out/lib - ln -s $out/bin $out/sbin - - copy_bin_and_libs () { - [ -f "$out/bin/$(basename $1)" ] && rm "$out/bin/$(basename $1)" - cp -pdv $1 $out/bin - } - - # Copy BusyBox. - for BIN in ${pkgs.busybox}/{s,}bin/*; do - copy_bin_and_libs $BIN - done - - # Copy some util-linux stuff. - copy_bin_and_libs ${pkgs.util-linux}/sbin/blkid - - # Copy dmsetup and lvm. - copy_bin_and_libs ${getBin pkgs.lvm2}/bin/dmsetup - copy_bin_and_libs ${getBin pkgs.lvm2}/bin/lvm - - # Add RAID mdadm tool. - copy_bin_and_libs ${pkgs.mdadm}/sbin/mdadm - copy_bin_and_libs ${pkgs.mdadm}/sbin/mdmon - - # Copy udev. - copy_bin_and_libs ${udev}/bin/udevadm - copy_bin_and_libs ${udev}/lib/systemd/systemd-sysctl - for BIN in ${udev}/lib/udev/*_id; do - copy_bin_and_libs $BIN - done - # systemd-udevd is only a symlink to udevadm these days - ln -sf udevadm $out/bin/systemd-udevd - - # Copy modprobe. - copy_bin_and_libs ${pkgs.kmod}/bin/kmod - ln -sf kmod $out/bin/modprobe - - # Copy resize2fs if any ext* filesystems are to be resized - ${optionalString (any (fs: fs.autoResize && (lib.hasPrefix "ext" fs.fsType)) fileSystems) '' - # We need mke2fs in the initrd. - copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs - ''} - - # Copy secrets if needed. - # - # TODO: move out to a separate script; see #85000. - ${optionalString (!config.boot.loader.supportsInitrdSecrets) - (concatStringsSep "\n" (mapAttrsToList (dest: source: - let source' = if source == null then dest else source; in - '' - mkdir -p $(dirname "$out/secrets/${dest}") - # Some programs (e.g. ssh) doesn't like secrets to be - # symlinks, so we use `cp -L` here to match the - # behaviour when secrets are natively supported. - cp -Lr ${source'} "$out/secrets/${dest}" - '' - ) config.boot.initrd.secrets)) - } - - ${config.boot.initrd.extraUtilsCommands} - - # Copy ld manually since it isn't detected correctly - cp -pv ${pkgs.stdenv.cc.libc.out}/lib/ld*.so.? $out/lib - - # Copy all of the needed libraries - find $out/bin $out/lib -type f | while read BIN; do - echo "Copying libs for executable $BIN" - for LIB in $(${findLibs}/bin/find-libs $BIN); do - TGT="$out/lib/$(basename $LIB)" - if [ ! -f "$TGT" ]; then - SRC="$(readlink -e $LIB)" - cp -pdv "$SRC" "$TGT" - fi - done - done - - # Strip binaries further than normal. - chmod -R u+w $out - stripDirs "$STRIP" "lib bin" "-s" - - # Run patchelf to make the programs refer to the copied libraries. - find $out/bin $out/lib -type f | while read i; do - if ! test -L $i; then - nuke-refs -e $out $i - fi - done - - find $out/bin -type f | while read i; do - if ! test -L $i; then - echo "patching $i..." - patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true - fi - done - - if [ -z "${toString (pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform)}" ]; then - # Make sure that the patchelf'ed binaries still work. - echo "testing patched programs..." - $out/bin/ash -c 'echo hello world' | grep "hello world" - export LD_LIBRARY_PATH=$out/lib - $out/bin/mount --help 2>&1 | grep -q "BusyBox" - $out/bin/blkid -V 2>&1 | grep -q 'libblkid' - $out/bin/udevadm --version - $out/bin/dmsetup --version 2>&1 | tee -a log | grep -q "version:" - LVM_SYSTEM_DIR=$out $out/bin/lvm version 2>&1 | tee -a log | grep -q "LVM" - $out/bin/mdadm --version - - ${config.boot.initrd.extraUtilsCommandsTest} - fi - ''; # */ - - - # Networkd link files are used early by udev to set up interfaces early. - # This must be done in stage 1 to avoid race conditions between udev and - # network daemons. - linkUnits = pkgs.runCommand "link-units" { - allowedReferences = [ extraUtils ]; - preferLocalBuild = true; - } ('' - mkdir -p $out - cp -v ${udev}/lib/systemd/network/*.link $out/ - '' + ( - let - links = filterAttrs (n: v: hasSuffix ".link" n) config.systemd.network.units; - files = mapAttrsToList (n: v: "${v.unit}/${n}") links; - in - concatMapStringsSep "\n" (file: "cp -v ${file} $out/") files - )); - - udevRules = pkgs.runCommand "udev-rules" { - allowedReferences = [ extraUtils ]; - preferLocalBuild = true; - } '' - mkdir -p $out - - echo 'ENV{LD_LIBRARY_PATH}="${extraUtils}/lib"' > $out/00-env.rules - - cp -v ${udev}/lib/udev/rules.d/60-cdrom_id.rules $out/ - cp -v ${udev}/lib/udev/rules.d/60-persistent-storage.rules $out/ - cp -v ${udev}/lib/udev/rules.d/75-net-description.rules $out/ - cp -v ${udev}/lib/udev/rules.d/80-drivers.rules $out/ - cp -v ${udev}/lib/udev/rules.d/80-net-setup-link.rules $out/ - cp -v ${pkgs.lvm2}/lib/udev/rules.d/*.rules $out/ - ${config.boot.initrd.extraUdevRulesCommands} - - for i in $out/*.rules; do - substituteInPlace $i \ - --replace ata_id ${extraUtils}/bin/ata_id \ - --replace scsi_id ${extraUtils}/bin/scsi_id \ - --replace cdrom_id ${extraUtils}/bin/cdrom_id \ - --replace ${pkgs.coreutils}/bin/basename ${extraUtils}/bin/basename \ - --replace ${pkgs.util-linux}/bin/blkid ${extraUtils}/bin/blkid \ - --replace ${getBin pkgs.lvm2}/bin ${extraUtils}/bin \ - --replace ${pkgs.mdadm}/sbin ${extraUtils}/sbin \ - --replace ${pkgs.bash}/bin/sh ${extraUtils}/bin/sh \ - --replace ${udev} ${extraUtils} - done - - # Work around a bug in QEMU, which doesn't implement the "READ - # DISC INFORMATION" SCSI command: - # https://bugzilla.redhat.com/show_bug.cgi?id=609049 - # As a result, `cdrom_id' doesn't print - # ID_CDROM_MEDIA_TRACK_COUNT_DATA, which in turn prevents the - # /dev/disk/by-label symlinks from being created. We need these - # in the NixOS installation CD, so use ID_CDROM_MEDIA in the - # corresponding udev rules for now. This was the behaviour in - # udev <= 154. See also - # http://www.spinics.net/lists/hotplug/msg03935.html - substituteInPlace $out/60-persistent-storage.rules \ - --replace ID_CDROM_MEDIA_TRACK_COUNT_DATA ID_CDROM_MEDIA - ''; # */ - - - # The init script of boot stage 1 (loading kernel modules for - # mounting the root FS). - bootStage1 = pkgs.substituteAll { - src = ./stage-1-init.sh; - - shell = "${extraUtils}/bin/ash"; - - isExecutable = true; - - postInstall = '' - echo checking syntax - # check both with bash - ${pkgs.buildPackages.bash}/bin/sh -n $target - # and with ash shell, just in case - ${pkgs.buildPackages.busybox}/bin/ash -n $target - ''; - - inherit linkUnits udevRules extraUtils modulesClosure; - - inherit (config.boot) resumeDevice; - - inherit (config.system.build) earlyMountScript; - - inherit (config.boot.initrd) checkJournalingFS verbose - preLVMCommands preDeviceCommands postDeviceCommands postMountCommands preFailCommands kernelModules; - - resumeDevices = map (sd: if sd ? device then sd.device else "/dev/disk/by-label/${sd.label}") - (filter (sd: hasPrefix "/dev/" sd.device && !sd.randomEncryption.enable - # Don't include zram devices - && !(hasPrefix "/dev/zram" sd.device) - ) config.swapDevices); - - fsInfo = - let f = fs: [ fs.mountPoint (if fs.device != null then fs.device else "/dev/disk/by-label/${fs.label}") fs.fsType (builtins.concatStringsSep "," fs.options) ]; - in pkgs.writeText "initrd-fsinfo" (concatStringsSep "\n" (concatMap f fileSystems)); - - setHostId = optionalString (config.networking.hostId != null) '' - hi="${config.networking.hostId}" - ${if pkgs.stdenv.isBigEndian then '' - echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > /etc/hostid - '' else '' - echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > /etc/hostid - ''} - ''; + emergencyEnv = pkgs.buildEnv { + name = "packages"; + paths = map (p: lib.getBin p) config.boot.initrd.emergencyPackages; + pathsToLink = [ "/bin" ]; }; - - # The closure of the init script of boot stage 1 is what we put in - # the initial RAM disk. - initialRamdisk = pkgs.makeInitrd { - name = "initrd-${kernel-name}"; - inherit (config.boot.initrd) compressor compressorArgs prepend; - - contents = - [ { object = bootStage1; - symlink = "/init"; - } - { object = pkgs.writeText "mdadm.conf" config.boot.initrd.mdadmConf; - symlink = "/etc/mdadm.conf"; - } - { object = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" { - src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf"; - preferLocalBuild = true; - } '' - target=$out - ${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out - ''; - symlink = "/etc/modprobe.d/ubuntu.conf"; - } - { object = pkgs.kmod-debian-aliases; - symlink = "/etc/modprobe.d/debian.conf"; - } - ]; - }; + initialRamdisk = pkgs.makeInitrdNG { contents = config.boot.initrd.objects; }; # Script to add secret files to the initrd at bootloader update time initialRamdiskSecretAppender = @@ -591,6 +323,31 @@ in ''; }; + boot.initrd.extraUnits = mkOption { type = types.attrsOf types.path; }; + + boot.initrd.unitOverrides = mkOption { type = types.attrsOf (types.attrsOf types.path); }; + + boot.initrd.emergencyPackages = mkOption { type = types.listOf types.package; }; + + boot.initrd.objects = with types; mkOption { + type = listOf (submodule { + options.object = mkOption { type = path; }; + options.symlink = mkOption { + type = nullOr path; + default = null; + }; + options.executable = mkOption { + type = bool; + default = false; + }; + }); + }; + + boot.initrd.emergencyHashedPassword = mkOption { + type = types.nullOr types.str; + default = "!"; + }; + boot.loader.supportsInitrdSecrets = mkOption { internal = true; default = false; @@ -654,7 +411,7 @@ in ]; system.build = - { inherit bootStage1 initialRamdisk initialRamdiskSecretAppender extraUtils; }; + { inherit initialRamdisk initialRamdiskSecretAppender; }; system.requiredKernelConfig = with config.lib.kernelConfig; [ (isYes "TMPFS") @@ -663,5 +420,112 @@ in boot.initrd.supportedFilesystems = map (fs: fs.fsType) fileSystems; + boot.initrd.emergencyPackages = [ + pkgs.bash + pkgs.coreutils + pkgs.kmod + systemd + # TODO: These are actually needed for boot, not just emergency + pkgs.util-linuxMinimal + ]; + + boot.initrd.objects = extraUnitsObjects ++ [ + { + object = "${systemd}/lib/systemd/systemd"; + symlink = "/init"; + executable = true; + } + { + object = "${systemdUnits}"; + symlink = "/etc/systemd"; + } + { + object = builtins.toFile "passwd" "root:x:0:0:System Administrator:/root:/bin/bash"; + symlink = "/etc/passwd"; + } + { + object = builtins.toFile "shadow" "root:${config.boot.initrd.emergencyHashedPassword}:::::::"; + symlink = "/etc/shadow"; + } + # TODO: These are required for emergency mode; figure out which + # parts specifically are needed + { + object = "${pkgs.glibc}/lib"; + executable = true; + } + { + object = config.environment.etc.os-release.source; + symlink = "/etc/initrd-release"; + } + { + object = config.environment.etc.os-release.source; + symlink = "/etc/os-release"; + } + { + object = fstab; + symlink = "/etc/fstab"; + } + { + symlink = "/etc/modules-load.d/nixos.conf"; + object = pkgs.writeText "nixos.conf" + (lib.concatStringsSep "\n" config.boot.initrd.kernelModules); + } + { + object = "${initrdUdevRules}/lib/udev"; + symlink = "/usr/lib/udev"; + } + { + object = "${modulesClosure}/lib/modules"; + symlink = "/lib/modules"; + } + { + object = "${emergencyEnv}/bin/"; + symlink = "/bin"; + executable = true; + } + # Put it at /sbin too so we don't have to set /proc/sys/kernel/modprobe + { + object = "${emergencyEnv}/bin/"; + symlink = "/sbin"; + executable = true; + } + { + symlink = "/etc/bashrc"; + object = pkgs.writeShellScript "bashrc" '' + PATH=${emergencyEnv}/bin + ''; + } + ] ++ map (p: { + object = "${systemd}/${p}"; + executable = true; + }) [ + "lib/systemd/systemd-modules-load" + "bin/systemctl" + "lib/systemd/systemd-udevd" + "bin/udevadm" + "lib/systemd/systemd-journald" + "lib/systemd/systemd-sulogin-shell" + "lib/systemd/system-generators" + "bin/journalctl" + "lib/systemd/systemd-vconsole-setup" + "bin/systemd-tty-ask-password-agent" + "lib/systemd/systemd-shutdown" + "lib/systemd/systemd-makefs" + "lib/systemd/systemd-growfs" + ] ++ map (p: { + object = "${lib.getBin pkgs.lvm2}/${p}"; + executable = true; + }) [ "bin/dmsetup" "bin/lvm" ] ++ map (p: { + object = "${lib.getBin p}/bin"; + executable = true; + }) config.boot.initrd.emergencyPackages; + + # TODO: This doesn't seem like it should be necessary. + # Seems like we're missing some dependency in the default + # unit files since we aren't copying all the default symlinks + boot.initrd.unitOverrides."systemd-udevd.service".modules = pkgs.writeText "modules.conf" '' + [Unit] + After=systemd-modules-load.service + ''; }; } diff --git a/nixos/modules/system/boot/systemd-units.nix b/nixos/modules/system/boot/systemd-units.nix new file mode 100644 index 0000000000000..70a81967a4a99 --- /dev/null +++ b/nixos/modules/system/boot/systemd-units.nix @@ -0,0 +1,41 @@ +{ runCommand, lib, systemd, extraUnits, unitOverrides }: + +runCommand "systemd-units" { } '' + set -euo pipefail + mkdir $out + (cd ${systemd}/example/systemd; find . -type f -o -type l) | while read -r f; do + mkdir -p $out/$(dirname "$f") + cp -d "${systemd}/example/systemd/$f" "$out/$f" + done + + mkdir $out/system/initrd.target.wants + + # For some reason, running 'udevadm info --cleanup-db' in initrd causes + # LUKS devices to get SYSTEMD_READY=0 in stage 2 (but not other devices) + # which prevents LUKS encrypted file systems from being mounted in stage 2. + rm $out/system/initrd-udevadm-cleanup-db.service + + ln -s ../plymouth-start.service $out/system/initrd.target.wants/plymouth-start.service + + rm $out/system/proc-sys-fs-binfmt_misc.automount + rm $out/system/systemd-random-seed.service + rm $out/system/systemd-update-utmp.service + rm $out/system/systemd-update-done.service + rm $out/system/systemd-sysctl.service + rm $out/system/systemd-hwdb-update.service + + rm $out/system/default.target + ln -s initrd.target $out/system/default.target + + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: u: '' + ln -s ${u} $out/system/${n} + ln -s ../${n} $out/system/initrd.target.wants/${n} + '') extraUnits)} + + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: os: '' + mkdir $out/system/${n}.d + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (oname: o: '' + ln -s ${o} $out/system/${n}.d/${oname}.conf + '') os)} + '') unitOverrides)} +'' diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix index e468cb880039c..a34634b154907 100644 --- a/nixos/modules/tasks/filesystems.nix +++ b/nixos/modules/tasks/filesystems.nix @@ -124,7 +124,10 @@ let else if config.fsType == "reiserfs" then "-q" else null; in { - options = mkIf config.autoResize [ "x-nixos.autoresize" ]; + options = mkMerge [ + (mkIf config.autoResize [ "x-systemd.growfs" ]) + (mkIf config.autoFormat [ "x-systemd.makefs" ]) + ]; formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions); }; @@ -272,39 +275,6 @@ in wants = [ "local-fs.target" "remote-fs.target" ]; }; - # Emit systemd services to format requested filesystems. - systemd.services = - let - - formatDevice = fs: - let - mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount"; - device' = escapeSystemdPath fs.device; - device'' = "${device'}.device"; - in nameValuePair "mkfs-${device'}" - { description = "Initialisation of Filesystem ${fs.device}"; - wantedBy = [ mountPoint' ]; - before = [ mountPoint' "systemd-fsck@${device'}.service" ]; - requires = [ device'' ]; - after = [ device'' ]; - path = [ pkgs.util-linux ] ++ config.system.fsPackages; - script = - '' - if ! [ -e "${fs.device}" ]; then exit 1; fi - # FIXME: this is scary. The test could be more robust. - type=$(blkid -p -s TYPE -o value "${fs.device}" || true) - if [ -z "$type" ]; then - echo "creating ${fs.fsType} filesystem on ${fs.device}..." - mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}" - fi - ''; - unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ]; - unitConfig.DefaultDependencies = false; # needed to prevent a cycle - serviceConfig.Type = "oneshot"; - }; - - in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)); - systemd.tmpfiles.rules = [ "d /run/keys 0750 root ${toString config.ids.gids.keys}" "z /run/keys 0750 root ${toString config.ids.gids.keys}" @@ -327,6 +297,29 @@ in "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; }; + # systemd-fstab-generator creates systemd-makefs@dev-foo.service + # units in /run, which takes precedent over + # /etc/systemd/system/systemd-makefs@.service. In order to force + # NixOS to put this PATH config into a drop-in in + # /etc/systemd/system/systemd-makefs@.service.d, add a package + # with a dummy unit. Drop-ins are applied over /run unit files, + # unlike unit files in /etc + systemd.services."systemd-makefs@".path = config.system.fsPackages; + systemd.packages = [(pkgs.runCommand "systemd-makefs" {} '' + mkdir -p $out/lib/systemd/system + ln -s /dev/null $out/lib/systemd/system/systemd-makefs@.service + '')]; + # Support systemd-makefs in initrd as well. Without + # IgnoreOnIsolate, the Requires dependency between sysroot.mount + # and systemd-makefs@dev-vda.service causes the file system to be + # unmounted when the service is stopped by the isolate command. + boot.initrd.unitOverrides."systemd-makefs@.service".mke2fs = pkgs.writeText "mke2fs.conf" '' + [Unit] + IgnoreOnIsolate=yes + [Service] + Environment=PATH=${lib.concatMapStringsSep ":" (p: "${lib.getBin p}/bin") config.system.fsPackages} + ''; + boot.initrd.objects = map (p: { object = "${lib.getBin p}/bin"; executable = true; }) config.system.fsPackages; }; } diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index d9935bcafb716..cdc0775b9c487 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -672,15 +672,32 @@ in # configuration, where the regular value for the `fileSystems' # attribute should be disregarded for the purpose of building a VM # test image (since those filesystems don't exist in the VM). - fileSystems = mkVMOverride ( + fileSystems = let + roStore = { + device = "store"; + fsType = "9p"; + options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ] ++ lib.optional (cfg.msize != null) "msize=${toString cfg.msize}"; + neededForBoot = true; + }; + in mkVMOverride ( cfg.fileSystems // - { "/".device = cfg.bootDevice; - ${if cfg.writableStore then "/nix/.ro-store" else "/nix/store"} = - { device = "store"; - fsType = "9p"; - options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ] ++ lib.optional (cfg.msize != null) "msize=${toString cfg.msize}"; - neededForBoot = true; + { "/" = + { device = cfg.bootDevice; + autoFormat = true; + fsType = "ext4"; }; + "/nix/store" = if !cfg.writableStore then roStore else { + device = "overlay"; + fsType = "overlay"; + options = [ + "lowerdir=/sysroot/nix/.ro-store" + "upperdir=/sysroot/nix/.rw-store/store" + "workdir=/sysroot/nix/.rw-store/work" + "x-systemd.wants=rw-store.service" + "x-systemd.after=rw-store.service" + ]; + }; + "/nix/.ro-store" = mkIf cfg.writableStore roStore; "/tmp" = mkIf config.boot.tmpOnTmpfs { device = "tmpfs"; fsType = "tmpfs"; @@ -714,6 +731,16 @@ in noCheck = true; # fsck fails on a r/o filesystem }; }); + boot.initrd.extraUnits."rw-store.service" = lib.mkIf cfg.writableStore (pkgs.writeText "rw-store.service" '' + [Unit] + Before=sysroot-nix-store.mount + RequiresMountsFor=/sysroot/nix/.rw-store /sysroot/nix/.ro-store + DefaultDependencies=no + [Service] + ExecStart=${pkgs.coreutils}/bin/mkdir -p -m 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/.ro-store /sysroot/nix/store + Type=oneshot + RemainAfterExit=yes + ''); swapDevices = mkVMOverride [ ]; boot.initrd.luks.devices = mkVMOverride {}; 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 0000000000000..6ab3445cb5cb4 --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng-tool.nix @@ -0,0 +1,10 @@ +{ rustPlatform }: + +rustPlatform.buildRustPackage rec { + pname = "make-initrd-ng"; + version = "0.1.0"; + + src = ./make-initrd-ng; + + cargoSha256 = "0vx7whn47372mg19i3vbf7ag5g0ylifx8rin2hcy81fyhmms16h6"; +} 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 0000000000000..f2ef05e46ae0d --- /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, strip-nondeterminism +# 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 strip-nondeterminism]; +} '' + 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 0000000000000..75e732029b511 --- /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 0000000000000..9076f6b156176 --- /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/src/main.rs b/pkgs/build-support/kernel/make-initrd-ng/src/main.rs new file mode 100644 index 0000000000000..25bb246e6fb9e --- /dev/null +++ b/pkgs/build-support/kernel/make-initrd-ng/src/main.rs @@ -0,0 +1,207 @@ +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()); // TODO: Is cloning dumb? + 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 it's not a dynamically linked ELF file, we're done. + if !Command::new("ldd").arg(&source).output()?.status.success() { + 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); + // 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/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 71a063a4393fc..1d7763826b688 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -553,6 +553,9 @@ in 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 = { shell = targetPackages.runtimeShell; }; } ../build-support/setup-hooks/make-wrapper.sh;