diff options
Diffstat (limited to 'nixos/modules/system/boot/loader')
20 files changed, 3357 insertions, 0 deletions
diff --git a/nixos/modules/system/boot/loader/efi.nix b/nixos/modules/system/boot/loader/efi.nix new file mode 100644 index 00000000000..6043c904c45 --- /dev/null +++ b/nixos/modules/system/boot/loader/efi.nix @@ -0,0 +1,20 @@ +{ lib, ... }: + +with lib; + +{ + options.boot.loader.efi = { + + canTouchEfiVariables = mkOption { + default = false; + type = types.bool; + description = "Whether the installation process is allowed to modify EFI boot variables."; + }; + + efiSysMountPoint = mkOption { + default = "/boot"; + type = types.str; + description = "Where the EFI System Partition is mounted."; + }; + }; +} diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh new file mode 100644 index 00000000000..8ae23dc988c --- /dev/null +++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh @@ -0,0 +1,106 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +default=$1 +if test -z "$1"; then + echo "Syntax: generations-dir-builder.sh <DEFAULT-CONFIG>" + exit 1 +fi + +echo "updating the boot generations directory..." + +mkdir -p /boot + +rm -Rf /boot/system* || true + +target=/boot/grub/menu.lst +tmp=$target.tmp + +# Convert a path to a file in the Nix store such as +# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>. +cleanName() { + local path="$1" + echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g' +} + +# Copy a file from the Nix store to /boot/kernels. +declare -A filesCopied + +copyToKernelsDir() { + local src="$1" + local dst="/boot/kernels/$(cleanName $src)" + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if ! test -e $dst; then + local dstTmp=$dst.tmp.$$ + cp $src $dstTmp + mv $dstTmp $dst + fi + filesCopied[$dst]=1 + result=$dst +} + + +# Copy its kernel and initrd to /boot/kernels. +addEntry() { + local path="$1" + local generation="$2" + local outdir=/boot/system-$generation + + if ! test -e $path/kernel -a -e $path/initrd; then + return + fi + + local kernel=$(readlink -f $path/kernel) + local initrd=$(readlink -f $path/initrd) + + if test -n "@copyKernels@"; then + copyToKernelsDir $kernel; kernel=$result + copyToKernelsDir $initrd; initrd=$result + fi + + mkdir -p $outdir + ln -sf $(readlink -f $path) $outdir/system + ln -sf $(readlink -f $path/init) $outdir/init + ln -sf $initrd $outdir/initrd + ln -sf $kernel $outdir/kernel + + if test $(readlink -f "$path") = "$default"; then + cp "$kernel" /boot/nixos-kernel + cp "$initrd" /boot/nixos-initrd + cp "$(readlink -f "$path/init")" /boot/nixos-init + + mkdir -p /boot/default + # ln -sfT: overrides target even if it exists. + ln -sfT $(readlink -f $path) /boot/default/system + ln -sfT $(readlink -f $path/init) /boot/default/init + ln -sfT $initrd /boot/default/initrd + ln -sfT $kernel /boot/default/kernel + fi +} + +if test -n "@copyKernels@"; then + mkdir -p /boot/kernels +fi + +# Add all generations of the system profile to the menu, in reverse +# (most recent to least recent) order. +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + addEntry $link $generation +done + +# Remove obsolete files from /boot/kernels. +for fn in /boot/kernels/*; do + if ! test "${filesCopied[$fn]}" = 1; then + rm -vf -- "$fn" + fi +done diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix new file mode 100644 index 00000000000..1437ab38770 --- /dev/null +++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix @@ -0,0 +1,62 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + generationsDirBuilder = pkgs.substituteAll { + src = ./generations-dir-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + inherit (config.boot.loader.generationsDir) copyKernels; + }; + +in + +{ + options = { + + boot.loader.generationsDir = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to create symlinks to the system generations under + <literal>/boot</literal>. When enabled, + <literal>/boot/default/kernel</literal>, + <literal>/boot/default/initrd</literal>, etc., are updated to + point to the current generation's kernel image, initial RAM + disk, and other bootstrap files. + + This optional is not necessary with boot loaders such as GNU GRUB + for which the menu is updated to point to the latest bootstrap + files. However, it is needed for U-Boot on platforms where the + boot command line is stored in flash memory rather than in a + menu file. + ''; + }; + + copyKernels = mkOption { + default = false; + type = types.bool; + description = '' + Whether copy the necessary boot files into /boot, so + /nix/store is not needed by the boot loader. + ''; + }; + + }; + + }; + + + config = mkIf config.boot.loader.generationsDir.enable { + + system.build.installBootLoader = generationsDirBuilder; + system.boot.loader.id = "generationsDir"; + system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target; + + }; +} diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix b/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix new file mode 100644 index 00000000000..545b594674f --- /dev/null +++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix @@ -0,0 +1,82 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + blCfg = config.boot.loader; + dtCfg = config.hardware.deviceTree; + cfg = blCfg.generic-extlinux-compatible; + + timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout; + + # The builder used to write during system activation + builder = import ./extlinux-conf-builder.nix { inherit pkgs; }; + # The builder exposed in populateCmd, which runs on the build architecture + populateBuilder = import ./extlinux-conf-builder.nix { pkgs = pkgs.buildPackages; }; +in +{ + options = { + boot.loader.generic-extlinux-compatible = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to generate an extlinux-compatible configuration file + under <literal>/boot/extlinux.conf</literal>. For instance, + U-Boot's generic distro boot support uses this file format. + + See <link xlink:href="http://git.denx.de/?p=u-boot.git;a=blob;f=doc/README.distro;hb=refs/heads/master">U-boot's documentation</link> + for more information. + ''; + }; + + useGenerationDeviceTree = mkOption { + default = true; + type = types.bool; + description = '' + Whether to generate Device Tree-related directives in the + extlinux configuration. + + When enabled, the bootloader will attempt to load the device + tree binaries from the generation's kernel. + + Note that this affects all generations, regardless of the + setting value used in their configurations. + ''; + }; + + configurationLimit = mkOption { + default = 20; + example = 10; + type = types.int; + description = '' + Maximum number of configurations in the boot menu. + ''; + }; + + populateCmd = mkOption { + type = types.str; + readOnly = true; + description = '' + Contains the builder command used to populate an image, + honoring all options except the <literal>-c <path-to-default-configuration></literal> + argument. + Useful to have for sdImage.populateRootCommands + ''; + }; + + }; + }; + + config = let + builderArgs = "-g ${toString cfg.configurationLimit} -t ${timeoutStr}" + + lib.optionalString (dtCfg.name != null) " -n ${dtCfg.name}" + + lib.optionalString (!cfg.useGenerationDeviceTree) " -r"; + in + mkIf cfg.enable { + system.build.installBootLoader = "${builder} ${builderArgs} -c"; + system.boot.loader.id = "generic-extlinux-compatible"; + + boot.loader.generic-extlinux-compatible.populateCmd = "${populateBuilder} ${builderArgs}"; + }; +} diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix new file mode 100644 index 00000000000..576a07c1d27 --- /dev/null +++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix @@ -0,0 +1,8 @@ +{ pkgs }: + +pkgs.substituteAll { + src = ./extlinux-conf-builder.sh; + isExecutable = true; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + inherit (pkgs) bash; +} diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh new file mode 100644 index 00000000000..1a0da005029 --- /dev/null +++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh @@ -0,0 +1,157 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +usage() { + echo "usage: $0 -t <timeout> -c <path-to-default-configuration> [-d <boot-dir>] [-g <num-generations>] [-n <dtbName>] [-r]" >&2 + exit 1 +} + +timeout= # Timeout in centiseconds +default= # Default configuration +target=/boot # Target directory +numGenerations=0 # Number of other generations to include in the menu + +while getopts "t:c:d:g:n:r" opt; do + case "$opt" in + t) # U-Boot interprets '0' as infinite and negative as instant boot + if [ "$OPTARG" -lt 0 ]; then + timeout=0 + elif [ "$OPTARG" = 0 ]; then + timeout=-10 + else + timeout=$((OPTARG * 10)) + fi + ;; + c) default="$OPTARG" ;; + d) target="$OPTARG" ;; + g) numGenerations="$OPTARG" ;; + n) dtbName="$OPTARG" ;; + r) noDeviceTree=1 ;; + \?) usage ;; + esac +done + +[ "$timeout" = "" -o "$default" = "" ] && usage + +mkdir -p $target/nixos +mkdir -p $target/extlinux + +# Convert a path to a file in the Nix store such as +# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>. +cleanName() { + local path="$1" + echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g' +} + +# Copy a file from the Nix store to $target/nixos. +declare -A filesCopied + +copyToKernelsDir() { + local src=$(readlink -f "$1") + local dst="$target/nixos/$(cleanName $src)" + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if ! test -e $dst; then + local dstTmp=$dst.tmp.$$ + cp -r $src $dstTmp + mv $dstTmp $dst + fi + filesCopied[$dst]=1 + result=$dst +} + +# Copy its kernel, initrd and dtbs to $target/nixos, and echo out an +# extlinux menu entry +addEntry() { + local path=$(readlink -f "$1") + local tag="$2" # Generation number or 'default' + + if ! test -e $path/kernel -a -e $path/initrd; then + return + fi + + copyToKernelsDir "$path/kernel"; kernel=$result + copyToKernelsDir "$path/initrd"; initrd=$result + dtbDir=$(readlink -m "$path/dtbs") + if [ -e "$dtbDir" ]; then + copyToKernelsDir "$dtbDir"; dtbs=$result + fi + + timestampEpoch=$(stat -L -c '%Z' $path) + + timestamp=$(date "+%Y-%m-%d %H:%M" -d @$timestampEpoch) + nixosLabel="$(cat $path/nixos-version)" + extraParams="$(cat $path/kernel-params)" + + echo + echo "LABEL nixos-$tag" + if [ "$tag" = "default" ]; then + echo " MENU LABEL NixOS - Default" + else + echo " MENU LABEL NixOS - Configuration $tag ($timestamp - $nixosLabel)" + fi + echo " LINUX ../nixos/$(basename $kernel)" + echo " INITRD ../nixos/$(basename $initrd)" + echo " APPEND init=$path/init $extraParams" + + if [ -n "$noDeviceTree" ]; then + return + fi + + if [ -d "$dtbDir" ]; then + # if a dtbName was specified explicitly, use that, else use FDTDIR + if [ -n "$dtbName" ]; then + echo " FDT ../nixos/$(basename $dtbs)/${dtbName}" + else + echo " FDTDIR ../nixos/$(basename $dtbs)" + fi + else + if [ -n "$dtbName" ]; then + echo "Explicitly requested dtbName $dtbName, but there's no FDTDIR - bailing out." >&2 + exit 1 + fi + fi +} + +tmpFile="$target/extlinux/extlinux.conf.tmp.$$" + +cat > $tmpFile <<EOF +# Generated file, all changes will be lost on nixos-rebuild! + +# Change this to e.g. nixos-42 to temporarily boot to an older configuration. +DEFAULT nixos-default + +MENU TITLE ------------------------------------------------------------ +TIMEOUT $timeout +EOF + +addEntry $default default >> $tmpFile + +if [ "$numGenerations" -gt 0 ]; then + # Add up to $numGenerations generations of the system profile to the menu, + # in reverse (most recent to least recent) order. + for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r \ + | head -n $numGenerations); do + link=/nix/var/nix/profiles/system-$generation-link + addEntry $link $generation + done >> $tmpFile +fi + +mv -f $tmpFile $target/extlinux/extlinux.conf + +# Remove obsolete files from $target/nixos. +for fn in $target/nixos/*; do + if ! test "${filesCopied[$fn]}" = 1; then + echo "Removing no longer needed boot file: $fn" + chmod +w -- "$fn" + rm -rf -- "$fn" + fi +done diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix new file mode 100644 index 00000000000..8db271f8713 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/grub.nix @@ -0,0 +1,848 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.boot.loader.grub; + + efi = config.boot.loader.efi; + + grubPkgs = + # Package set of targeted architecture + if cfg.forcei686 then pkgs.pkgsi686Linux else pkgs; + + realGrub = if cfg.version == 1 then grubPkgs.grub + else if cfg.zfsSupport then grubPkgs.grub2.override { zfsSupport = true; } + else if cfg.trustedBoot.enable + then if cfg.trustedBoot.isHPLaptop + then grubPkgs.trustedGrub-for-HP + else grubPkgs.trustedGrub + else grubPkgs.grub2; + + grub = + # Don't include GRUB if we're only generating a GRUB menu (e.g., + # in EC2 instances). + if cfg.devices == ["nodev"] + then null + else realGrub; + + grubEfi = + # EFI version of Grub v2 + if cfg.efiSupport && (cfg.version == 2) + then realGrub.override { efiSupport = cfg.efiSupport; } + else null; + + f = x: if x == null then "" else "" + x; + + grubConfig = args: + let + efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint; + efiSysMountPoint' = replaceChars [ "/" ] [ "-" ] efiSysMountPoint; + in + pkgs.writeText "grub-config.xml" (builtins.toXML + { splashImage = f cfg.splashImage; + splashMode = f cfg.splashMode; + backgroundColor = f cfg.backgroundColor; + grub = f grub; + grubTarget = f (grub.grubTarget or ""); + shell = "${pkgs.runtimeShell}"; + fullName = lib.getName realGrub; + fullVersion = lib.getVersion realGrub; + grubEfi = f grubEfi; + grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else ""; + bootPath = args.path; + storePath = config.boot.loader.grub.storePath; + bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId; + timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout; + users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts."; + theme = f cfg.theme; + inherit efiSysMountPoint; + inherit (args) devices; + inherit (efi) canTouchEfiVariables; + inherit (cfg) + version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber + extraGrubInstallArgs + extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels + default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios; + path = with pkgs; makeBinPath ( + [ coreutils gnused gnugrep findutils diffutils btrfs-progs util-linux mdadm ] + ++ optional (cfg.efiSupport && (cfg.version == 2)) efibootmgr + ++ optionals cfg.useOSProber [ busybox os-prober ]); + font = if cfg.font == null then "" + else (if lib.last (lib.splitString "." cfg.font) == "pf2" + then cfg.font + else "${convertedFont}"); + }); + + bootDeviceCounters = foldr (device: attr: attr // { ${device} = (attr.${device} or 0) + 1; }) {} + (concatMap (args: args.devices) cfg.mirroredBoots); + + convertedFont = (pkgs.runCommand "grub-font-converted.pf2" {} + (builtins.concatStringsSep " " + ([ "${realGrub}/bin/grub-mkfont" + cfg.font + "--output" "$out" + ] ++ (optional (cfg.fontSize!=null) "--size ${toString cfg.fontSize}"))) + ); + + defaultSplash = pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath; +in + +{ + + ###### interface + + options = { + + boot.loader.grub = { + + enable = mkOption { + default = !config.boot.isContainer; + defaultText = literalExpression "!config.boot.isContainer"; + type = types.bool; + description = '' + Whether to enable the GNU GRUB boot loader. + ''; + }; + + version = mkOption { + default = 2; + example = 1; + type = types.int; + description = '' + The version of GRUB to use: <literal>1</literal> for GRUB + Legacy (versions 0.9x), or <literal>2</literal> (the + default) for GRUB 2. + ''; + }; + + device = mkOption { + default = ""; + example = "/dev/disk/by-id/wwn-0x500001234567890a"; + type = types.str; + description = '' + The device on which the GRUB boot loader will be installed. + The special value <literal>nodev</literal> means that a GRUB + boot menu will be generated, but GRUB itself will not + actually be installed. To install GRUB on multiple devices, + use <literal>boot.loader.grub.devices</literal>. + ''; + }; + + devices = mkOption { + default = []; + example = [ "/dev/disk/by-id/wwn-0x500001234567890a" ]; + type = types.listOf types.str; + description = '' + The devices on which the boot loader, GRUB, will be + installed. Can be used instead of <literal>device</literal> to + install GRUB onto multiple devices. + ''; + }; + + users = mkOption { + default = {}; + example = { + root = { hashedPasswordFile = "/path/to/file"; }; + }; + description = '' + User accounts for GRUB. When specified, the GRUB command line and + all boot options except the default are password-protected. + All passwords and hashes provided will be stored in /boot/grub/grub.cfg, + and will be visible to any local user who can read this file. Additionally, + any passwords and hashes provided directly in a Nix configuration + (as opposed to external files) will be copied into the Nix store, and + will be visible to all local users. + ''; + type = with types; attrsOf (submodule { + options = { + hashedPasswordFile = mkOption { + example = "/path/to/file"; + default = null; + type = with types; uniq (nullOr str); + description = '' + Specifies the path to a file containing the password hash + for the account, generated with grub-mkpasswd-pbkdf2. + This hash will be stored in /boot/grub/grub.cfg, and will + be visible to any local user who can read this file. + ''; + }; + hashedPassword = mkOption { + example = "grub.pbkdf2.sha512.10000.674DFFDEF76E13EA...2CC972B102CF4355"; + default = null; + type = with types; uniq (nullOr str); + description = '' + Specifies the password hash for the account, + generated with grub-mkpasswd-pbkdf2. + This hash will be copied to the Nix store, and will be visible to all local users. + ''; + }; + passwordFile = mkOption { + example = "/path/to/file"; + default = null; + type = with types; uniq (nullOr str); + description = '' + Specifies the path to a file containing the + clear text password for the account. + This password will be stored in /boot/grub/grub.cfg, and will + be visible to any local user who can read this file. + ''; + }; + password = mkOption { + example = "Pa$$w0rd!"; + default = null; + type = with types; uniq (nullOr str); + description = '' + Specifies the clear text password for the account. + This password will be copied to the Nix store, and will be visible to all local users. + ''; + }; + }; + }); + }; + + mirroredBoots = mkOption { + default = [ ]; + example = [ + { path = "/boot1"; devices = [ "/dev/disk/by-id/wwn-0x500001234567890a" ]; } + { path = "/boot2"; devices = [ "/dev/disk/by-id/wwn-0x500009876543210a" ]; } + ]; + description = '' + Mirror the boot configuration to multiple partitions and install grub + to the respective devices corresponding to those partitions. + ''; + + type = with types; listOf (submodule { + options = { + + path = mkOption { + example = "/boot1"; + type = types.str; + description = '' + The path to the boot directory where GRUB will be written. Generally + this boot path should double as an EFI path. + ''; + }; + + efiSysMountPoint = mkOption { + default = null; + example = "/boot1/efi"; + type = types.nullOr types.str; + description = '' + The path to the efi system mount point. Usually this is the same + partition as the above path and can be left as null. + ''; + }; + + efiBootloaderId = mkOption { + default = null; + example = "NixOS-fsid"; + type = types.nullOr types.str; + description = '' + The id of the bootloader to store in efi nvram. + The default is to name it NixOS and append the path or efiSysMountPoint. + This is only used if <literal>boot.loader.efi.canTouchEfiVariables</literal> is true. + ''; + }; + + devices = mkOption { + default = [ ]; + example = [ "/dev/disk/by-id/wwn-0x500001234567890a" "/dev/disk/by-id/wwn-0x500009876543210a" ]; + type = types.listOf types.str; + description = '' + The path to the devices which will have the GRUB MBR written. + Note these are typically device paths and not paths to partitions. + ''; + }; + + }; + }); + }; + + configurationName = mkOption { + default = ""; + example = "Stable 2.6.21"; + type = types.str; + description = '' + GRUB entry name instead of default. + ''; + }; + + storePath = mkOption { + default = "/nix/store"; + type = types.str; + description = '' + Path to the Nix store when looking for kernels at boot. + Only makes sense when copyKernels is false. + ''; + }; + + extraPrepareConfig = mkOption { + default = ""; + type = types.lines; + description = '' + Additional bash commands to be run at the script that + prepares the GRUB menu entries. + ''; + }; + + extraConfig = mkOption { + default = ""; + example = '' + serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1 + terminal_input --append serial + terminal_output --append serial + ''; + type = types.lines; + description = '' + Additional GRUB commands inserted in the configuration file + just before the menu entries. + ''; + }; + + extraGrubInstallArgs = mkOption { + default = [ ]; + example = [ "--modules=nativedisk ahci pata part_gpt part_msdos diskfilter mdraid1x lvm ext2" ]; + type = types.listOf types.str; + description = '' + Additional arguments passed to <literal>grub-install</literal>. + + A use case for this is to build specific GRUB2 modules + directly into the GRUB2 kernel image, so that they are available + and activated even in the <literal>grub rescue</literal> shell. + + They are also necessary when the BIOS/UEFI is bugged and cannot + correctly read large disks (e.g. above 2 TB), so GRUB2's own + <literal>nativedisk</literal> and related modules can be used + to use its own disk drivers. The example shows one such case. + This is also useful for booting from USB. + See the + <link xlink:href="http://git.savannah.gnu.org/cgit/grub.git/tree/grub-core/commands/nativedisk.c?h=grub-2.04#n326"> + GRUB source code + </link> + for which disk modules are available. + + The list elements are passed directly as <literal>argv</literal> + arguments to the <literal>grub-install</literal> program, in order. + ''; + }; + + extraInstallCommands = mkOption { + default = ""; + example = '' + # the example below generates detached signatures that GRUB can verify + # https://www.gnu.org/software/grub/manual/grub/grub.html#Using-digital-signatures + ''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -name '*.sig' -delete + old_gpg_home=$GNUPGHOME + export GNUPGHOME="$(mktemp -d)" + ''${pkgs.gnupg}/bin/gpg --import ''${priv_key} > /dev/null 2>&1 + ''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -exec ''${pkgs.gnupg}/bin/gpg --detach-sign "{}" \; > /dev/null 2>&1 + rm -rf $GNUPGHOME + export GNUPGHOME=$old_gpg_home + ''; + type = types.lines; + description = '' + Additional shell commands inserted in the bootloader installer + script after generating menu entries. + ''; + }; + + extraPerEntryConfig = mkOption { + default = ""; + example = "root (hd0)"; + type = types.lines; + description = '' + Additional GRUB commands inserted in the configuration file + at the start of each NixOS menu entry. + ''; + }; + + extraEntries = mkOption { + default = ""; + type = types.lines; + example = '' + # GRUB 1 example (not GRUB 2 compatible) + title Windows + chainloader (hd0,1)+1 + + # GRUB 2 example + menuentry "Windows 7" { + chainloader (hd0,4)+1 + } + + # GRUB 2 with UEFI example, chainloading another distro + menuentry "Fedora" { + set root=(hd1,1) + chainloader /efi/fedora/grubx64.efi + } + ''; + description = '' + Any additional entries you want added to the GRUB boot menu. + ''; + }; + + extraEntriesBeforeNixOS = mkOption { + default = false; + type = types.bool; + description = '' + Whether extraEntries are included before the default option. + ''; + }; + + extraFiles = mkOption { + type = types.attrsOf types.path; + default = {}; + example = literalExpression '' + { "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; } + ''; + description = '' + A set of files to be copied to <filename>/boot</filename>. + Each attribute name denotes the destination file name in + <filename>/boot</filename>, while the corresponding + attribute value specifies the source file. + ''; + }; + + useOSProber = mkOption { + default = false; + type = types.bool; + description = '' + If set to true, append entries for other OSs detected by os-prober. + ''; + }; + + splashImage = mkOption { + type = types.nullOr types.path; + example = literalExpression "./my-background.png"; + description = '' + Background image used for GRUB. + Set to <literal>null</literal> to run GRUB in text mode. + + <note><para> + For grub 1: + It must be a 640x480, + 14-colour image in XPM format, optionally compressed with + <command>gzip</command> or <command>bzip2</command>. + </para></note> + + <note><para> + For grub 2: + File must be one of .png, .tga, .jpg, or .jpeg. JPEG images must + not be progressive. + The image will be scaled if necessary to fit the screen. + </para></note> + ''; + }; + + backgroundColor = mkOption { + type = types.nullOr types.str; + example = "#7EBAE4"; + default = null; + description = '' + Background color to be used for GRUB to fill the areas the image isn't filling. + + <note><para> + This options has no effect for GRUB 1. + </para></note> + ''; + }; + + theme = mkOption { + type = types.nullOr types.path; + example = literalExpression "pkgs.nixos-grub2-theme"; + default = null; + description = '' + Grub theme to be used. + + <note><para> + This options has no effect for GRUB 1. + </para></note> + ''; + }; + + splashMode = mkOption { + type = types.enum [ "normal" "stretch" ]; + default = "stretch"; + description = '' + Whether to stretch the image or show the image in the top-left corner unstretched. + + <note><para> + This options has no effect for GRUB 1. + </para></note> + ''; + }; + + font = mkOption { + type = types.nullOr types.path; + default = "${realGrub}/share/grub/unicode.pf2"; + defaultText = literalExpression ''"''${pkgs.grub2}/share/grub/unicode.pf2"''; + description = '' + Path to a TrueType, OpenType, or pf2 font to be used by Grub. + ''; + }; + + fontSize = mkOption { + type = types.nullOr types.int; + example = 16; + default = null; + description = '' + Font size for the grub menu. Ignored unless <literal>font</literal> + is set to a ttf or otf font. + ''; + }; + + gfxmodeEfi = mkOption { + default = "auto"; + example = "1024x768"; + type = types.str; + description = '' + The gfxmode to pass to GRUB when loading a graphical boot interface under EFI. + ''; + }; + + gfxmodeBios = mkOption { + default = "1024x768"; + example = "auto"; + type = types.str; + description = '' + The gfxmode to pass to GRUB when loading a graphical boot interface under BIOS. + ''; + }; + + gfxpayloadEfi = mkOption { + default = "keep"; + example = "text"; + type = types.str; + description = '' + The gfxpayload to pass to GRUB when loading a graphical boot interface under EFI. + ''; + }; + + gfxpayloadBios = mkOption { + default = "text"; + example = "keep"; + type = types.str; + description = '' + The gfxpayload to pass to GRUB when loading a graphical boot interface under BIOS. + ''; + }; + + configurationLimit = mkOption { + default = 100; + example = 120; + type = types.int; + description = '' + Maximum of configurations in boot menu. GRUB has problems when + there are too many entries. + ''; + }; + + copyKernels = mkOption { + default = false; + type = types.bool; + description = '' + Whether the GRUB menu builder should copy kernels and initial + ramdisks to /boot. This is done automatically if /boot is + on a different partition than /. + ''; + }; + + default = mkOption { + default = "0"; + type = types.either types.int types.str; + apply = toString; + description = '' + Index of the default menu item to be booted. + Can also be set to "saved", which will make GRUB select + the menu item that was used at the last boot. + ''; + }; + + fsIdentifier = mkOption { + default = "uuid"; + type = types.enum [ "uuid" "label" "provided" ]; + description = '' + Determines how GRUB will identify devices when generating the + configuration file. A value of uuid / label signifies that grub + will always resolve the uuid or label of the device before using + it in the configuration. A value of provided means that GRUB will + use the device name as show in <command>df</command> or + <command>mount</command>. Note, zfs zpools / datasets are ignored + and will always be mounted using their labels. + ''; + }; + + zfsSupport = mkOption { + default = false; + type = types.bool; + description = '' + Whether GRUB should be built against libzfs. + ZFS support is only available for GRUB v2. + This option is ignored for GRUB v1. + ''; + }; + + efiSupport = mkOption { + default = false; + type = types.bool; + description = '' + Whether GRUB should be built with EFI support. + EFI support is only available for GRUB v2. + This option is ignored for GRUB v1. + ''; + }; + + efiInstallAsRemovable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to invoke <literal>grub-install</literal> with + <literal>--removable</literal>.</para> + + <para>Unless you turn this on, GRUB will install itself somewhere in + <literal>boot.loader.efi.efiSysMountPoint</literal> (exactly where + depends on other config variables). If you've set + <literal>boot.loader.efi.canTouchEfiVariables</literal> *AND* you + are currently booted in UEFI mode, then GRUB will use + <literal>efibootmgr</literal> to modify the boot order in the + EFI variables of your firmware to include this location. If you are + *not* booted in UEFI mode at the time GRUB is being installed, the + NVRAM will not be modified, and your system will not find GRUB at + boot time. However, GRUB will still return success so you may miss + the warning that gets printed ("<literal>efibootmgr: EFI variables + are not supported on this system.</literal>").</para> + + <para>If you turn this feature on, GRUB will install itself in a + special location within <literal>efiSysMountPoint</literal> (namely + <literal>EFI/boot/boot$arch.efi</literal>) which the firmwares + are hardcoded to try first, regardless of NVRAM EFI variables.</para> + + <para>To summarize, turn this on if: + <itemizedlist> + <listitem><para>You are installing NixOS and want it to boot in UEFI mode, + but you are currently booted in legacy mode</para></listitem> + <listitem><para>You want to make a drive that will boot regardless of + the NVRAM state of the computer (like a USB "removable" drive)</para></listitem> + <listitem><para>You simply dislike the idea of depending on NVRAM + state to make your drive bootable</para></listitem> + </itemizedlist> + ''; + }; + + enableCryptodisk = mkOption { + default = false; + type = types.bool; + description = '' + Enable support for encrypted partitions. GRUB should automatically + unlock the correct encrypted partition and look for filesystems. + ''; + }; + + forceInstall = mkOption { + default = false; + type = types.bool; + description = '' + Whether to try and forcibly install GRUB even if problems are + detected. It is not recommended to enable this unless you know what + you are doing. + ''; + }; + + forcei686 = mkOption { + default = false; + type = types.bool; + description = '' + Whether to force the use of a ia32 boot loader on x64 systems. Required + to install and run NixOS on 64bit x86 systems with 32bit (U)EFI. + ''; + }; + + trustedBoot = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable trusted boot. GRUB will measure all critical components during + the boot process to offer TCG (TPM) support. + ''; + }; + + systemHasTPM = mkOption { + default = ""; + example = "YES_TPM_is_activated"; + type = types.str; + description = '' + Assertion that the target system has an activated TPM. It is a safety + check before allowing the activation of 'trustedBoot.enable'. TrustedBoot + WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available. + ''; + }; + + isHPLaptop = mkOption { + default = false; + type = types.bool; + description = '' + Use a special version of TrustedGRUB that is needed by some HP laptops + and works only for the HP laptops. + ''; + }; + + }; + + }; + + }; + + + ###### implementation + + config = mkMerge [ + + { boot.loader.grub.splashImage = mkDefault ( + if cfg.version == 1 then pkgs.fetchurl { + url = "http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz"; + sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59"; + } + # GRUB 1.97 doesn't support gzipped XPMs. + else defaultSplash); + } + + (mkIf (cfg.splashImage == defaultSplash) { + boot.loader.grub.backgroundColor = mkDefault "#2F302F"; + boot.loader.grub.splashMode = mkDefault "normal"; + }) + + (mkIf cfg.enable { + + boot.loader.grub.devices = optional (cfg.device != "") cfg.device; + + boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [ + { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; } + ]; + + boot.loader.supportsInitrdSecrets = true; + + system.build.installBootLoader = + let + install-grub-pl = pkgs.substituteAll { + src = ./install-grub.pl; + utillinux = pkgs.util-linux; + btrfsprogs = pkgs.btrfs-progs; + }; + perl = pkgs.perl.withPackages (p: with p; [ + FileSlurp FileCopyRecursive + XMLLibXML XMLSAX XMLSAXBase + ListCompare JSON + ]); + in pkgs.writeScript "install-grub.sh" ('' + #!${pkgs.runtimeShell} + set -e + ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"} + '' + flip concatMapStrings cfg.mirroredBoots (args: '' + ${perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@ + '') + cfg.extraInstallCommands); + + system.build.grub = grub; + + # Common attribute for boot loaders so only one of them can be + # set at once. + system.boot.loader.id = "grub"; + + environment.systemPackages = optional (grub != null) grub; + + boot.loader.grub.extraPrepareConfig = + concatStrings (mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/cp -pf "${v}" "@bootPath@/${n}" + '') config.boot.loader.grub.extraFiles); + + assertions = [ + { + assertion = !cfg.zfsSupport || cfg.version == 2; + message = "Only GRUB version 2 provides ZFS support"; + } + { + assertion = cfg.mirroredBoots != [ ]; + message = "You must set the option ‘boot.loader.grub.devices’ or " + + "'boot.loader.grub.mirroredBoots' to make the system bootable."; + } + { + assertion = cfg.efiSupport || all (c: c < 2) (mapAttrsToList (n: c: if n == "nodev" then 0 else c) bootDeviceCounters); + message = "You cannot have duplicated devices in mirroredBoots"; + } + { + assertion = !cfg.trustedBoot.enable || cfg.version == 2; + message = "Trusted GRUB is only available for GRUB 2"; + } + { + assertion = !cfg.efiSupport || !cfg.trustedBoot.enable; + message = "Trusted GRUB does not have EFI support"; + } + { + assertion = !cfg.zfsSupport || !cfg.trustedBoot.enable; + message = "Trusted GRUB does not have ZFS support"; + } + { + assertion = !cfg.trustedBoot.enable || cfg.trustedBoot.systemHasTPM == "YES_TPM_is_activated"; + message = "Trusted GRUB can break the system! Confirm that the system has an activated TPM by setting 'systemHasTPM'."; + } + { + assertion = cfg.efiInstallAsRemovable -> cfg.efiSupport; + message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn on boot.loader.grub.efiSupport"; + } + { + assertion = cfg.efiInstallAsRemovable -> !config.boot.loader.efi.canTouchEfiVariables; + message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn off boot.loader.efi.canTouchEfiVariables"; + } + ] ++ flip concatMap cfg.mirroredBoots (args: [ + { + assertion = args.devices != [ ]; + message = "A boot path cannot have an empty devices string in ${args.path}"; + } + { + assertion = hasPrefix "/" args.path; + message = "Boot paths must be absolute, not ${args.path}"; + } + { + assertion = if args.efiSysMountPoint == null then true else hasPrefix "/" args.efiSysMountPoint; + message = "EFI paths must be absolute, not ${args.efiSysMountPoint}"; + } + ] ++ forEach args.devices (device: { + assertion = device == "nodev" || hasPrefix "/" device; + message = "GRUB devices must be absolute paths, not ${device} in ${args.path}"; + })); + }) + + ]; + + + imports = + [ (mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ] "") + (mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ]) + (mkRenamedOptionModule [ "boot" "extraGrubEntries" ] [ "boot" "loader" "grub" "extraEntries" ]) + (mkRenamedOptionModule [ "boot" "extraGrubEntriesBeforeNixos" ] [ "boot" "loader" "grub" "extraEntriesBeforeNixOS" ]) + (mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ]) + (mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ]) + (mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ]) + (mkRemovedOptionModule [ "boot" "loader" "grub" "extraInitrd" ] '' + This option has been replaced with the bootloader agnostic + boot.initrd.secrets option. To migrate to the initrd secrets system, + extract the extraInitrd archive into your main filesystem: + + # zcat /boot/extra_initramfs.gz | cpio -idvmD /etc/secrets/initrd + /path/to/secret1 + /path/to/secret2 + + then replace boot.loader.grub.extraInitrd with boot.initrd.secrets: + + boot.initrd.secrets = { + "/path/to/secret1" = "/etc/secrets/initrd/path/to/secret1"; + "/path/to/secret2" = "/etc/secrets/initrd/path/to/secret2"; + }; + + See the boot.initrd.secrets option documentation for more information. + '') + ]; + +} diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl new file mode 100644 index 00000000000..0c93b288fc6 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/install-grub.pl @@ -0,0 +1,780 @@ +use strict; +use warnings; +use Class::Struct; +use XML::LibXML; +use File::Basename; +use File::Path; +use File::stat; +use File::Copy; +use File::Copy::Recursive qw(rcopy pathrm); +use File::Slurp; +use File::Temp; +use JSON; +use File::Find; +require List::Compare; +use POSIX; +use Cwd; + +# system.build.toplevel path +my $defaultConfig = $ARGV[1] or die; + +# Grub config XML generated by grubConfig function in grub.nix +my $dom = XML::LibXML->load_xml(location => $ARGV[0]); + +sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); } + +sub getList { + my ($name) = @_; + my @list = (); + foreach my $entry ($dom->findnodes("/expr/attrs/attr[\@name = '$name']/list/string/\@value")) { + $entry = $entry->findvalue(".") or die; + push(@list, $entry); + } + return @list; +} + +sub readFile { + my ($fn) = @_; local $/ = undef; + open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE; + local $/ = "\n"; chomp $s; return $s; +} + +sub writeFile { + my ($fn, $s) = @_; + open FILE, ">$fn" or die "cannot create $fn: $!\n"; + print FILE $s or die; + close FILE or die; +} + +sub runCommand { + my ($cmd) = @_; + open FILE, "$cmd 2>/dev/null |" or die "Failed to execute: $cmd\n"; + my @ret = <FILE>; + close FILE; + return ($?, @ret); +} + +my $grub = get("grub"); +my $grubVersion = int(get("version")); +my $grubTarget = get("grubTarget"); +my $extraConfig = get("extraConfig"); +my $extraPrepareConfig = get("extraPrepareConfig"); +my $extraPerEntryConfig = get("extraPerEntryConfig"); +my $extraEntries = get("extraEntries"); +my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true"; +my $splashImage = get("splashImage"); +my $splashMode = get("splashMode"); +my $backgroundColor = get("backgroundColor"); +my $configurationLimit = int(get("configurationLimit")); +my $copyKernels = get("copyKernels") eq "true"; +my $timeout = int(get("timeout")); +my $defaultEntry = get("default"); +my $fsIdentifier = get("fsIdentifier"); +my $grubEfi = get("grubEfi"); +my $grubTargetEfi = get("grubTargetEfi"); +my $bootPath = get("bootPath"); +my $storePath = get("storePath"); +my $canTouchEfiVariables = get("canTouchEfiVariables"); +my $efiInstallAsRemovable = get("efiInstallAsRemovable"); +my $efiSysMountPoint = get("efiSysMountPoint"); +my $gfxmodeEfi = get("gfxmodeEfi"); +my $gfxmodeBios = get("gfxmodeBios"); +my $gfxpayloadEfi = get("gfxpayloadEfi"); +my $gfxpayloadBios = get("gfxpayloadBios"); +my $bootloaderId = get("bootloaderId"); +my $forceInstall = get("forceInstall"); +my $font = get("font"); +my $theme = get("theme"); +my $saveDefault = $defaultEntry eq "saved"; +$ENV{'PATH'} = get("path"); + +die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2; + +print STDERR "updating GRUB $grubVersion menu...\n"; + +mkpath("$bootPath/grub", 0, 0700); + +# Discover whether the bootPath is on the same filesystem as / and +# /nix/store. If not, then all kernels and initrds must be copied to +# the bootPath. +if (stat($bootPath)->dev != stat("/nix/store")->dev) { + $copyKernels = 1; +} + +# Discover information about the location of the bootPath +struct(Fs => { + device => '$', + type => '$', + mount => '$', +}); +sub PathInMount { + my ($path, $mount) = @_; + my @splitMount = split /\//, $mount; + my @splitPath = split /\//, $path; + if ($#splitPath < $#splitMount) { + return 0; + } + for (my $i = 0; $i <= $#splitMount; $i++) { + if ($splitMount[$i] ne $splitPath[$i]) { + return 0; + } + } + return 1; +} + +# Figure out what filesystem is used for the directory with init/initrd/kernel files +sub GetFs { + my ($dir) = @_; + my $bestFs = Fs->new(device => "", type => "", mount => ""); + foreach my $fs (read_file("/proc/self/mountinfo")) { + chomp $fs; + my @fields = split / /, $fs; + my $mountPoint = $fields[4]; + next unless -d $mountPoint; + my @mountOptions = split /,/, $fields[5]; + + # Skip the optional fields. + my $n = 6; $n++ while $fields[$n] ne "-"; $n++; + my $fsType = $fields[$n]; + my $device = $fields[$n + 1]; + my @superOptions = split /,/, $fields[$n + 2]; + + # Skip the bind-mount on /nix/store. + next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions); + # Skip mount point generated by systemd-efi-boot-generator? + next if $fsType eq "autofs"; + + # Ensure this matches the intended directory + next unless PathInMount($dir, $mountPoint); + + # Is it better than our current match? + if (length($mountPoint) > length($bestFs->mount)) { + $bestFs = Fs->new(device => $device, type => $fsType, mount => $mountPoint); + } + } + return $bestFs; +} +struct (Grub => { + path => '$', + search => '$', +}); +my $driveid = 1; +sub GrubFs { + my ($dir) = @_; + my $fs = GetFs($dir); + my $path = substr($dir, length($fs->mount)); + if (substr($path, 0, 1) ne "/") { + $path = "/$path"; + } + my $search = ""; + + if ($grubVersion > 1) { + # ZFS is completely separate logic as zpools are always identified by a label + # or custom UUID + if ($fs->type eq 'zfs') { + my $sid = index($fs->device, '/'); + + if ($sid < 0) { + $search = '--label ' . $fs->device; + $path = '/@' . $path; + } else { + $search = '--label ' . substr($fs->device, 0, $sid); + $path = '/' . substr($fs->device, $sid) . '/@' . $path; + } + } else { + my %types = ('uuid' => '--fs-uuid', 'label' => '--label'); + + if ($fsIdentifier eq 'provided') { + # If the provided dev is identifying the partition using a label or uuid, + # we should get the label / uuid and do a proper search + my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/; + if ($#matches > 1) { + die "Too many matched devices" + } elsif ($#matches == 1) { + $search = "$types{$matches[0]} $matches[1]" + } + } else { + # Determine the identifying type + $search = $types{$fsIdentifier} . ' '; + + # Based on the type pull in the identifier from the system + my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid -o export @{[$fs->device]}"); + if ($status != 0) { + die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}"; + } + my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/; + if ($#matches != 0) { + die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n" + } + $search .= $matches[0]; + } + + # BTRFS is a special case in that we need to fix the referrenced path based on subvolumes + if ($fs->type eq 'btrfs') { + my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs subvol show @{[$fs->mount]}"); + if ($status != 0) { + die "Failed to retrieve subvolume info for @{[$fs->mount]}\n"; + } + my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s; + if ($#ids > 0) { + die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n" + } elsif ($#ids == 0) { + my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs subvol list @{[$fs->mount]}"); + if ($status != 0) { + die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n"; + } + my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/; + if ($#paths > 0) { + die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n"; + } elsif ($#paths != 0) { + die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n"; + } + $path = "/$paths[0]$path"; + } + } + } + if (not $search eq "") { + $search = "search --set=drive$driveid " . $search; + $path = "(\$drive$driveid)$path"; + $driveid += 1; + } + } + return Grub->new(path => $path, search => $search); +} +my $grubBoot = GrubFs($bootPath); +my $grubStore; +if ($copyKernels == 0) { + $grubStore = GrubFs($storePath); +} + +# Generate the header. +my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n"; + +if ($grubVersion == 1) { + # $defaultEntry might be "saved", indicating that we want to use the last selected configuration as default. + # Incidentally this is already the correct value for the grub 1 config to achieve this behaviour. + $conf .= " + default $defaultEntry + timeout $timeout + "; + if ($splashImage) { + copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n"; + $conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n"; + } +} + +else { + my @users = (); + foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) { + my $name = $user->findvalue('@name') or die; + my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value'); + my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value'); + my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value'); + my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value'); + + if ($hashedPasswordFile) { + open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!"; + $hashedPassword = <$f>; + chomp $hashedPassword; + } + if ($passwordFile) { + open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!"; + $password = <$f>; + chomp $password; + } + + if ($hashedPassword) { + if (index($hashedPassword, "grub.pbkdf2.") == 0) { + $conf .= "\npassword_pbkdf2 $name $hashedPassword"; + } + else { + die "Password hash for GRUB user '$name' is not valid!"; + } + } + elsif ($password) { + $conf .= "\npassword $name $password"; + } + else { + die "GRUB user '$name' has no password!"; + } + push(@users, $name); + } + if (@users) { + $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n"; + } + + if ($copyKernels == 0) { + $conf .= " + " . $grubStore->search; + } + # FIXME: should use grub-mkconfig. + my $defaultEntryText = $defaultEntry; + if ($saveDefault) { + $defaultEntryText = "\"\${saved_entry}\""; + } + $conf .= " + " . $grubBoot->search . " + if [ -s \$prefix/grubenv ]; then + load_env + fi + + # ‘grub-reboot’ sets a one-time saved entry, which we process here and + # then delete. + if [ \"\${next_entry}\" ]; then + set default=\"\${next_entry}\" + set next_entry= + save_env next_entry + set timeout=1 + set boot_once=true + else + set default=$defaultEntryText + set timeout=$timeout + fi + + function savedefault { + if [ -z \"\${boot_once}\"]; then + saved_entry=\"\${chosen}\" + save_env saved_entry + fi + } + + # Setup the graphics stack for bios and efi systems + if [ \"\${grub_platform}\" = \"efi\" ]; then + insmod efi_gop + insmod efi_uga + else + insmod vbe + fi + "; + + if ($font) { + copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n"; + $conf .= " + insmod font + if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then + insmod gfxterm + if [ \"\${grub_platform}\" = \"efi\" ]; then + set gfxmode=$gfxmodeEfi + set gfxpayload=$gfxpayloadEfi + else + set gfxmode=$gfxmodeBios + set gfxpayload=$gfxpayloadBios + fi + terminal_output gfxterm + fi + "; + } + if ($splashImage) { + # Keeps the image's extension. + my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$"); + # The module for jpg is jpeg. + if ($suffix eq ".jpg") { + $suffix = ".jpeg"; + } + if ($backgroundColor) { + $conf .= " + background_color '$backgroundColor' + "; + } + copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n"; + $conf .= " + insmod " . substr($suffix, 1) . " + if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then + set color_normal=white/black + set color_highlight=black/white + else + set menu_color_normal=cyan/blue + set menu_color_highlight=white/blue + fi + "; + } + + rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme"; + + if ($theme) { + # Copy theme + rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n"; + $conf .= " + # Sets theme. + set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt + export theme + # Load theme fonts, if any + "; + + find( { wanted => sub { + if ($_ =~ /\.pf2$/i) { + $font = File::Spec->abs2rel($File::Find::name, $theme); + $conf .= " + loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font + "; + } + }, no_chdir => 1 }, $theme ); + } +} + +$conf .= "$extraConfig\n"; + + +# Generate the menu entries. +$conf .= "\n"; + +my %copied; +mkpath("$bootPath/kernels", 0, 0755) if $copyKernels; + +sub copyToKernelsDir { + my ($path) = @_; + return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels; + $path =~ /\/nix\/store\/(.*)/ or die; + my $name = $1; $name =~ s/\//-/g; + my $dst = "$bootPath/kernels/$name"; + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if (! -e $dst) { + my $tmp = "$dst.tmp"; + copy $path, $tmp or die "cannot copy $path to $tmp: $!\n"; + rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n"; + } + $copied{$dst} = 1; + return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name"; +} + +sub addEntry { + my ($name, $path, $options) = @_; + return unless -e "$path/kernel" && -e "$path/initrd"; + + my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel")); + my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd")); + + # Include second initrd with secrets + if (-e -x "$path/append-initrd-secrets") { + my $initrdName = basename($initrd); + my $initrdSecretsPath = "$bootPath/kernels/$initrdName-secrets"; + + mkpath(dirname($initrdSecretsPath), 0, 0755); + my $oldUmask = umask; + # Make sure initrd is not world readable (won't work if /boot is FAT) + umask 0137; + my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX"); + system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n"; + # Check whether any secrets were actually added + if (-e $initrdSecretsPathTemp && ! -z _) { + rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n"; + $copied{$initrdSecretsPath} = 1; + $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets"; + } else { + unlink $initrdSecretsPathTemp; + rmdir dirname($initrdSecretsPathTemp); + } + umask $oldUmask; + } + + my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef; + + # FIXME: $confName + + my $kernelParams = + "init=" . Cwd::abs_path("$path/init") . " " . + readFile("$path/kernel-params"); + my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : ""; + + if ($grubVersion == 1) { + $conf .= "title $name\n"; + $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; + $conf .= " kernel $xen $xenParams\n" if $xen; + $conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n"; + $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n"; + if ($saveDefault) { + $conf .= " savedefault\n"; + } + $conf .= "\n"; + } else { + $conf .= "menuentry \"$name\" " . ($options||"") . " {\n"; + if ($saveDefault) { + $conf .= " savedefault\n"; + } + $conf .= $grubBoot->search . "\n"; + if ($copyKernels == 0) { + $conf .= $grubStore->search . "\n"; + } + $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; + $conf .= " multiboot $xen $xenParams\n" if $xen; + $conf .= " " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n"; + $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n"; + $conf .= "}\n\n"; + } +} + + +# Add default entries. +$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS; + +addEntry("NixOS - Default", $defaultConfig, "--unrestricted"); + +$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS; + +# Find all the children of the current default configuration +# Do not search for grand children +my @links = sort (glob "$defaultConfig/specialisation/*"); +foreach my $link (@links) { + + my $entryName = ""; + + my $cfgName = readFile("$link/configuration-name"); + + my $date = strftime("%F", localtime(lstat($link)->mtime)); + my $version = + -e "$link/nixos-version" + ? readFile("$link/nixos-version") + : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); + + if ($cfgName) { + $entryName = $cfgName; + } else { + my $linkname = basename($link); + $entryName = "($linkname - $date - $version)"; + } + addEntry("NixOS - $entryName", $link); +} + +my $grubBootPath = $grubBoot->path; +# extraEntries could refer to @bootRoot@, which we have to substitute +$conf =~ s/\@bootRoot\@/$grubBootPath/g; + +# Emit submenus for all system profiles. +sub addProfile { + my ($profile, $description) = @_; + + # Add entries for all generations of this profile. + $conf .= "submenu \"$description\" {\n" if $grubVersion == 2; + + sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; } + + my @links = sort + { nrFromGen($b) <=> nrFromGen($a) } + (glob "$profile-*-link"); + + my $curEntry = 0; + foreach my $link (@links) { + last if $curEntry++ >= $configurationLimit; + if (! -e "$link/nixos-version") { + warn "skipping corrupt system profile entry ‘$link’\n"; + next; + } + my $date = strftime("%F", localtime(lstat($link)->mtime)); + my $version = + -e "$link/nixos-version" + ? readFile("$link/nixos-version") + : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); + addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link); + } + + $conf .= "}\n" if $grubVersion == 2; +} + +addProfile "/nix/var/nix/profiles/system", "NixOS - All configurations"; + +if ($grubVersion == 2) { + for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") { + my $name = basename($profile); + next unless $name =~ /^\w+$/; + addProfile $profile, "NixOS - Profile '$name'"; + } +} + +# extraPrepareConfig could refer to @bootPath@, which we have to substitute +$extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g; + +# Run extraPrepareConfig in sh +if ($extraPrepareConfig ne "") { + system((get("shell"), "-c", $extraPrepareConfig)); +} + +# write the GRUB config. +my $confFile = $grubVersion == 1 ? "$bootPath/grub/menu.lst" : "$bootPath/grub/grub.cfg"; +my $tmpFile = $confFile . ".tmp"; +writeFile($tmpFile, $conf); + + +# check whether to install GRUB EFI or not +sub getEfiTarget { + if ($grubVersion == 1) { + return "no" + } elsif (($grub ne "") && ($grubEfi ne "")) { + # EFI can only be installed when target is set; + # A target is also required then for non-EFI grub + if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die } + else { return "both" } + } elsif (($grub ne "") && ($grubEfi eq "")) { + # TODO: It would be safer to disallow non-EFI grub installation if no taget is given. + # If no target is given, then grub auto-detects the target which can lead to errors. + # E.g. it seems as if grub would auto-detect a EFI target based on the availability + # of a EFI partition. + # However, it seems as auto-detection is currently relied on for non-x86_64 and non-i386 + # architectures in NixOS. That would have to be fixed in the nixos modules first. + return "no" + } elsif (($grub eq "") && ($grubEfi ne "")) { + # EFI can only be installed when target is set; + if ($grubTargetEfi eq "") { die } + else {return "only" } + } else { + # prevent an installation if neither grub nor grubEfi is given + return "neither" + } +} + +my $efiTarget = getEfiTarget(); + +# Append entries detected by os-prober +if (get("useOSProber") eq "true") { + if ($saveDefault) { + # os-prober will read this to determine if "savedefault" should be added to generated entries + $ENV{'GRUB_SAVEDEFAULT'} = "true"; + } + + my $targetpackage = ($efiTarget eq "no") ? $grub : $grubEfi; + system(get("shell"), "-c", "pkgdatadir=$targetpackage/share/grub $targetpackage/etc/grub.d/30_os-prober >> $tmpFile"); +} + +# Atomically switch to the new config +rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n"; + + +# Remove obsolete files from $bootPath/kernels. +foreach my $fn (glob "$bootPath/kernels/*") { + next if defined $copied{$fn}; + print STDERR "removing obsolete file $fn\n"; + unlink $fn; +} + + +# +# Install GRUB if the parameters changed from the last time we installed it. +# + +struct(GrubState => { + name => '$', + version => '$', + efi => '$', + devices => '$', + efiMountPoint => '$', + extraGrubInstallArgs => '@', +}); +# If you add something to the state file, only add it to the end +# because it is read line-by-line. +sub readGrubState { + my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () ); + open FILE, "<$bootPath/grub/state" or return $defaultGrubState; + local $/ = "\n"; + my $name = <FILE>; + chomp($name); + my $version = <FILE>; + chomp($version); + my $efi = <FILE>; + chomp($efi); + my $devices = <FILE>; + chomp($devices); + my $efiMountPoint = <FILE>; + chomp($efiMountPoint); + # Historically, arguments in the state file were one per each line, but that + # gets really messy when newlines are involved, structured arguments + # like lists are needed (they have to have a separator encoding), or even worse, + # when we need to remove a setting in the future. Thus, the 6th line is a JSON + # object that can store structured data, with named keys, and all new state + # should go in there. + my $jsonStateLine = <FILE>; + # For historical reasons we do not check the values above for un-definedness + # (that is, when the state file has too few lines and EOF is reached), + # because the above come from the first version of this logic and are thus + # guaranteed to be present. + $jsonStateLine = defined $jsonStateLine ? $jsonStateLine : '{}'; # empty JSON object + chomp($jsonStateLine); + if ($jsonStateLine eq "") { + $jsonStateLine = '{}'; # empty JSON object + } + my %jsonState = %{decode_json($jsonStateLine)}; + my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : (); + close FILE; + my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs ); + return $grubState +} + +my @deviceTargets = getList('devices'); +my $prevGrubState = readGrubState(); +my @prevDeviceTargets = split/,/, $prevGrubState->devices; +my @extraGrubInstallArgs = getList('extraGrubInstallArgs'); +my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs}; + +my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference()); +my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference()); +my $nameDiffer = get("fullName") ne $prevGrubState->name; +my $versionDiffer = get("fullVersion") ne $prevGrubState->version; +my $efiDiffer = $efiTarget ne $prevGrubState->efi; +my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint; +if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") { + warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER"; + $ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1"; +} +my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1"); + +# install a symlink so that grub can detect the boot drive +my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!"; +symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!"; + +# install non-EFI GRUB +if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { + foreach my $dev (@deviceTargets) { + next if $dev eq "nodev"; + print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n"; + my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs); + if ($forceInstall eq "true") { + push @command, "--force"; + } + if ($grubTarget ne "") { + push @command, "--target=$grubTarget"; + } + (system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n"; + } +} + + +# install EFI GRUB +if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) { + print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n"; + my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs); + if ($forceInstall eq "true") { + push @command, "--force"; + } + if ($canTouchEfiVariables eq "true") { + push @command, "--bootloader-id=$bootloaderId"; + } else { + push @command, "--no-nvram"; + push @command, "--removable" if $efiInstallAsRemovable eq "true"; + } + + (system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n"; +} + + +# update GRUB state file +if ($requireNewInstall != 0) { + # Temp file for atomic rename. + my $stateFile = "$bootPath/grub/state"; + my $stateFileTmp = $stateFile . ".tmp"; + + open FILE, ">$stateFileTmp" or die "cannot create $stateFileTmp: $!\n"; + print FILE get("fullName"), "\n" or die; + print FILE get("fullVersion"), "\n" or die; + print FILE $efiTarget, "\n" or die; + print FILE join( ",", @deviceTargets ), "\n" or die; + print FILE $efiSysMountPoint, "\n" or die; + my %jsonState = ( + extraGrubInstallArgs => \@extraGrubInstallArgs + ); + my $jsonStateLine = encode_json(\%jsonState); + print FILE $jsonStateLine, "\n" or die; + close FILE or die; + + # Atomically switch to the new state file + rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n"; +} diff --git a/nixos/modules/system/boot/loader/grub/ipxe.nix b/nixos/modules/system/boot/loader/grub/ipxe.nix new file mode 100644 index 00000000000..ef8595592f4 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/ipxe.nix @@ -0,0 +1,64 @@ +# This module adds a scripted iPXE entry to the GRUB boot menu. + +{ config, lib, pkgs, ... }: + +with lib; + +let + scripts = builtins.attrNames config.boot.loader.grub.ipxe; + + grubEntry = name: + '' + menuentry "iPXE - ${name}" { + linux16 @bootRoot@/ipxe.lkrn + initrd16 @bootRoot@/${name}.ipxe + } + + ''; + + scriptFile = name: + let + value = builtins.getAttr name config.boot.loader.grub.ipxe; + in + if builtins.typeOf value == "path" then value + else builtins.toFile "${name}.ipxe" value; +in +{ + options = + { boot.loader.grub.ipxe = mkOption { + type = types.attrsOf (types.either types.path types.str); + description = + '' + Set of iPXE scripts available for + booting from the GRUB boot menu. + ''; + default = { }; + example = literalExpression '' + { demo = ''' + #!ipxe + dhcp + chain http://boot.ipxe.org/demo/boot.php + '''; + } + ''; + }; + }; + + config = mkIf (builtins.length scripts != 0) { + + boot.loader.grub.extraEntries = + if config.boot.loader.grub.version == 2 then + toString (map grubEntry scripts) + else + throw "iPXE is not supported with GRUB 1."; + + boot.loader.grub.extraFiles = + { "ipxe.lkrn" = "${pkgs.ipxe}/ipxe.lkrn"; } + // + builtins.listToAttrs ( map + (name: { name = name+".ipxe"; value = scriptFile name; }) + scripts + ); + }; + +} diff --git a/nixos/modules/system/boot/loader/grub/memtest.nix b/nixos/modules/system/boot/loader/grub/memtest.nix new file mode 100644 index 00000000000..71e50dd0577 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/memtest.nix @@ -0,0 +1,116 @@ +# This module adds Memtest86+/Memtest86 to the GRUB boot menu. + +{ config, lib, pkgs, ... }: + +with lib; + +let + memtest86 = pkgs.memtest86plus; + efiSupport = config.boot.loader.grub.efiSupport; + cfg = config.boot.loader.grub.memtest86; +in + +{ + options = { + + boot.loader.grub.memtest86 = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Make Memtest86+ (or MemTest86 if EFI support is enabled), + a memory testing program, available from the + GRUB boot menu. MemTest86 is an unfree program, so + this requires <literal>allowUnfree</literal> to be set to + <literal>true</literal>. + ''; + }; + + params = mkOption { + default = []; + example = [ "console=ttyS0,115200" ]; + type = types.listOf types.str; + description = '' + Parameters added to the Memtest86+ command line. As of memtest86+ 5.01 + the following list of (apparently undocumented) parameters are + accepted: + + <itemizedlist> + + <listitem> + <para><literal>console=...</literal>, set up a serial console. + Examples: + <literal>console=ttyS0</literal>, + <literal>console=ttyS0,9600</literal> or + <literal>console=ttyS0,115200n8</literal>.</para> + </listitem> + + <listitem> + <para><literal>btrace</literal>, enable boot trace.</para> + </listitem> + + <listitem> + <para><literal>maxcpus=N</literal>, limit number of CPUs.</para> + </listitem> + + <listitem> + <para><literal>onepass</literal>, run one pass and exit if there + are no errors.</para> + </listitem> + + <listitem> + <para><literal>tstlist=...</literal>, list of tests to run. + Example: <literal>0,1,2</literal>.</para> + </listitem> + + <listitem> + <para><literal>cpumask=...</literal>, set a CPU mask, to select CPUs + to use for testing.</para> + </listitem> + + </itemizedlist> + + This list of command line options was obtained by reading the + Memtest86+ source code. + ''; + }; + + }; + }; + + config = mkMerge [ + (mkIf (cfg.enable && efiSupport) { + assertions = [ + { + assertion = cfg.params == []; + message = "Parameters are not available for MemTest86"; + } + ]; + + boot.loader.grub.extraFiles = { + "memtest86.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi"; + }; + + boot.loader.grub.extraEntries = '' + menuentry "Memtest86" { + chainloader /memtest86.efi + } + ''; + }) + + (mkIf (cfg.enable && !efiSupport) { + boot.loader.grub.extraEntries = + if config.boot.loader.grub.version == 2 then + '' + menuentry "Memtest86+" { + linux16 @bootRoot@/memtest.bin ${toString cfg.params} + } + '' + else + throw "Memtest86+ is not supported with GRUB 1."; + + boot.loader.grub.extraFiles."memtest.bin" = "${memtest86}/memtest.bin"; + }) + ]; +} diff --git a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh new file mode 100644 index 00000000000..bd3fc64999d --- /dev/null +++ b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh @@ -0,0 +1,92 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +if test $# -ne 1; then + echo "Usage: init-script-builder.sh DEFAULT-CONFIG" + exit 1 +fi + +defaultConfig="$1" + + +[ "$(stat -f -c '%i' /)" = "$(stat -f -c '%i' /boot)" ] || { + # see grub-menu-builder.sh + echo "WARNING: /boot being on a different filesystem not supported by init-script-builder.sh" +} + + + +target="/sbin/init" +targetOther="/boot/init-other-configurations-contents.txt" + +tmp="$target.tmp" +tmpOther="$targetOther.tmp" + + +configurationCounter=0 +numAlienEntries=`cat <<EOF | egrep '^[[:space:]]*title' | wc -l +@extraEntries@ +EOF` + + + + +# Add an entry to $targetOther +addEntry() { + local name="$1" + local path="$2" + local shortSuffix="$3" + + configurationCounter=$((configurationCounter + 1)) + + local stage2=$path/init + + content="$( + echo "#!/bin/sh" + echo "# $name" + echo "# created by init-script-builder.sh" + echo "exec $stage2" + )" + + [ "$path" != "$defaultConfig" ] || { + echo "$content" > $tmp + echo "# older configurations: $targetOther" >> $tmp + chmod +x $tmp + } + + echo -e "$content\n\n" >> $tmpOther +} + + +mkdir -p /boot /sbin + +addEntry "NixOS - Default" $defaultConfig "" + +# Add all generations of the system profile to the menu, in reverse +# (most recent to least recent) order. +for link in $((ls -d $defaultConfig/specialisation/* ) | sort -n); do + date=$(stat --printf="%y\n" $link | sed 's/\..*//') + addEntry "NixOS - variation" $link "" +done + +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + date=$(stat --printf="%y\n" $link | sed 's/\..*//') + if [ -d $link/kernel ]; then + kernelVersion=$(cd $(dirname $(readlink -f $link/kernel))/lib/modules && echo *) + suffix="($date - $kernelVersion)" + else + suffix="($date)" + fi + addEntry "NixOS - Configuration $generation $suffix" $link "$generation ($date)" +done + +mv $tmpOther $targetOther +mv $tmp $target diff --git a/nixos/modules/system/boot/loader/init-script/init-script.nix b/nixos/modules/system/boot/loader/init-script/init-script.nix new file mode 100644 index 00000000000..374d9524ff1 --- /dev/null +++ b/nixos/modules/system/boot/loader/init-script/init-script.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + initScriptBuilder = pkgs.substituteAll { + src = ./init-script-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + }; + +in + +{ + + ###### interface + + options = { + + boot.loader.initScript = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Some systems require a /sbin/init script which is started. + Or having it makes starting NixOS easier. + This applies to some kind of hosting services and user mode linux. + + Additionally this script will create + /boot/init-other-configurations-contents.txt containing + contents of remaining configurations. You can copy paste them into + /sbin/init manually running a rescue system or such. + ''; + }; + }; + + }; + + + ###### implementation + + config = mkIf config.boot.loader.initScript.enable { + + system.build.installBootLoader = initScriptBuilder; + + }; + +} diff --git a/nixos/modules/system/boot/loader/loader.nix b/nixos/modules/system/boot/loader/loader.nix new file mode 100644 index 00000000000..01475f79b9c --- /dev/null +++ b/nixos/modules/system/boot/loader/loader.nix @@ -0,0 +1,20 @@ +{ lib, ... }: + +with lib; + +{ + imports = [ + (mkRenamedOptionModule [ "boot" "loader" "grub" "timeout" ] [ "boot" "loader" "timeout" ]) + (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "timeout" ] [ "boot" "loader" "timeout" ]) + ]; + + options = { + boot.loader.timeout = mkOption { + default = 5; + type = types.nullOr types.int; + description = '' + Timeout (in seconds) until loader boots the default menu item. Use null if the loader menu should be displayed indefinitely. + ''; + }; + }; +} diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix new file mode 100644 index 00000000000..64e106036ab --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix @@ -0,0 +1,9 @@ +{ pkgs, configTxt, firmware ? pkgs.raspberrypifw }: + +pkgs.substituteAll { + src = ./raspberrypi-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + inherit firmware configTxt; +} diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh new file mode 100644 index 00000000000..0541ca1ba62 --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh @@ -0,0 +1,143 @@ +#! @bash@/bin/sh + +# This can end up being called disregarding the shebang. +set -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +usage() { + echo "usage: $0 -c <path-to-default-configuration> [-d <boot-dir>]" >&2 + exit 1 +} + +default= # Default configuration +target=/boot # Target directory + +while getopts "c:d:" opt; do + case "$opt" in + c) default="$OPTARG" ;; + d) target="$OPTARG" ;; + \?) usage ;; + esac +done + +echo "updating the boot generations directory..." + +mkdir -p $target/old + +# Convert a path to a file in the Nix store such as +# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>. +cleanName() { + local path="$1" + echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g' +} + +# Copy a file from the Nix store to $target/kernels. +declare -A filesCopied + +copyToKernelsDir() { + local src="$1" + local dst="$target/old/$(cleanName $src)" + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if ! test -e $dst; then + local dstTmp=$dst.tmp.$$ + cp $src $dstTmp + mv $dstTmp $dst + fi + filesCopied[$dst]=1 + result=$dst +} + +copyForced() { + local src="$1" + local dst="$2" + cp $src $dst.tmp + mv $dst.tmp $dst +} + +outdir=$target/old +mkdir -p $outdir || true + +# Copy its kernel and initrd to $target/old. +addEntry() { + local path="$1" + local generation="$2" + + if ! test -e $path/kernel -a -e $path/initrd; then + return + fi + + local kernel=$(readlink -f $path/kernel) + local initrd=$(readlink -f $path/initrd) + local dtb_path=$(readlink -f $path/dtbs) + + if test -n "@copyKernels@"; then + copyToKernelsDir $kernel; kernel=$result + copyToKernelsDir $initrd; initrd=$result + fi + + echo $(readlink -f $path) > $outdir/$generation-system + echo $(readlink -f $path/init) > $outdir/$generation-init + cp $path/kernel-params $outdir/$generation-cmdline.txt + echo $initrd > $outdir/$generation-initrd + echo $kernel > $outdir/$generation-kernel + + if test "$generation" = "default"; then + copyForced $kernel $target/kernel.img + copyForced $initrd $target/initrd + for dtb in $dtb_path/{broadcom,}/bcm*.dtb; do + dst="$target/$(basename $dtb)" + copyForced $dtb "$dst" + filesCopied[$dst]=1 + done + cp "$(readlink -f "$path/init")" $target/nixos-init + echo "`cat $path/kernel-params` init=$path/init" >$target/cmdline.txt + fi +} + +addEntry $default default + +# Add all generations of the system profile to the menu, in reverse +# (most recent to least recent) order. +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + addEntry $link $generation +done + +# Add the firmware files +fwdir=@firmware@/share/raspberrypi/boot/ +copyForced $fwdir/bootcode.bin $target/bootcode.bin +copyForced $fwdir/fixup.dat $target/fixup.dat +copyForced $fwdir/fixup4.dat $target/fixup4.dat +copyForced $fwdir/fixup4cd.dat $target/fixup4cd.dat +copyForced $fwdir/fixup4db.dat $target/fixup4db.dat +copyForced $fwdir/fixup4x.dat $target/fixup4x.dat +copyForced $fwdir/fixup_cd.dat $target/fixup_cd.dat +copyForced $fwdir/fixup_db.dat $target/fixup_db.dat +copyForced $fwdir/fixup_x.dat $target/fixup_x.dat +copyForced $fwdir/start.elf $target/start.elf +copyForced $fwdir/start4.elf $target/start4.elf +copyForced $fwdir/start4cd.elf $target/start4cd.elf +copyForced $fwdir/start4db.elf $target/start4db.elf +copyForced $fwdir/start4x.elf $target/start4x.elf +copyForced $fwdir/start_cd.elf $target/start_cd.elf +copyForced $fwdir/start_db.elf $target/start_db.elf +copyForced $fwdir/start_x.elf $target/start_x.elf + +# Add the config.txt +copyForced @configTxt@ $target/config.txt + +# Remove obsolete files from $target and $target/old. +for fn in $target/old/*linux* $target/old/*initrd-initrd* $target/bcm*.dtb; do + if ! test "${filesCopied[$fn]}" = 1; then + rm -vf -- "$fn" + fi +done diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix new file mode 100644 index 00000000000..1023361f0b1 --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix @@ -0,0 +1,105 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.boot.loader.raspberryPi; + + builderUboot = import ./uboot-builder.nix { inherit pkgs configTxt; inherit (cfg) version; }; + builderGeneric = import ./raspberrypi-builder.nix { inherit pkgs configTxt; }; + + builder = + if cfg.uboot.enable then + "${builderUboot} -g ${toString cfg.uboot.configurationLimit} -t ${timeoutStr} -c" + else + "${builderGeneric} -c"; + + blCfg = config.boot.loader; + timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout; + + isAarch64 = pkgs.stdenv.hostPlatform.isAarch64; + optional = pkgs.lib.optionalString; + + configTxt = + pkgs.writeText "config.txt" ('' + # U-Boot used to need this to work, regardless of whether UART is actually used or not. + # TODO: check when/if this can be removed. + enable_uart=1 + + # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel + # when attempting to show low-voltage or overtemperature warnings. + avoid_warnings=1 + '' + optional isAarch64 '' + # Boot in 64-bit mode. + arm_64bit=1 + '' + (if cfg.uboot.enable then '' + kernel=u-boot-rpi.bin + '' else '' + kernel=kernel.img + initramfs initrd followkernel + '') + optional (cfg.firmwareConfig != null) cfg.firmwareConfig); + +in + +{ + options = { + + boot.loader.raspberryPi = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to create files with the system generations in + <literal>/boot</literal>. + <literal>/boot/old</literal> will hold files from old generations. + ''; + }; + + version = mkOption { + default = 2; + type = types.enum [ 0 1 2 3 4 ]; + description = ""; + }; + + uboot = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable using uboot as bootmanager for the raspberry pi. + ''; + }; + + configurationLimit = mkOption { + default = 20; + example = 10; + type = types.int; + description = '' + Maximum number of configurations in the boot menu. + ''; + }; + + }; + + firmwareConfig = mkOption { + default = null; + type = types.nullOr types.lines; + description = '' + Extra options that will be appended to <literal>/boot/config.txt</literal> file. + For possible values, see: https://www.raspberrypi.org/documentation/configuration/config-txt/ + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = singleton { + assertion = !pkgs.stdenv.hostPlatform.isAarch64 || cfg.version >= 3; + message = "Only Raspberry Pi >= 3 supports aarch64."; + }; + + system.build.installBootLoader = builder; + system.boot.loader.id = "raspberrypi"; + system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target; + }; +} diff --git a/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix new file mode 100644 index 00000000000..a4352ab9a24 --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix @@ -0,0 +1,37 @@ +{ pkgs, version, configTxt }: + +let + isAarch64 = pkgs.stdenv.hostPlatform.isAarch64; + + uboot = + if version == 0 then + pkgs.ubootRaspberryPiZero + else if version == 1 then + pkgs.ubootRaspberryPi + else if version == 2 then + pkgs.ubootRaspberryPi2 + else if version == 3 then + if isAarch64 then + pkgs.ubootRaspberryPi3_64bit + else + pkgs.ubootRaspberryPi3_32bit + else + throw "U-Boot is not yet supported on the raspberry pi 4."; + + extlinuxConfBuilder = + import ../generic-extlinux-compatible/extlinux-conf-builder.nix { + pkgs = pkgs.buildPackages; + }; +in +pkgs.substituteAll { + src = ./uboot-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + firmware = pkgs.raspberrypifw; + inherit uboot; + inherit configTxt; + inherit extlinuxConfBuilder; + inherit version; +} + diff --git a/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh new file mode 100644 index 00000000000..ea591427179 --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh @@ -0,0 +1,38 @@ +#! @bash@/bin/sh -e + +target=/boot # Target directory + +while getopts "t:c:d:g:" opt; do + case "$opt" in + d) target="$OPTARG" ;; + *) ;; + esac +done + +copyForced() { + local src="$1" + local dst="$2" + cp $src $dst.tmp + mv $dst.tmp $dst +} + +# Call the extlinux builder +"@extlinuxConfBuilder@" "$@" + +# Add the firmware files +fwdir=@firmware@/share/raspberrypi/boot/ +copyForced $fwdir/bootcode.bin $target/bootcode.bin +copyForced $fwdir/fixup.dat $target/fixup.dat +copyForced $fwdir/fixup_cd.dat $target/fixup_cd.dat +copyForced $fwdir/fixup_db.dat $target/fixup_db.dat +copyForced $fwdir/fixup_x.dat $target/fixup_x.dat +copyForced $fwdir/start.elf $target/start.elf +copyForced $fwdir/start_cd.elf $target/start_cd.elf +copyForced $fwdir/start_db.elf $target/start_db.elf +copyForced $fwdir/start_x.elf $target/start_x.elf + +# Add the uboot file +copyForced @uboot@/u-boot.bin $target/u-boot-rpi.bin + +# Add the config.txt +copyForced @configTxt@ $target/config.txt diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py new file mode 100644 index 00000000000..fa879437fd8 --- /dev/null +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -0,0 +1,316 @@ +#! @python3@/bin/python3 -B +import argparse +import shutil +import os +import sys +import errno +import subprocess +import glob +import tempfile +import errno +import warnings +import ctypes +libc = ctypes.CDLL("libc.so.6") +import re +import datetime +import glob +import os.path +from typing import NamedTuple, List, Optional + +class SystemIdentifier(NamedTuple): + profile: Optional[str] + generation: int + specialisation: Optional[str] + + +def copy_if_not_exists(source: str, dest: str) -> None: + if not os.path.exists(dest): + shutil.copyfile(source, dest) + + +def generation_dir(profile: Optional[str], generation: int) -> str: + if profile: + return "/nix/var/nix/profiles/system-profiles/%s-%d-link" % (profile, generation) + else: + return "/nix/var/nix/profiles/system-%d-link" % (generation) + +def system_dir(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str: + d = generation_dir(profile, generation) + if specialisation: + return os.path.join(d, "specialisation", specialisation) + else: + return d + +BOOT_ENTRY = """title NixOS{profile}{specialisation} +version Generation {generation} {description} +linux {kernel} +initrd {initrd} +options {kernel_params} +""" + +def generation_conf_filename(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str: + pieces = [ + "nixos", + profile or None, + "generation", + str(generation), + f"specialisation-{specialisation}" if specialisation else None, + ] + return "-".join(p for p in pieces if p) + ".conf" + + +def write_loader_conf(profile: Optional[str], generation: int, specialisation: Optional[str]) -> None: + with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f: + if "@timeout@" != "": + f.write("timeout @timeout@\n") + f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation)) + if not @editor@: + f.write("editor 0\n"); + f.write("console-mode @consoleMode@\n"); + os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf") + + +def profile_path(profile: Optional[str], generation: int, specialisation: Optional[str], name: str) -> str: + return os.path.realpath("%s/%s" % (system_dir(profile, generation, specialisation), name)) + + +def copy_from_profile(profile: Optional[str], generation: int, specialisation: Optional[str], name: str, dry_run: bool = False) -> str: + store_file_path = profile_path(profile, generation, specialisation, name) + suffix = os.path.basename(store_file_path) + store_dir = os.path.basename(os.path.dirname(store_file_path)) + efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix) + if not dry_run: + copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path)) + return efi_file_path + + +def describe_generation(generation_dir: str) -> str: + try: + with open("%s/nixos-version" % generation_dir) as f: + nixos_version = f.read() + except IOError: + nixos_version = "Unknown" + + kernel_dir = os.path.dirname(os.path.realpath("%s/kernel" % generation_dir)) + module_dir = glob.glob("%s/lib/modules/*" % kernel_dir)[0] + kernel_version = os.path.basename(module_dir) + + build_time = int(os.path.getctime(generation_dir)) + build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F') + + description = "NixOS {}, Linux Kernel {}, Built on {}".format( + nixos_version, kernel_version, build_date + ) + + return description + + +def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str], machine_id: str) -> None: + kernel = copy_from_profile(profile, generation, specialisation, "kernel") + initrd = copy_from_profile(profile, generation, specialisation, "initrd") + try: + append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets") + subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)]) + except FileNotFoundError: + pass + entry_file = "@efiSysMountPoint@/loader/entries/%s" % ( + generation_conf_filename(profile, generation, specialisation)) + generation_dir = os.readlink(system_dir(profile, generation, specialisation)) + tmp_path = "%s.tmp" % (entry_file) + kernel_params = "init=%s/init " % generation_dir + + with open("%s/kernel-params" % (generation_dir)) as params_file: + kernel_params = kernel_params + params_file.read() + with open(tmp_path, 'w') as f: + f.write(BOOT_ENTRY.format(profile=" [" + profile + "]" if profile else "", + specialisation=" (%s)" % specialisation if specialisation else "", + generation=generation, + kernel=kernel, + initrd=initrd, + kernel_params=kernel_params, + description=describe_generation(generation_dir))) + if machine_id is not None: + f.write("machine-id %s\n" % machine_id) + os.rename(tmp_path, entry_file) + + +def mkdir_p(path: str) -> None: + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST or not os.path.isdir(path): + raise + + +def get_generations(profile: Optional[str] = None) -> List[SystemIdentifier]: + gen_list = subprocess.check_output([ + "@nix@/bin/nix-env", + "--list-generations", + "-p", + "/nix/var/nix/profiles/%s" % ("system-profiles/" + profile if profile else "system"), + "--option", "build-users-group", ""], + universal_newlines=True) + gen_lines = gen_list.split('\n') + gen_lines.pop() + + configurationLimit = @configurationLimit@ + configurations = [ + SystemIdentifier( + profile=profile, + generation=int(line.split()[0]), + specialisation=None + ) + for line in gen_lines + ] + return configurations[-configurationLimit:] + + +def get_specialisations(profile: Optional[str], generation: int, _: Optional[str]) -> List[SystemIdentifier]: + specialisations_dir = os.path.join( + system_dir(profile, generation, None), "specialisation") + if not os.path.exists(specialisations_dir): + return [] + return [SystemIdentifier(profile, generation, spec) for spec in os.listdir(specialisations_dir)] + + +def remove_old_entries(gens: List[SystemIdentifier]) -> None: + rex_profile = re.compile("^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$") + rex_generation = re.compile("^@efiSysMountPoint@/loader/entries/nixos.*-generation-(.*)\.conf$") + known_paths = [] + for gen in gens: + known_paths.append(copy_from_profile(*gen, "kernel", True)) + known_paths.append(copy_from_profile(*gen, "initrd", True)) + for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"): + try: + if rex_profile.match(path): + prof = rex_profile.sub(r"\1", path) + else: + prof = "system" + gen_number = int(rex_generation.sub(r"\1", path)) + if not (prof, gen_number) in gens: + os.unlink(path) + except ValueError: + pass + for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"): + if not path in known_paths and not os.path.isdir(path): + os.unlink(path) + + +def get_profiles() -> List[str]: + if os.path.isdir("/nix/var/nix/profiles/system-profiles/"): + return [x + for x in os.listdir("/nix/var/nix/profiles/system-profiles/") + if not x.endswith("-link")] + else: + return [] + + +def main() -> None: + parser = argparse.ArgumentParser(description='Update NixOS-related systemd-boot files') + parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot') + args = parser.parse_args() + + try: + with open("/etc/machine-id") as machine_file: + machine_id = machine_file.readlines()[0] + except IOError as e: + if e.errno != errno.ENOENT: + raise + # Since systemd version 232 a machine ID is required and it might not + # be there on newly installed systems, so let's generate one so that + # bootctl can find it and we can also pass it to write_entry() later. + cmd = ["@systemd@/bin/systemd-machine-id-setup", "--print"] + machine_id = subprocess.run( + cmd, text=True, check=True, stdout=subprocess.PIPE + ).stdout.rstrip() + + if os.getenv("NIXOS_INSTALL_GRUB") == "1": + warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning) + os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1" + + if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1": + # bootctl uses fopen() with modes "wxe" and fails if the file exists. + if os.path.exists("@efiSysMountPoint@/loader/loader.conf"): + os.unlink("@efiSysMountPoint@/loader/loader.conf") + + flags = [] + + if "@canTouchEfiVariables@" != "1": + flags.append("--no-variables") + + if "@graceful@" == "1": + flags.append("--graceful") + + subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@"] + flags + ["install"]) + else: + # Update bootloader to latest if needed + systemd_version = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2] + sdboot_status = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True) + + # See status_binaries() in systemd bootctl.c for code which generates this + m = re.search("^\W+File:.*/EFI/(BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$", + sdboot_status, re.IGNORECASE | re.MULTILINE) + + needs_install = False + + if m is None: + print("could not find any previously installed systemd-boot, installing.") + # Let systemd-boot attempt an installation if a previous one wasn't found + needs_install = True + else: + sdboot_version = f'({m.group(2)})' + if systemd_version != sdboot_version: + print("updating systemd-boot from %s to %s" % (sdboot_version, systemd_version)) + needs_install = True + + if needs_install: + subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"]) + + mkdir_p("@efiSysMountPoint@/efi/nixos") + mkdir_p("@efiSysMountPoint@/loader/entries") + + gens = get_generations() + for profile in get_profiles(): + gens += get_generations(profile) + remove_old_entries(gens) + for gen in gens: + try: + write_entry(*gen, machine_id) + for specialisation in get_specialisations(*gen): + write_entry(*specialisation, machine_id) + if os.readlink(system_dir(*gen)) == args.default_config: + write_loader_conf(*gen) + except OSError as e: + profile = f"profile '{gen.profile}'" if gen.profile else "default profile" + print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr) + + for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False): + relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/") + actual_root = os.path.join("@efiSysMountPoint@", relative_root) + + for file in files: + actual_file = os.path.join(actual_root, file) + + if os.path.exists(actual_file): + os.unlink(actual_file) + os.unlink(os.path.join(root, file)) + + if not len(os.listdir(actual_root)): + os.rmdir(actual_root) + os.rmdir(root) + + mkdir_p("@efiSysMountPoint@/efi/nixos/.extra-files") + + subprocess.check_call("@copyExtraFiles@") + + # Since fat32 provides little recovery facilities after a crash, + # it can leave the system in an unbootable state, when a crash/outage + # happens shortly after an update. To decrease the likelihood of this + # event sync the efi filesystem after each update. + rc = libc.syncfs(os.open("@efiSysMountPoint@", os.O_RDONLY)) + if rc != 0: + print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix new file mode 100644 index 00000000000..c07567ec82e --- /dev/null +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -0,0 +1,303 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.boot.loader.systemd-boot; + + efi = config.boot.loader.efi; + + systemdBootBuilder = pkgs.substituteAll { + src = ./systemd-boot-builder.py; + + isExecutable = true; + + inherit (pkgs) python3; + + systemd = config.systemd.package; + + nix = config.nix.package.out; + + timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else ""; + + editor = if cfg.editor then "True" else "False"; + + configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit; + + inherit (cfg) consoleMode graceful; + + inherit (efi) efiSysMountPoint canTouchEfiVariables; + + memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else ""; + + netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else ""; + + copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' + empty_file=$(mktemp) + + ${concatStrings (mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n} + '') cfg.extraFiles)} + + ${concatStrings (mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n} + '') cfg.extraEntries)} + ''; + }; + + checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" { + nativeBuildInputs = [ pkgs.mypy ]; + } '' + install -m755 ${systemdBootBuilder} $out + mypy \ + --no-implicit-optional \ + --disallow-untyped-calls \ + --disallow-untyped-defs \ + $out + ''; +in { + + imports = + [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ]) + ]; + + options.boot.loader.systemd-boot = { + enable = mkOption { + default = false; + + type = types.bool; + + description = "Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager"; + }; + + editor = mkOption { + default = true; + + type = types.bool; + + description = '' + Whether to allow editing the kernel command-line before + boot. It is recommended to set this to false, as it allows + gaining root access by passing init=/bin/sh as a kernel + parameter. However, it is enabled by default for backwards + compatibility. + ''; + }; + + configurationLimit = mkOption { + default = null; + example = 120; + type = types.nullOr types.int; + description = '' + Maximum number of latest generations in the boot menu. + Useful to prevent boot partition running out of disk space. + + <literal>null</literal> means no limit i.e. all generations + that were not garbage collected yet. + ''; + }; + + consoleMode = mkOption { + default = "keep"; + + type = types.enum [ "0" "1" "2" "auto" "max" "keep" ]; + + description = '' + The resolution of the console. The following values are valid: + + <itemizedlist> + <listitem><para> + <literal>"0"</literal>: Standard UEFI 80x25 mode + </para></listitem> + <listitem><para> + <literal>"1"</literal>: 80x50 mode, not supported by all devices + </para></listitem> + <listitem><para> + <literal>"2"</literal>: The first non-standard mode provided by the device firmware, if any + </para></listitem> + <listitem><para> + <literal>"auto"</literal>: Pick a suitable mode automatically using heuristics + </para></listitem> + <listitem><para> + <literal>"max"</literal>: Pick the highest-numbered available mode + </para></listitem> + <listitem><para> + <literal>"keep"</literal>: Keep the mode selected by firmware (the default) + </para></listitem> + </itemizedlist> + ''; + }; + + memtest86 = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Make MemTest86 available from the systemd-boot menu. MemTest86 is a + program for testing memory. MemTest86 is an unfree program, so + this requires <literal>allowUnfree</literal> to be set to + <literal>true</literal>. + ''; + }; + + entryFilename = mkOption { + default = "memtest86.conf"; + type = types.str; + description = '' + <literal>systemd-boot</literal> orders the menu entries by the config file names, + so if you want something to appear after all the NixOS entries, + it should start with <filename>o</filename> or onwards. + ''; + }; + }; + + netbootxyz = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Make <literal>netboot.xyz</literal> available from the + <literal>systemd-boot</literal> menu. <literal>netboot.xyz</literal> + is a menu system that allows you to boot OS installers and + utilities over the network. + ''; + }; + + entryFilename = mkOption { + default = "o_netbootxyz.conf"; + type = types.str; + description = '' + <literal>systemd-boot</literal> orders the menu entries by the config file names, + so if you want something to appear after all the NixOS entries, + it should start with <filename>o</filename> or onwards. + ''; + }; + }; + + extraEntries = mkOption { + type = types.attrsOf types.lines; + default = {}; + example = literalExpression '' + { "memtest86.conf" = ''' + title MemTest86 + efi /efi/memtest86/memtest86.efi + '''; } + ''; + description = '' + Any additional entries you want added to the <literal>systemd-boot</literal> menu. + These entries will be copied to <filename>/boot/loader/entries</filename>. + Each attribute name denotes the destination file name, + and the corresponding attribute value is the contents of the entry. + + <literal>systemd-boot</literal> orders the menu entries by the config file names, + so if you want something to appear after all the NixOS entries, + it should start with <filename>o</filename> or onwards. + ''; + }; + + extraFiles = mkOption { + type = types.attrsOf types.path; + default = {}; + example = literalExpression '' + { "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; } + ''; + description = '' + A set of files to be copied to <filename>/boot</filename>. + Each attribute name denotes the destination file name in + <filename>/boot</filename>, while the corresponding + attribute value specifies the source file. + ''; + }; + + graceful = mkOption { + default = false; + + type = types.bool; + + description = '' + Invoke <literal>bootctl install</literal> with the <literal>--graceful</literal> option, + which ignores errors when EFI variables cannot be written or when the EFI System Partition + cannot be found. Currently only applies to random seed operations. + + Only enable this option if <literal>systemd-boot</literal> otherwise fails to install, as the + scope or implication of the <literal>--graceful</literal> option may change in the future. + ''; + }; + + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; + message = "This kernel does not support the EFI boot stub"; + } + ] ++ concatMap (filename: [ + { + assertion = !(hasInfix "/" filename); + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; + } + { + assertion = hasSuffix ".conf" filename; + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; + } + ]) (builtins.attrNames cfg.extraEntries) + ++ concatMap (filename: [ + { + assertion = !(hasPrefix "/" filename); + message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash"; + } + { + assertion = !(hasInfix ".." filename); + message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory"; + } + { + assertion = !(hasInfix "nixos/.extra-files" (toLower filename)); + message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory"; + } + ]) (builtins.attrNames cfg.extraFiles); + + boot.loader.grub.enable = mkDefault false; + + boot.loader.supportsInitrdSecrets = true; + + boot.loader.systemd-boot.extraFiles = mkMerge [ + # TODO: This is hard-coded to use the 64-bit EFI app, but it could probably + # be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI + # app filename is BOOTIA32.efi. + (mkIf cfg.memtest86.enable { + "efi/memtest86/BOOTX64.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi"; + }) + (mkIf cfg.netbootxyz.enable { + "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}"; + }) + ]; + + boot.loader.systemd-boot.extraEntries = mkMerge [ + (mkIf cfg.memtest86.enable { + "${cfg.memtest86.entryFilename}" = '' + title MemTest86 + efi /efi/memtest86/BOOTX64.efi + ''; + }) + (mkIf cfg.netbootxyz.enable { + "${cfg.netbootxyz.entryFilename}" = '' + title netboot.xyz + efi /efi/netbootxyz/netboot.xyz.efi + ''; + }) + ]; + + system = { + build.installBootLoader = checkedSystemdBootBuilder; + + boot.loader.id = "systemd-boot"; + + requiredKernelConfig = with config.lib.kernelConfig; [ + (isYes "EFI_STUB") + ]; + }; + }; +} |