nixfiles/system/common/bootnext.nix

154 lines
4.9 KiB
Nix

{
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.kdePackages.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"];
}
];
};
}