Created
July 8, 2018 22:59
-
-
Save betaboon/97abed457de8be43f89e7ca49d33d58d to your computer and use it in GitHub Desktop.
refind-nixos-module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! @python3@/bin/python3 -B | |
import argparse | |
import os | |
import os.path | |
import sys | |
import errno | |
import subprocess | |
import glob | |
import datetime | |
import shutil | |
import ctypes | |
libc = ctypes.CDLL("libc.so.6") | |
extra_config = ''' | |
@extraConfig@ | |
''' | |
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 system_dir(profile, generation): | |
if profile: | |
return "/nix/var/nix/profiles/system-profiles/%s-%d-link" % (profile, generation) | |
return "/nix/var/nix/profiles/system-%d-link" % (generation) | |
def copy_if_not_exists(source, dest): | |
if not os.path.exists(dest): | |
shutil.copyfile(source, dest) | |
def get_generations(profile=None): | |
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() | |
return [(profile, int(line.split()[0])) for line in gen_lines] | |
def get_profiles(): | |
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") | |
] | |
return [] | |
def profile_path(profile, generation, name): | |
return os.readlink("%s/%s" % (system_dir(profile, generation), name)) | |
def copy_from_profile(profile, generation, name, dry_run=False): | |
store_file_path = profile_path(profile, 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) | |
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): | |
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 generation_details(profile, generation): | |
kernel = copy_from_profile(profile, generation, "kernel") | |
initrd = copy_from_profile(profile, generation, "initrd") | |
generation_dir = os.readlink(system_dir(profile, generation)) | |
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() | |
description = describe_generation(generation_dir) | |
return { | |
"profile": profile, | |
"generation": generation, | |
"kernel": kernel, | |
"initrd": initrd, | |
"kernel_params": kernel_params, | |
"description": description | |
} | |
MENU_ENTRY = """ | |
menuentry "NixOS" {{ | |
loader {kernel} | |
initrd {initrd} | |
options "{kernel_params}" | |
{submenuentries} | |
}} | |
""" | |
SUBMENUENTRY = """ | |
submenuentry "Generation {generation} {description}" {{ | |
loader {kernel} | |
initrd {initrd} | |
options "{kernel_params}" | |
}} | |
""" | |
def write_refind_config(path, default_generation, generations): | |
with open(path, 'w') as f: | |
if "@timeout@" != "": | |
f.write("timeout @timeout@") | |
f.write(extra_config) | |
rev_generations = sorted(generations, key=lambda x: x[1], reverse=True) | |
submenuentries = [] | |
for generation in rev_generations: | |
submenuentries.append(SUBMENUENTRY.format( | |
**generation_details(*generation) | |
)) | |
f.write(MENU_ENTRY.format( | |
**generation_details(*default_generation), | |
submenuentries="\n".join(submenuentries) | |
)) | |
def main(): | |
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() | |
mkdir_p("@efiSysMountPoint@/efi/refind") | |
if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1": | |
if "@canTouchEfiVariables@" == "1": | |
subprocess.check_call( | |
["@refind@/bin/refind-install"], | |
env={"PATH": ":".join([ | |
"@efibootmgr@/bin", | |
"@coreutils@/bin", | |
"@utillinux@/bin", | |
"@gnugrep@/bin", | |
"@gnused@/bin", | |
"@gawk@/bin", | |
])} | |
) | |
else: | |
print("DONT KNOW WHAT TO DO") | |
if "@extraIcons@" != "": | |
icons_dir = "@efiSysMountPoint@/efi/refind/extra-icons" | |
if os.path.exists(icons_dir): | |
if os.path.exists(icons_dir + "-backup"): | |
shutil.rmtree(icons_dir + "-backup") | |
os.rename(icons_dir, icons_dir + "-backup") | |
print("Notice: Backed up existing extra-icons directory as extra-icons-backup.") | |
shutil.copytree("@extraIcons@", icons_dir) | |
generations = get_generations() | |
for profile in get_profiles(): | |
generations += get_generations(profile) | |
default_generation = None | |
for generation in generations: | |
if os.readlink(system_dir(*generation)) == args.default_config: | |
default_generation = generation | |
write_refind_config( | |
"@efiSysMountPoint@/efi/refind/refind.conf", | |
default_generation, | |
generations | |
) | |
# 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ config, lib, pkgs, ... }: | |
with lib; | |
let | |
cfg = config.boot.loader.refind; | |
efi = config.boot.loader.efi; | |
refindBuilder = pkgs.substituteAll { | |
src = ./refind-builder.py; | |
isExecutable = true; | |
inherit (pkgs) python3; | |
nix = config.nix.package.out; | |
timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else ""; | |
extraConfig = cfg.extraConfig; | |
extraIcons = if cfg.extraIcons != null then cfg.extraIcons else ""; | |
inherit (pkgs) refind efibootmgr coreutils gnugrep gnused gawk utillinux; | |
inherit (efi) efiSysMountPoint canTouchEfiVariables; | |
}; | |
in { | |
options.boot.loader.refind = { | |
enable = mkOption { | |
default = false; | |
type = types.bool; | |
description = "Whether to enable the refind EFI boot manager"; | |
}; | |
extraConfig = mkOption { | |
type = types.lines; | |
default = ""; | |
description = "Extra configuration text appended to refind.conf"; | |
}; | |
extraIcons = mkOption { | |
type = types.nullOr types.path; | |
default = null; | |
description = "A directory containing icons to be copied to 'extra-icons'"; | |
}; | |
}; | |
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"; | |
} | |
]; | |
boot.loader.grub.enable = mkDefault false; | |
boot.loader.supportsInitrdSecrets = false; # TODO what does this do ? | |
system = { | |
build.installBootLoader = refindBuilder; | |
boot.loader.id = "refind"; | |
requiredKernelConfig = with config.lib.kernelConfig; [ | |
(isYes "EFI_STUB") | |
]; | |
}; | |
}; | |
} |
Thanks again!
+1! Still works in 2024!
Seems not work with nixos-unstable
. It need a patch to get it working
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey @betaboon! Thanks so much for open-sourcing this script. I'm new to NixOS, and I was looking into using rEFInd as my bootloader, and was thinking along the lines of generating menuentries programatically for NixOS generations, and it looks like these scripts do exactly that. Is it sufficient to just import the .nix file in my configuration.nix and add the right configs? Also, have you used this script recently / do you know if it's still working with 20.04?
Thanks again!
EDIT: Decided to dive in, and it works on 20.04! Thanks so much! For people in the future: you will most likely need to mess with efibootmgr to add rEFInd and/or take out other bootloaders.