{ config, lib, pkgs, options, ... }: let inherit (lib) types escapeShellArg; cfg = config.nixfiles.common.bootnext; bootNextScriptMain = pkgs.writeShellScript "bootnext-wrapped" '' set -Eeuxo pipefail PATH=${lib.escapeShellArg (with pkgs; lib.makeBinPath [ gnugrep coreutils efibootmgr ])} export PATH function do_bootnext() { uuid="$1" shift entryName="$1" shift efibootmgr -n "$(efibootmgr | grep -Fi "$uuid" | grep -F "$entryName" | cut -d' ' -f1 | tr -dc '[:digit:]')" } case "$1" in ${lib.concatStringsSep "\n" ( lib.mapAttrsToList (name: value: " ${escapeShellArg name}) do_bootnext ${escapeShellArg value.efiPartUUID} ${escapeShellArg value.name} ;;" ) cfg.entries )} *) echo "Boot entry \"$1\" not configured."; exit 1;; esac ''; bootNextScript = pkgs.writeShellScriptBin "bootnext" '' # this wrapper is needed because the sudoers config needs the path to the # actual script and self referencing is a pain. this way we can guarantee # that the script passed is exactly the same as the one in the sudoers # config. i could use realpath but this is probably safer since it is not # evaluated at runtime. who knows. if [[ "$(id -u)" -ne 0 ]]; then exec sudo ${escapeShellArg bootNextScriptMain} "$@" else exec ${escapeShellArg bootNextScriptMain} "$@" fi ''; desktopWrapper = pkgs.writeShellScript "bootnext-desktop-wrapper" '' if ${pkgs.libsForQt5.kdialog}/bin/kdialog --warningyesno "Are you sure you want to reboot?" ; then ${bootNextScript}/bin/bootnext "$@" reboot fi ''; bootnextDesktopEntries = pkgs.symlinkJoin { name = "bootnext-desktop-entries"; paths = lib.mapAttrsToList (name: value: pkgs.makeDesktopItem { name = "bootnext-reboot-${name}"; desktopName = "Reboot into ${value.desktopEntry.name}"; comment = "Select the entry defined by the `${name}` configuration in the bootnext script and then reboot."; icon = "${value.desktopEntry.icon}"; keywords = [ "bootnext" "reboot" "${name}" "${value.desktopEntry.name}" ]; exec = "${desktopWrapper} ${name}"; }) (lib.filterAttrs (_: value: value.desktopEntry.enable) cfg.entries); }; in { options = { nixfiles.common.bootnext = { enable = lib.mkOption { description = '' Whether to enable the bootnext wrapper command for controlling boot order ''; type = types.bool; default = false; example = true; }; enableDesktopEntries = lib.mkEnableOption "generation of bootnext Desktop entries" // { default = true; }; entries = let entryModule = {name, config, ... }: { options = let uuidType = with types; lib.mkOptionType { name = "uuid"; description = "UUID"; descriptionClass = "noun"; check = let uuidRegex = "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"; in x: str.check x && (builtins.match uuidRegex x) != null; inherit (str) merge; }; in { efiPartUUID = lib.mkOption { description = "UUID of EFI partition containing boot entry"; type = uuidType; apply = lib.strings.toLower; }; name = lib.mkOption { description = "Name of boot entry as it appears in efibootmgr"; type = types.str; example = "Windows Boot Manager"; }; desktopEntry = { enable = lib.mkOption { description = "Whether to generate this desktop entry."; type = types.bool; default = true; example = false; }; name = lib.mkOption { description = "Display name of boot entry for desktop entry."; type = types.str; default = config.name; example = "Windows"; }; icon = lib.mkOption { description = "Path or name of icon to use for desktop entry"; type = with types; nullOr str; default = null; }; }; }; }; in lib.mkOption { description = "bootnext entry"; type = with types; attrsOf (submodule entryModule); }; }; }; config = lib.mkIf cfg.enable { environment.systemPackages = [ bootNextScript ] ++ lib.optional cfg.enableDesktopEntries bootnextDesktopEntries; security.sudo.extraRules = lib.mkAfter [ { commands = [ { command = "${bootNextScriptMain}"; options = [ "NOPASSWD" ]; } ]; groups = [ "wheel" ]; } ]; }; }