Skip to content

Instantly share code, notes, and snippets.

@CertainLach
Created April 5, 2025 18:48
Show Gist options
  • Save CertainLach/d3a5e42500c58451544c363f7265bea1 to your computer and use it in GitHub Desktop.
Save CertainLach/d3a5e42500c58451544c363f7265bea1 to your computer and use it in GitHub Desktop.
Heads NixOS bootloader generator
{
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";
};
}
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