add boot.loader.extra-files module, add fileSystems.*.postMountCommands/preUnmountCommands

This commit is contained in:
Niklas Gollenstede 2024-07-24 16:58:03 +02:00
parent 65c1691644
commit 95aa261987
5 changed files with 169 additions and 33 deletions

View File

@ -16,6 +16,7 @@
"acltype", // zfs
"acpi", // abbr
"ahci", // abbr
"armstub", "armstubs", // cocnat
"asDropin", // systemd
"ashift", // zfs
"askpass", // program
@ -56,6 +57,7 @@
"dontStrip", // nixos
"dontUnpack", // nixos
"dosfstools", // package
"dotglob", // option
"draid", // zfs
"e2fsprogs", // package
"elif", // abbr (else if)
@ -140,6 +142,7 @@
"ostype", // virtual box
"overlayed", // word
"overridable", // word
"overscan", // word
"ovmf", // package
"partlabel", // linux
"partprobe", // program / function
@ -197,6 +200,7 @@
"typecode", // cli arg
"uart", "uarts", // serial protocol
"uartmode", // virtual box
"uboot", // package
"udev", // program
"udevadm", // program
"udptunnel", // program

View File

@ -23,17 +23,17 @@ See `nix run .#$hostname -- --help` for options and more commands.
```nix
#*/# end of MarkDown, beginning of NixOS config flake input:
dirname: inputs: { config, pkgs, lib, name, ... }: let lib = inputs.self.lib.__internal__; in let
#suffix = builtins.head (builtins.match ''example-(.*)'' name); # make differences in config based on this when using »preface.instances«
hash = builtins.substring 0 8 (builtins.hashString "sha256" name);
in { preface = { # (any »preface« options have to be defined here)
instances = [ "example-explicit" "example" "example-minimal" "example-crypt" "example-raidz" ]; # Generate multiple variants of this host, with these »name«s.
instances = [ "explicit-fs" "complex-fs" "minimal-setup" "encrypted" "multi-disk-raidz" "rpi" ]; # Generate multiple variants of this host, with these »name«s.
}; imports = [ ({ ## Hardware
nixpkgs.hostPlatform = "x86_64-linux"; system.stateVersion = "22.05";
nixpkgs.hostPlatform = if name == "rpi" then "aarch64-linux" else "x86_64-linux"; system.stateVersion = "24.05";
## What follows is a whole bunch of boilerplate-ish stuff, most of which multiple hosts would have in common and which would thus be moved to one or more modules:
boot.loader.extlinux.enable = true;
boot.loader.extlinux.enable = name != "rpi";
boot.loader.grub.enable = false;
# Example of adding and/or overwriting setup/maintenance functions:
#installer.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1500; };
@ -41,7 +41,7 @@ in { preface = { # (any »preface« options have to be defined here)
boot.initrd.systemd.enable = true;
}) (lib.mkIf (name == "example-explicit") { ## Minimal explicit FS setup
}) (lib.mkIf (name == "explicit-fs") { ## Minimal explicit FS setup
# Declare a boot and system partition.
setup.disks.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; };
@ -60,7 +60,7 @@ in { preface = { # (any »preface« options have to be defined here)
fileSystems."/nix/store" = { options = ["bind,ro"]; device = "/system/nix/store"; neededForBoot = true; };
}) (lib.mkIf (name == "example") { ## More complex but automatic FS setup
}) (lib.mkIf (name == "complex-fs") { ## More complex but automatic FS setup
#setup.disks.devices.primary.size = "16G"; # (default)
setup.bootpart.enable = true; setup.bootpart.size = "512M";
@ -88,7 +88,7 @@ in { preface = { # (any »preface« options have to be defined here)
setup.temproot.local.mounts."/var/log" = lib.mkForce null; # example: don't keep logs
}) (lib.mkIf (name == "example-minimal") { ## Minimal automatic FS setup
}) (lib.mkIf (name == "minimal-setup") { ## Minimal automatic FS setup
setup.bootpart.enable = true; # (also set by »boot.loader.extlinux.enable«)
setup.temproot.enable = true;
@ -98,7 +98,7 @@ in { preface = { # (any »preface« options have to be defined here)
setup.temproot.remote.type = "none";
}) (lib.mkIf (name == "example-crypt") { ## Minimal automatic FS setup
}) (lib.mkIf (name == "encrypted") { ## Encrypted FS setup
setup.keystore.enable = true;
setup.keystore.keys."luks/keystore-${hash}/0" = "random"; # (this makes little practical sense)
@ -117,10 +117,10 @@ in { preface = { # (any »preface« options have to be defined here)
#setup.keystore.keys."luks/rpool-${hash}/0" = "random";
}) (lib.mkIf (name == "example-raidz") { ## Multi-disk ZFS setup
}) (lib.mkIf (name == "multi-disk-raidz") { ## Multi-disk ZFS setup
boot.loader.extlinux.enable = lib.mkForce false; # use UEFI boot this time
boot.loader.systemd-boot.enable = true; boot.loader.grub.enable = false;
boot.loader.systemd-boot.enable = true;
setup.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; });
setup.bootpart.enable = true; setup.bootpart.size = "512M";
@ -140,6 +140,39 @@ in { preface = { # (any »preface« options have to be defined here)
setup.disks.partitions."rpool-arc-${hash}" = { type = "bf00"; };
}) (lib.optionalAttrs (name == "rpi") { ## Booting on Raspberry PIs
# This is mostly a demo for the `extra-files` module, but it does produce an image that boots on a rPI4
setup.temproot = { enable = true; local.bind.base = "ext4"; remote.type = "none"; };
setup.bootpart.enable = true;
boot.loader.generic-extlinux-compatible.enable = true;
boot.loader.extra-files.enable = true;
boot.loader.extra-files.files = let
fw = files: lib.genAttrs files (file: { source = "${pkgs.raspberrypifw}/share/raspberrypi/boot/${file}"; });
in {
"config.txt".format = lib.generators.toINI { listsAsDuplicateKeys = true; };
"config.txt".text = lib.mkOrder 100 ''
# Generated file. Do not edit.
'';
"config.txt".data.all = {
arm_64bit = 1; enable_uart = 1; avoid_warnings = 1;
};
"config.txt".data.pi4 = {
enable_gic = 1; disable_overscan = 1; arm_boost = 1;
kernel = "u-boot-rpi4.bin";
armstub = "armstub8-gic.bin";
};
"u-boot-rpi4.bin".source = "${pkgs.ubootRaspberryPi4_64bit}/u-boot.bin";
"armstub8-gic.bin".source = "${pkgs.raspberrypi-armstubs}/armstub8-gic.bin";
} // (fw [
"start4.elf" "fixup4.dat" "bcm2711-rpi-cm4s.dtb" "bcm2711-rpi-400.dtb" "bcm2711-rpi-4-b.dtb" "bcm2711-rpi-cm4.dtb" "bcm2711-rpi-cm4-io.dtb"
]);
#imports = [ "${inputs.nixos-hardware}/raspberry-pi/4" ]; # activating the correct hardware config should help
#hardware.deviceTree.filter = "bcm271*.dtb";
}) ({ ## Base Config
# Some base config:

View File

@ -23,26 +23,26 @@ dirname: inputs: args@{ config, options, pkgs, lib, ... }: let lib = inputs.self
in {
options = { boot.loader.extlinux = {
enable = lib.mkEnableOption (lib.mdDoc ''
enable = lib.mkEnableOption ''
`extlinux`, a simple bootloader for legacy-BIOS environments, like (by default) Qemu.
This uses the same implementation as `boot.loader.generic-extlinux-compatible` to generate the bootloader configuration, but then actually also installs `extlinux` itself, instead of relying on something else (like an externally installed u-boot) to read and execute the configuration.
Any options affecting the config file generation by `boot.loader.generic-extlinux-compatible` apply, but `boot.loader.generic-extlinux-compatible.enable` should not be set to `true`.
Since the bootloader runs before selecting a generation or specialisation to run, all sub-options, similar to e.g. {option}`boot.loader.timeout`, apply globally to the system, from whichever configuration last applied its bootloader (e.g. by newly `nixos-rebuild switch/boot`ing it or by calling its `.../bin/switch-to-configuration switch/boot`)
'');
package = lib.mkOption { description = lib.mdDoc ''
'';
package = lib.mkOption { description = ''
The `syslinux` package to install `extlinux` from use.
''; type = lib.types.package; default = pkgs.syslinux; defaultText = lib.literalExpression "pkgs.syslinux"; };
targetDir = lib.mkOption { description = lib.mdDoc ''
targetDir = lib.mkOption { description = ''
The path in whose `./extlinux` sub dir `extlinux` will be installed to. When `nixos-rebuild boot/switch` gets called, this or a parent path needs to be mounted from {option}`.targetPart`.
''; type = lib.types.strMatching ''^/.*[^/]$''; default = "/boot"; };
targetPart = lib.mkOption { description = lib.mdDoc ''
targetPart = lib.mkOption { description = ''
The `/dev/disk/by-{id,label,partlabel,partuuid,uuid}/*` path of the *disk partition* holding the filesystem that `extlinux` is installed to. This must be formatted with a filesystem that `extlinux` supports and mounted as (a parent of) {option}`.targetDir`. The disk on which the partition lies will have the bootloader section of its MBR replaced by `extlinux`'s.
''; type = lib.types.strMatching ''^/.*[^/]$''; default = targetMount.device; };
allowInstableTargetPart = lib.mkOption { internal = true; type = lib.types.bool; };
showUI = (lib.mkEnableOption (lib.mdDoc ''
showUI = (lib.mkEnableOption ''
a simple graphical user interface to select the configuration to start during early boot
'')) // { default = true; example = false; };
'') // { default = true; example = false; };
}; };
config = let

View File

@ -0,0 +1,79 @@
/*
# Extra Boot-Partition Files
This module allows copying additional files to the boot partition when installing/updating the bootloader.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, options, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup;
cfg = config.boot.loader.extra-files;
targetMount = let path = lib.findFirst (path: config.fileSystems?${path}) "/" (lib.fun.parentPaths cfg.targetDir); in config.fileSystems.${path};
supportedFSes = [ "vfat" "ntfs" "ext2" "ext3" "ext4" "btrfs" "xfs" "ufs" ]; fsSupported = fs: builtins.elem fs supportedFSes;
in {
options = { boot.loader.extra-files = {
enable = lib.mkEnableOption ''
copying of additional files to the boot partition during the system's installation and activation.
Note that this modifies the global, non-versioned bootloader state based on the last generation(s) installed / switched to, and that it only ensures the files existence, possibly overwriting previous files, but does not delete files (left by previous generations or configurations)
'';
files = lib.mkOption { description = ''
...
''; example = lib.literalExpression ''
{ "config.txt".text = lib.mkAfter "disable_splash=1";
"start4.elf".source = "${pkgs.raspberrypifw}/share/raspberrypi/boot/start4.elf";
}
''; default = { }; type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, config, ... }: { options = {
source = lib.mkOption { description = "Source path of the file."; type = lib.types.path; };
text = lib.mkOption { description = "Text lines of the file."; default = null; type = lib.types.nullOr lib.types.lines; };
format = lib.mkOption { description = "Formatter to use to transform `.data` into `.text` lines (if `!= null`)."; default = null; type = lib.types.nullOr (lib.types.functionTo lib.types.str); };
data = lib.mkOption { description = "Data to serialize to become the files `.text`. A `.format`ter must be set for the file for this to become applicable, and the data assigned must conform to the particular formatters input requirements."; type = (pkgs.formats.json { }).type; };
}; config = {
text = lib.mkIf (config.format != null) (config.format config.data);
}; }))); apply = lib.filterAttrs (k: v: v != null); };
tree = lib.mkOption { internal = true; readOnly = true; type = lib.types.package; };
targetDir = lib.mkOption { description = ''
The where the files will be installed (copied) to. This has to be mounted when `nixos-rebuild boot/switch` gets called.
''; type = lib.types.strMatching ''^/.*[^/]$''; default = "/boot"; };
}; } // {
system.build.installBootLoader = lib.mkOption { apply = old: if !cfg.enable then old else pkgs.writeShellScript "${old.name or "install-bootloader"}-extra-files" ''
${old} "$@" || exit
shopt -s dotglob # for the rest of the script, have globs (*) also match hidden files
function copy () { # 1: src, 2: dst
cd "$1" ; for name in * ; do
if [[ -d "$1"/"$name" ]] ; then
rm "$2"/"$name" 2>/dev/null || true
mkdir -p "$2"/"$name" ; copy "$1"/"$name" "$2"/"$name"
else
if ! ${pkgs.diffutils}/bin/cmp --quiet "$1"/"$name" "$2"/"$name" ; then
cp -a "$1"/"$name" "$2"/"$name"
fi
fi
done
}
copy ${cfg.tree} ${lib.escapeShellArg cfg.targetDir}
''; };
};
config = {
# This copies referenced files to reduce the installation's closure size.
boot.loader.extra-files.tree = lib.fun.writeTextFiles pkgs "boot-files" { checkPhase = ''
${lib.concatStringsSep "\n" (lib.mapAttrsToList (path: file: ''
path=${lib.escapeShellArg path} ; mkdir -p "$( dirname "$path" )"
cp -aT ${lib.escapeShellArg file.source} "$path"
'') (lib.filterAttrs (__: _:_.text == null) cfg.files))}
''; } (lib.fun.catAttrSets "text" (lib.filterAttrs (__: _:_.text != null) cfg.files));
};
}

View File

@ -17,6 +17,13 @@ in {
Note that if a symlink exists at a mount point when systemd's fstab-generator runs, it will read/resolve the symlink and use the link's target as the mount point, resulting in mismatching unit names for that mount, effectively disabling its `.preMountCommands`.
This does not (apparently and unfortunately) run when mounting via the `mount` command (and probably not with the `mount` system call either).
''; type = lib.types.lines; default = ""; };
postMountCommands = lib.mkOption { description = ''
Like `.preMountCommands`, but runs after mounting the filesystem.
''; type = lib.types.lines; default = ""; };
preUnmountCommands = lib.mkOption { description = ''
Like `.preMountCommands`, but runs before unmounting the filesystem.
This will only run before unmounting when the FS is unmounted by systemd before the FS is unmounted
''; type = lib.types.lines; default = ""; };
postUnmountCommands = lib.mkOption { description = ''
Like `.preMountCommands`, but runs after unmounting the filesystem.
''; type = lib.types.lines; default = ""; };
@ -32,27 +39,40 @@ in {
}) config.fileSystems);
# The implementation is derived from the "mkfs-${device'}" service in nixpkgs.
services = initrd: lib.fun.mapMergeUnique (_: fs@{ mountPoint, device, depends, ... }: if
(fs.preMountCommands != "" || fs.postUnmountCommands != "") && initrd == utils.fsNeededForBoot fs
then let
mkServices = inInitrd: lib.fun.mapMergeUnique (_: fs@{ mountPoint, device, depends, ... }: let
isDevice = lib.fun.startsWith "/dev/" device;
mountPoint' = utils.escapeSystemdPath mountPoint;
mountPoint' = utils.escapeSystemdPath mountPoint; mountPointDep = [ "${mountPoint'}.mount" ];
device' = utils.escapeSystemdPath device;
in { "pre-mount-${mountPoint'}" = rec { # TODO: in initrd (or during installation), how to deal with the fact that the system is not mounted at "/"?
description = "Prepare mounting ${device} at ${mountPoint}";
wantedBy = [ "${mountPoint'}.mount" ]; before = wantedBy; partOf = wantedBy;
requires = lib.optional isDevice "${device'}.device"; after = lib.optional isDevice "${device'}.device";
unitConfig.RequiresMountsFor = map utils.escapeSystemdExecArg (depends ++ (lib.optional (lib.hasPrefix "/" device) device) ++ [ (builtins.dirOf mountPoint) ]);
unitConfig.DefaultDependencies = false; restartIfChanged = false;
serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true;
script = lib.mkIf (fs.preMountCommands != "") fs.preMountCommands;
preStop = lib.mkIf (fs.postUnmountCommands != "") fs.postUnmountCommands; # ("preStop" still runs post unmount)
}; } else { }) config.fileSystems;
mkService = when: { # TODO: in initrd (or during installation), how to deal with the fact that the system is mounted at "/sysroot"?
description = "Prepare mounting ${device} at ${mountPoint}";
wantedBy = mountPointDep; ${if when != "after" then when else null} = mountPointDep; partOf = mountPointDep;
requires = lib.optional isDevice "${device'}.device"; after = (lib.optionals (when == "after") mountPointDep) ++ (lib.optional isDevice "${device'}.device");
unitConfig.RequiresMountsFor = map utils.escapeSystemdExecArg (depends ++ (lib.optional (lib.hasPrefix "/" device) device) ++ [ (builtins.dirOf mountPoint) ]);
unitConfig.DefaultDependencies = false; restartIfChanged = false;
serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true;
};
prepare = ''
#set -x
'';
in lib.optionalAttrs (inInitrd == utils.fsNeededForBoot fs) (
(lib.optionalAttrs (
fs.preMountCommands != "" || fs.postUnmountCommands != ""
) { "${mountPoint'}-before" = (mkService "before") // {
script = lib.mkIf (fs.preMountCommands != "") (prepare + fs.preMountCommands);
preStop = lib.mkIf (fs.postUnmountCommands != "") (prepare + fs.postUnmountCommands); # ("preStop" still runs post unmount)
}; })
// (lib.optionalAttrs (
fs.postMountCommands != "" || fs.preUnmountCommands != ""
) { "${mountPoint'}-after" = (mkService "after") // {
script = lib.mkIf (fs.postMountCommands != "") (prepare + fs.postMountCommands);
preStop = lib.mkIf (fs.preUnmountCommands != "") (prepare + fs.preUnmountCommands);
}; })
)) config.fileSystems;
in {
inherit assertions;
systemd.services = services false;
boot.initrd.systemd.services = lib.mkIf (config.boot.initrd.systemd.enable) (services true);
systemd.services = mkServices false;
boot.initrd.systemd.services = lib.mkIf (config.boot.initrd.systemd.enable) (mkServices true);
};
}