at 18.09-beta 8.2 kB view raw
1#! @python3@/bin/python3 -B 2import argparse 3import shutil 4import os 5import sys 6import errno 7import subprocess 8import glob 9import tempfile 10import errno 11import warnings 12import ctypes 13libc = ctypes.CDLL("libc.so.6") 14import re 15import datetime 16import glob 17import os.path 18 19def copy_if_not_exists(source, dest): 20 if not os.path.exists(dest): 21 shutil.copyfile(source, dest) 22 23def system_dir(profile, generation): 24 if profile: 25 return "/nix/var/nix/profiles/system-profiles/%s-%d-link" % (profile, generation) 26 else: 27 return "/nix/var/nix/profiles/system-%d-link" % (generation) 28 29BOOT_ENTRY = """title NixOS{profile} 30version Generation {generation} {description} 31linux {kernel} 32initrd {initrd} 33options {kernel_params} 34""" 35 36def write_loader_conf(profile, generation): 37 with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f: 38 if "@timeout@" != "": 39 f.write("timeout @timeout@\n") 40 if profile: 41 f.write("default nixos-%s-generation-%d\n" % (profile, generation)) 42 else: 43 f.write("default nixos-generation-%d\n" % (generation)) 44 if not @editor@: 45 f.write("editor 0\n"); 46 f.write("console-mode @consoleMode@\n"); 47 os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf") 48 49def profile_path(profile, generation, name): 50 return os.readlink("%s/%s" % (system_dir(profile, generation), name)) 51 52def copy_from_profile(profile, generation, name, dry_run=False): 53 store_file_path = profile_path(profile, generation, name) 54 suffix = os.path.basename(store_file_path) 55 store_dir = os.path.basename(os.path.dirname(store_file_path)) 56 efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix) 57 if not dry_run: 58 copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path)) 59 return efi_file_path 60 61def describe_generation(generation_dir): 62 try: 63 with open("%s/nixos-version" % generation_dir) as f: 64 nixos_version = f.read() 65 except IOError: 66 nixos_version = "Unknown" 67 68 kernel_dir = os.path.dirname(os.path.realpath("%s/kernel" % generation_dir)) 69 module_dir = glob.glob("%s/lib/modules/*" % kernel_dir)[0] 70 kernel_version = os.path.basename(module_dir) 71 72 build_time = int(os.path.getctime(generation_dir)) 73 build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F') 74 75 description = "NixOS {}, Linux Kernel {}, Built on {}".format( 76 nixos_version, kernel_version, build_date 77 ) 78 79 return description 80 81def write_entry(profile, generation, machine_id): 82 kernel = copy_from_profile(profile, generation, "kernel") 83 initrd = copy_from_profile(profile, generation, "initrd") 84 try: 85 append_initrd_secrets = profile_path(profile, generation, "append-initrd-secrets") 86 subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)]) 87 except FileNotFoundError: 88 pass 89 if profile: 90 entry_file = "@efiSysMountPoint@/loader/entries/nixos-%s-generation-%d.conf" % (profile, generation) 91 else: 92 entry_file = "@efiSysMountPoint@/loader/entries/nixos-generation-%d.conf" % (generation) 93 generation_dir = os.readlink(system_dir(profile, generation)) 94 tmp_path = "%s.tmp" % (entry_file) 95 kernel_params = "systemConfig=%s init=%s/init " % (generation_dir, generation_dir) 96 97 with open("%s/kernel-params" % (generation_dir)) as params_file: 98 kernel_params = kernel_params + params_file.read() 99 with open(tmp_path, 'w') as f: 100 f.write(BOOT_ENTRY.format(profile=" [" + profile + "]" if profile else "", 101 generation=generation, 102 kernel=kernel, 103 initrd=initrd, 104 kernel_params=kernel_params, 105 description=describe_generation(generation_dir))) 106 if machine_id is not None: 107 f.write("machine-id %s\n" % machine_id) 108 os.rename(tmp_path, entry_file) 109 110def mkdir_p(path): 111 try: 112 os.makedirs(path) 113 except OSError as e: 114 if e.errno != errno.EEXIST or not os.path.isdir(path): 115 raise 116 117def get_generations(profile=None): 118 gen_list = subprocess.check_output([ 119 "@nix@/bin/nix-env", 120 "--list-generations", 121 "-p", 122 "/nix/var/nix/profiles/%s" % ("system-profiles/" + profile if profile else "system"), 123 "--option", "build-users-group", ""], 124 universal_newlines=True) 125 gen_lines = gen_list.split('\n') 126 gen_lines.pop() 127 return [ (profile, int(line.split()[0])) for line in gen_lines ] 128 129def remove_old_entries(gens): 130 rex_profile = re.compile("^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$") 131 rex_generation = re.compile("^@efiSysMountPoint@/loader/entries/nixos.*-generation-(.*)\.conf$") 132 known_paths = [] 133 for gen in gens: 134 known_paths.append(copy_from_profile(*gen, "kernel", True)) 135 known_paths.append(copy_from_profile(*gen, "initrd", True)) 136 for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"): 137 try: 138 if rex_profile.match(path): 139 prof = rex_profile.sub(r"\1", path) 140 else: 141 prof = "system" 142 gen = int(rex_generation.sub(r"\1", path)) 143 if not (prof, gen) in gens: 144 os.unlink(path) 145 except ValueError: 146 pass 147 for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"): 148 if not path in known_paths: 149 os.unlink(path) 150 151def get_profiles(): 152 if os.path.isdir("/nix/var/nix/profiles/system-profiles/"): 153 return [x 154 for x in os.listdir("/nix/var/nix/profiles/system-profiles/") 155 if not x.endswith("-link")] 156 else: 157 return [] 158 159def main(): 160 parser = argparse.ArgumentParser(description='Update NixOS-related systemd-boot files') 161 parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot') 162 args = parser.parse_args() 163 164 try: 165 with open("/etc/machine-id") as machine_file: 166 machine_id = machine_file.readlines()[0] 167 except IOError as e: 168 if e.errno != errno.ENOENT: 169 raise 170 # Since systemd version 232 a machine ID is required and it might not 171 # be there on newly installed systems, so let's generate one so that 172 # bootctl can find it and we can also pass it to write_entry() later. 173 cmd = ["@systemd@/bin/systemd-machine-id-setup", "--print"] 174 machine_id = subprocess.check_output(cmd).rstrip() 175 176 if os.getenv("NIXOS_INSTALL_GRUB") == "1": 177 warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning) 178 os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1" 179 180 if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1": 181 # bootctl uses fopen() with modes "wxe" and fails if the file exists. 182 if os.path.exists("@efiSysMountPoint@/loader/loader.conf"): 183 os.unlink("@efiSysMountPoint@/loader/loader.conf") 184 185 if "@canTouchEfiVariables@" == "1": 186 subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "install"]) 187 else: 188 subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "--no-variables", "install"]) 189 190 mkdir_p("@efiSysMountPoint@/efi/nixos") 191 mkdir_p("@efiSysMountPoint@/loader/entries") 192 193 gens = get_generations() 194 for profile in get_profiles(): 195 gens += get_generations(profile) 196 remove_old_entries(gens) 197 for gen in gens: 198 write_entry(*gen, machine_id) 199 if os.readlink(system_dir(*gen)) == args.default_config: 200 write_loader_conf(*gen) 201 202 # Since fat32 provides little recovery facilities after a crash, 203 # it can leave the system in an unbootable state, when a crash/outage 204 # happens shortly after an update. To decrease the likelihood of this 205 # event sync the efi filesystem after each update. 206 rc = libc.syncfs(os.open("@efiSysMountPoint@", os.O_RDONLY)) 207 if rc != 0: 208 print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr) 209 210if __name__ == '__main__': 211 main()