diff --git a/modules/module-list.nix b/modules/module-list.nix index cf47e42665d..3fa741fa5d9 100644 --- a/modules/module-list.nix +++ b/modules/module-list.nix @@ -199,7 +199,9 @@ ./system/activation/top-level.nix ./system/boot/kernel.nix ./system/boot/loader/efi-boot-stub/efi-boot-stub.nix + ./system/boot/loader/efi.nix ./system/boot/loader/generations-dir/generations-dir.nix + ./system/boot/loader/gummiboot/gummiboot.nix ./system/boot/loader/raspberrypi/raspberrypi.nix ./system/boot/loader/grub/grub.nix ./system/boot/loader/grub/memtest.nix diff --git a/modules/rename.nix b/modules/rename.nix index 534137d4e09..858056732a1 100644 --- a/modules/rename.nix +++ b/modules/rename.nix @@ -99,4 +99,11 @@ in zipModules ([] ++ rename deprecated "kde.extraPackages" "environment.kdePackages" # ++ rename obsolete "environment.kdePackages" "environment.systemPackages" # !!! doesn't work! +# Multiple efi bootloaders now +++ rename obsolete "boot.loader.efiBootStub.efiSysMountPoint" "boot.loader.efi.efiSysMountPoint" +++ rename obsolete "boot.loader.efiBootStub.efiDisk" "boot.loader.efi.efibootmgr.efiDisk" +++ rename obsolete "boot.loader.efiBootStub.efiPartition" "boot.loader.efi.efibootmgr.efiPartition" +++ rename obsolete "boot.loader.efiBootStub.postEfiBootMgrCommands" "boot.loader.efi.efibootmgr.postEfiBootMgrCommands" +++ rename obsolete "boot.loader.efiBootStub.runEfibootmgr" "boot.loader.efi.efibootmgr.enable" + ) # do not add renaming after this. diff --git a/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix b/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix index 618c8d7737a..529de0f0e54 100644 --- a/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix +++ b/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix @@ -8,7 +8,13 @@ let isExecutable = true; inherit (pkgs) bash; path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.glibc] ++ (pkgs.stdenv.lib.optionals config.boot.loader.efiBootStub.runEfibootmgr [pkgs.efibootmgr pkgs.module_init_tools]); - inherit (config.boot.loader.efiBootStub) efiSysMountPoint runEfibootmgr installStartupNsh efiDisk efiPartition postEfiBootMgrCommands; + inherit (config.boot.loader.efiBootStub) installStartupNsh; + + inherit (config.boot.loader.efi) efiSysMountPoint; + + inherit (config.boot.loader.efi.efibootmgr) efiDisk efiPartition postEfiBootMgrCommands; + + runEfibootmgr = config.boot.loader.efi.efibootmgr.enable; efiShell = if config.boot.loader.efiBootStub.installShell then if pkgs.stdenv.isi686 then @@ -51,38 +57,6 @@ in ''; }; - efiDisk = mkOption { - default = "/dev/sda"; - description = '' - The disk that contains the EFI system partition. Only used by - efibootmgr - ''; - }; - - efiPartition = mkOption { - default = "1"; - description = '' - The partition number of the EFI system partition. Only used by - efibootmgr - ''; - }; - - efiSysMountPoint = mkOption { - default = "/boot"; - description = '' - Where the EFI System Partition is mounted. - ''; - }; - - runEfibootmgr = mkOption { - default = false; - description = '' - Whether to run efibootmgr to add the configuration to the boot options list. - WARNING! efibootmgr has been rumored to brick Apple firmware on - old kernels! Don't use it on kernels older than 2.6.39! - ''; - }; - installStartupNsh = mkOption { default = false; description = '' @@ -103,17 +77,6 @@ in ''; }; - postEfiBootMgrCommands = mkOption { - default = ""; - type = types.string; - description = '' - Shell commands to be executed immediately after efibootmgr has setup the system EFI. - Some systems do not follow the EFI specifications properly and insert extra entries. - Others will brick (fix by removing battery) on boot when it finds more than X entries. - This hook allows for running a few extra efibootmgr commands to combat these issues. - ''; - }; - }; }; }; diff --git a/modules/system/boot/loader/efi.nix b/modules/system/boot/loader/efi.nix new file mode 100644 index 00000000000..41074908bff --- /dev/null +++ b/modules/system/boot/loader/efi.nix @@ -0,0 +1,53 @@ +{ pkgs, ... }: + +with pkgs.lib; + +{ + options.boot.loader.efi = { + efibootmgr = { + efiDisk = mkOption { + default = "/dev/sda"; + + type = types.string; + + description = "The disk that contains the EFI system partition."; + }; + + enable = mkOption { + default = false; + + type = types.bool; + + description = '' + Whether to run efibootmgr to add the efi bootloaders configuration to the boot options list. + WARNING! efibootmgr has been rumored to brick Apple firmware on + old kernels! Don't use it on kernels older than 2.6.39! + ''; + }; + + efiPartition = mkOption { + default = "1"; + description = "The partition number of the EFI system partition."; + }; + + postEfiBootMgrCommands = mkOption { + default = ""; + type = types.string; + description = '' + Shell commands to be executed immediately after efibootmgr has setup the system EFI. + Some systems do not follow the EFI specifications properly and insert extra entries. + Others will brick (fix by removing battery) on boot when it finds more than X entries. + This hook allows for running a few extra efibootmgr commands to combat these issues. + ''; + }; + }; + + efiSysMountPoint = mkOption { + default = "/boot"; + + type = types.string; + + description = "Where the EFI System Partition is mounted."; + }; + }; +} diff --git a/modules/system/boot/loader/gummiboot/gummiboot-builder.py b/modules/system/boot/loader/gummiboot/gummiboot-builder.py new file mode 100644 index 00000000000..5da9746cdbd --- /dev/null +++ b/modules/system/boot/loader/gummiboot/gummiboot-builder.py @@ -0,0 +1,144 @@ +#! @python@/bin/python +import argparse +import shutil +import os +import errno +import subprocess +import glob +import tempfile + +def copy_if_not_exists(source, dest): + known_paths.append(dest) + if not os.path.exists(dest): + shutil.copyfile(source, dest) + +system_dir = lambda generation: "/nix/var/nix/profiles/system-%d-link" % (generation) + +def write_entry(generation, kernel, initrd): + entry_file = "@efiSysMountPoint@/loader/entries/nixos-generation-%d.conf" % (generation) + if os.path.exists(entry_file): + return + generation_dir = os.readlink(system_dir(generation)) + tmp_path = "%s.tmp" % (entry_file) + kernel_params = "systemConfig=%s init=%s/init " % (generation_dir, generation_dir) + with open("%s/kernel-params" % (generation_dir)) as params_file: + kernel_params = kernel_params + params_file.read() + with open("/etc/machine-id") as machine_file: + machine_id = machine_file.readlines()[0] + with open(tmp_path, 'w') as f: + print >> f, "title NixOS" + print >> f, "version Generation %d" % (generation) + print >> f, "machine-id %s" % (machine_id) + print >> f, "linux %s" % (kernel) + print >> f, "initrd %s" % (initrd) + print >> f, "options %s" % (kernel_params) + os.rename(tmp_path, entry_file) + +def write_loader_conf(generation): + with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f: + if "@timeout@" != "": + print >> f, "timeout @timeout@" + print >> f, "default nixos-generation-%d" % (generation) + os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf") + +def copy_from_profile(generation, name): + store_file_path = os.readlink("%s/%s" % (system_dir(generation), 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) + copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path)) + return efi_file_path + +def add_entry(generation): + efi_kernel_path = copy_from_profile(generation, "kernel") + efi_initrd_path = copy_from_profile(generation, "initrd") + write_entry(generation, efi_kernel_path, efi_initrd_path) + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST or not os.path.isdir(path): + raise + +def get_generations(profile): + gen_list = subprocess.check_output([ + "@nix@/bin/nix-env", + "--list-generations", + "-p", + "/nix/var/nix/profiles/%s" % (profile) + ]) + gen_lines = gen_list.split('\n') + gen_lines.pop() + return [ int(line.split()[0]) for line in gen_lines ] + +def remove_old_entries(gens): + slice_start = len("@efiSysMountPoint@/loader/entries/nixos-generation-") + slice_end = -1 * len(".conf") + for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos-generation-[1-9][0-9]*.conf"): + gen = int(path[slice_start:slice_end]) + if not gen in gens: + os.unlink(path) + for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"): + if not path in known_paths: + os.unlink(path) + +def update_gummiboot(): + mkdir_p("@efiSysMountPoint@/efi/gummiboot") + store_file_path = "@gummiboot@/bin/gummiboot.efi" + store_dir = os.path.basename("@gummiboot@") + efi_file_path = "/efi/gummiboot/%s-gummiboot.efi" % (store_dir) + copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path)) + return efi_file_path + +def update_efibootmgr(path): + subprocess.call(["@kmod@/sbin/modprobe", "efivars"]) + post_efibootmgr = """ +@postEfiBootMgrCommands@ + """ + efibootmgr_entries = subprocess.check_output(["@efibootmgr@/sbin/efibootmgr"]).split("\n") + for entry in efibootmgr_entries: + columns = entry.split() + if len(columns) > 2: + if ' '.join(columns[1:3]) == "NixOS gummiboot": + subprocess.call([ + "@efibootmgr@/sbin/efibootmgr", + "-B", + "-b", + columns[0][4:8] + ]) + subprocess.call([ + "@efibootmgr@/sbin/efibootmgr", + "-c", + "-d", + "@efiDisk@", + "-g", + "-l", + path.replace("/", "\\"), + "-L", + "NixOS gummiboot", + "-p", + "@efiPartition@", + ]) + subprocess.call(post_efibootmgr, shell=True) + +parser = argparse.ArgumentParser(description='Update NixOS-related gummiboot files') +parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot') +args = parser.parse_args() + +known_paths = [] +mkdir_p("@efiSysMountPoint@/efi/nixos") +mkdir_p("@efiSysMountPoint@/loader/entries") +gens = get_generations("system") +for gen in gens: + add_entry(gen) + if os.readlink(system_dir(gen)) == args.default_config: + write_loader_conf(gen) + +remove_old_entries(gens) + +# We deserve our own env var! +if os.getenv("NIXOS_INSTALL_GRUB") == "1": + gummiboot_path = update_gummiboot() + if "@runEfibootmgr@" == "1": + update_efibootmgr(gummiboot_path) diff --git a/modules/system/boot/loader/gummiboot/gummiboot.nix b/modules/system/boot/loader/gummiboot/gummiboot.nix new file mode 100644 index 00000000000..8ae0693923c --- /dev/null +++ b/modules/system/boot/loader/gummiboot/gummiboot.nix @@ -0,0 +1,71 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + cfg = config.boot.loader.gummiboot; + + efi = config.boot.loader.efi; + + gummibootBuilder = pkgs.substituteAll { + src = ./gummiboot-builder.py; + + isExecutable = true; + + inherit (pkgs) python gummiboot kmod efibootmgr; + + inherit (config.environment) nix; + + inherit (cfg) timeout; + + inherit (efi) efiSysMountPoint; + + inherit (efi.efibootmgr) postEfiBootMgrCommands efiDisk efiPartition; + + runEfibootmgr = efi.efibootmgr.enable; + }; +in { + options.boot.loader.gummiboot = { + enable = mkOption { + default = false; + + type = types.bool; + + description = "Whether to enable the gummiboot UEFI boot manager"; + }; + + timeout = mkOption { + default = null; + + example = 4; + + type = types.nullOr types.int; + + description = '' + Timeout (in seconds) for how long to show the menu (null if none). + Note that even with no timeout the menu can be forced if the space + key is pressed during bootup + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (config.boot.kernelPacakges.kernel.features or { efiBootStub = true; }) ? efiBootStub; + + message = "This kernel does not support the EFI boot stub"; + } + ]; + + system = { + build.installBootLoader = gummibootBuilder; + + boot.loader.id = "gummiboot"; + + requiredKernelConfig = with config.lib.kernelConfig; [ + (isYes "EFI_STUB") + ]; + }; + }; +}