Created
April 5, 2025 18:48
-
-
Save CertainLach/d3a5e42500c58451544c363f7265bea1 to your computer and use it in GitHub Desktop.
Heads NixOS bootloader generator
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, | |
... | |
}: | |
let | |
inherit (lib) | |
mkIf | |
types | |
mkOption | |
mkEnableOption | |
; | |
cfg = config.boot.heads; | |
in | |
{ | |
options = { | |
boot.heads = { | |
enable = mkEnableOption '' | |
Whether to generate HEADS compatable configuration file | |
under `/boot/kexec_menu.txt`. | |
See [HEADS's documentation](https://osresearch.net/BootOptions/) | |
for more information. | |
''; | |
configurationLimit = mkOption { | |
default = 20; | |
example = 10; | |
type = types.int; | |
description = '' | |
Maximum number of configurations in the boot menu. | |
''; | |
}; | |
}; | |
}; | |
config = mkIf cfg.enable { | |
boot.loader.external.enable = true; | |
# TODO: Rewrite in Rust | |
boot.loader.external.installHook = pkgs.writeScript "install-kexec-menu" '' | |
#!/bin/sh | |
${pkgs.deno}/bin/deno run --allow-write=/boot --allow-read=/boot --allow-read=/nix/store --allow-read=/nix/var/nix/profiles ${./main.ts} /boot ${toString cfg.configurationLimit} >&2 | |
''; | |
system.boot.loader.id = "heads"; | |
}; | |
} |
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
function usage(): never { | |
console.error(`usage: install-kexec-menu <boot-dir> <num-generations>`) | |
Deno.exit(1); | |
} | |
if (Deno.args.length !== 2) { | |
usage() | |
} | |
const [bootDir, _numGenerations] = Deno.args; | |
const numGenerations = parseInt(_numGenerations, 10); | |
console.info(`Installing Heads configuration to /${bootDir}`); | |
const binaryDir = `${bootDir}/nixos`; | |
await Deno.mkdir(binaryDir, { recursive: true }); | |
async function copyBinary(name: string): Promise<string> { | |
const cleanedName = name.replace(/^\/nix\/store\//, '').replaceAll('/', '-'); | |
const target = `${binaryDir}/${cleanedName}`; | |
try { | |
const stat = await Deno.stat(target); | |
if (stat.isFile) { | |
console.info(`skipping copying ${name}`); | |
} else { | |
throw new Error(`not a file: ${target}`); | |
} | |
} catch (e) { | |
if (e instanceof Deno.errors.NotFound) { | |
const tmpfile = await Deno.makeTempFile({ | |
dir: binaryDir, | |
}); | |
await Deno.copyFile(name, tmpfile) | |
await Deno.rename(tmpfile, target); | |
} | |
} | |
return `/nixos/${cleanedName}`; | |
} | |
const ids = []; | |
for await (const entry of Deno.readDir('/nix/var/nix/profiles')) { | |
const matched = entry.name.match(/^system-(\d+)-link$/); | |
if (matched === null) continue; | |
const id = parseInt(matched[1], 10); | |
ids.push(id); | |
} | |
console.log(`discovered ${ids.length} entries`); | |
const entries: string[] = []; | |
for (const id of ids.sort((a, b) => b - a).slice(0, numGenerations)) { | |
console.log(`processing entry ${id}`) | |
const dir = `/nix/var/nix/profiles/system-${id}-link`; | |
const bootSpec = JSON.parse(await Deno.readTextFile(`${dir}/boot.json`)); | |
const stat = await Deno.lstat(dir); | |
// Shouldn't be null, afaik | |
const statDate = stat.ctime ?? new Date(); | |
const generationDate = `${statDate.getFullYear()}-${(statDate.getMonth() + 1).toString().padStart(2, '0')}-${statDate.getDate().toString().padStart(2, '0')}` | |
if ("org.nixos.bootspec.v1" in bootSpec) { | |
const nixosBootspec = bootSpec['org.nixos.bootspec.v1']; | |
const initrdName = await copyBinary(nixosBootspec.initrd); | |
const kernelName = await copyBinary(nixosBootspec.kernel); | |
const generationName = `${nixosBootspec.label} (Generation ${id}, ${generationDate})`; | |
const kernelParams = `init=${nixosBootspec.init}${nixosBootspec.kernelParams.map((v: string) => ` ${v}`).join('')}`; | |
entries.push(`${generationName}|elf|kernel ${kernelName}|initrd ${initrdName}|append ${kernelParams}`); | |
// Xen support also requires fixed bootloader check in xen-dom0.nix | |
if ("org.xenproject.bootspec.v1" in bootSpec) { | |
const xenBootspec = bootSpec['org.xenproject.bootspec.v1']; | |
if (!xenBootspec.multibootPath) { | |
console.error('skipping xen entry installation, as bootspec was generated for the older version of nixpkgs, not supporting multiboot'); | |
continue; | |
} | |
const multibootName = await copyBinary(xenBootspec.multibootPath); | |
entries.splice(entries.length - 1, 0, | |
`${generationName} (with Xen Hypervisor)|xen|kernel ${multibootName} placeholder${xenBootspec.params.map((v: string) => ` ${v}`).join('')}|module ${kernelName} placeholder ${kernelParams}|module ${initrdName}`); | |
} | |
} | |
} | |
const tmpmenu = await Deno.makeTempFile({ dir: bootDir }); | |
await Deno.writeTextFile(tmpmenu, entries.join('\n') + '\n'); | |
await Deno.rename(tmpmenu, `${bootDir}/kexec_menu.txt`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment