Skip to content

Commit

Permalink
Create ESP boot from bootspec (#248)
Browse files Browse the repository at this point in the history
* Add script to generate ESP partition by bootspec

Signed-off-by: Alexander V. Nikolaev <[email protected]>
  • Loading branch information
avnik authored Sep 18, 2023
1 parent a837454 commit 5dd5711
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 37 deletions.
187 changes: 187 additions & 0 deletions modules/hardware/nvidia-jetson-orin/mk-esp-contents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#! @python3@/bin/python3 -B
"""This is opinionated rewrite of nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py,
as well as re-implement some logic from bootctl tool.
Two main goals:
* Use bootspec JSON as primary source of truth about system
* Be enough close to standard bootctl behavior, to allow local system updates
"""
import argparse
import errno
import json
import os
import shutil
import sys
from typing import Optional, Union, List, Dict, TypedDict

BOOT_ENTRY = """title {title}
version Generation {generation} {description}
linux {kernel}
initrd {initrd}
options {kernel_params}
"""

LOADER_CONF = """timeout 0
default nixos-generation-1.conf
console-mode keep
"""

LOADERS = {
"x86_64-linux": ("systemd-bootx64.efi", "BOOTX64.efi"),
"aarch64-linux": ("systemd-bootaa64.efi", "BOOTAA64.efi"),
}


class BootSpec(TypedDict):
kernel: str
initrd: str
init: str
kernelParams: List[str]
system: str
label: str


def eprint(*args: str) -> None:
print(*args, file=sys.stderr)


def mkdir_p(path: str) -> None:
if os.path.isdir(path):
return
eprint(f"Create directory {path}")
try:
os.makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST or not os.path.isdir(path):
raise


def write_file(path: str, content: str) -> None:
mkdir_p(os.path.dirname(path))
eprint(f"Writing {path}")
with open(path, "w") as f:
f.write(content)


def ensure_file(name: str) -> None:
if os.path.exists(name):
eprint(f"Found required file {name}")
else:
eprint(f"Can't find required file {name}")
sys.exit(1)


def copy_file(src: str, dst: str) -> None:
mkdir_p(os.path.dirname(dst))
eprint(f"Copying {src} to {dst}")
shutil.copy(src, dst)


def make_efi_name(store_file_path: str, root: str = "/") -> str:
suffix = os.path.basename(store_file_path)
store_dir = os.path.basename(os.path.dirname(store_file_path))
return os.path.join(root, "EFI/nixos/%s-%s.efi" % (store_dir, suffix))


def copy_loader(loader: str, esp: str, target_name: str) -> None:
efi = os.path.join(esp, "EFI")
copy_file(loader, os.path.join(efi, "systemd", os.path.basename(loader)))
copy_file(loader, os.path.join(efi, "BOOT", target_name))


def copy_nixos(esp: str, kernel: str, initrd: str, dtb: Optional[str] = None) -> None:
copy_file(kernel, make_efi_name(kernel, esp))
copy_file(initrd, make_efi_name(initrd, esp))
if dtb:
copy_file(dtb, make_efi_name(dtb, esp))


def write_loader_entry(
esp: str,
boot: BootSpec,
kernel_params: List[str],
machine_id: Optional[str],
random_seed: Optional[str],
device_tree: Optional[str],
) -> None:
entry = BOOT_ENTRY.format(
kernel=make_efi_name(boot["kernel"]),
initrd=make_efi_name(boot["initrd"]),
init=boot["init"],
kernel_params=" ".join(kernel_params),
description=boot["label"],
generation=1,
title="NixOS",
)
if machine_id:
entry += f"machine-id {machine_id}"

if device_tree:
dt = make_efi_name(device_tree)
entry += f"\ndevicetree /{dt}"

write_file(os.path.join(esp, "loader/entries/nixos-generation-1.conf"), entry)
write_file(os.path.join(esp, "loader/loader.conf"), LOADER_CONF)
write_file(os.path.join(esp, "loader/entries.srel"), "type1\n")
if random_seed:
copy_file(random_seed, os.path.join(esp, "loader/random_seed"))


def read_bootspec_file(toplevel: str) -> BootSpec:
bootfile = os.path.join(toplevel, "boot.json")
ensure_file(bootfile)
with open(bootfile, "r") as boot_json:
content = json.load(boot_json)
bootspec: Optional[BootSpec] = content.get("org.nixos.bootspec.v1")
if bootspec is None:
eprint(f"""Can't find "org.nixos.bootspec.v1" in {bootfile}""")
sys.exit(1)
return bootspec


def create_esp_contents(
toplevel: str,
output: str,
machine_id: Optional[str],
random_seed: Optional[str],
device_tree: Optional[str],
) -> None:
mkdir_p(output)
boot = read_bootspec_file(toplevel)
system = boot["system"]
selected_loader = LOADERS.get(system)
if selected_loader is None:
eprint(f"Haven't loader for system {system}")
sys.exit(1)
(loader, target_loader_filename) = selected_loader
loader = os.path.join(toplevel, "systemd/lib/systemd/boot/efi", loader)
ensure_file(loader)
copy_loader(loader, output, target_loader_filename)
copy_nixos(output, boot["kernel"], boot["initrd"], device_tree)
kernel_params = boot["kernelParams"]
init = boot["init"]
kernel_params.insert(0, f"init={init}")
write_loader_entry(
output, boot, kernel_params, machine_id, random_seed, device_tree
)


def main() -> None:
parser = argparse.ArgumentParser(description="Generate contents of ESP partition")
parser.add_argument(
"--toplevel", help="NixOS toplevel directory, contains boot.json"
)
parser.add_argument("--output", help="Output directory")
parser.add_argument("--device-tree", default=None, help="Device tree file")
parser.add_argument("--random-seed", default=None, help="Random seed file")
parser.add_argument(
"--machine-id", default=None, help="Machine id to add to loader's entry"
)
args = parser.parse_args()
create_esp_contents(
args.toplevel, args.output, args.machine_id, args.random_seed, args.device_tree
)


if __name__ == "__main__":
main()
58 changes: 21 additions & 37 deletions modules/hardware/nvidia-jetson-orin/sdimage.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,31 @@
disabledModules = [(modulesPath + "/profiles/all-hardware.nix")];

sdImage = let
kernelPath = "${config.boot.kernelPackages.kernel}/" + "${config.system.boot.loader.kernelFile}";
initrdPath = "${config.system.build.initialRamdisk}/" + "${config.system.boot.loader.initrdFile}";
mkESPContentSource = pkgs.substituteAll {
src = ./mk-esp-contents.py;
isExecutable = true;
inherit (pkgs.buildPackages) python3;
};
mkESPContent =
pkgs.runCommand "mk-esp-contents" {
nativeBuildInputs = with pkgs; [mypy python3];
} ''
install -m755 ${mkESPContentSource} $out
mypy \
--no-implicit-optional \
--disallow-untyped-calls \
--disallow-untyped-defs \
$out
'';
fdtPath = "${config.hardware.deviceTree.package}/${config.hardware.deviceTree.name}";
loaderConf = pkgs.writeText "loader.conf" ''
timeout 0
default nixos-generation-1.conf
console-mode keep
'';
entriesSrel = pkgs.writeText "entries.srel" ''
type1
'';
entry = pkgs.writeText "nixos-generation-1.conf" ''
title NixOS
version Generation 1
linux /EFI/nixos/${config.system.boot.loader.kernelFile}
initrd /EFI/nixos/${config.system.boot.loader.initrdFile}
options init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
devicetree /EFI/nixos/${config.hardware.deviceTree.name}
'';
in {
firmwareSize = 256;
# TODO: Replace contents of the populateFirmwareCommands with proper
# bootpsec-based ESP partition generation.
populateFirmwareCommands = ''
mkdir -pv firmware/EFI/systemd
cp -v ${config.systemd.package}/lib/systemd/boot/efi/systemd-bootaa64.efi firmware/EFI/systemd/systemd-bootaa64.efi
mkdir -pv firmware/EFI/BOOT
cp -v ${config.systemd.package}/lib/systemd/boot/efi/systemd-bootaa64.efi firmware/EFI/BOOT/BOOTAA64.EFI
mkdir -pv firmware/loader/entries
cp -v ${loaderConf} firmware/loader/loader.conf
cp -v ${entriesSrel} firmware/loader/entries.srel
cp -v ${entry} firmware/loader/entries/nixos-generation-1.conf
mkdir -pv firmware/EFI/nixos
cp -v ${kernelPath} "./firmware/EFI/nixos/${config.system.boot.loader.kernelFile}"
cp -v ${initrdPath} "./firmware/EFI/nixos/${config.system.boot.loader.initrdFile}"
cp -v ${fdtPath} "./firmware/EFI/nixos/${config.hardware.deviceTree.name}"
mkdir -pv firmware
${mkESPContent} \
--toplevel ${config.system.build.toplevel} \
--output firmware/ \
--device-tree ${fdtPath}
'';
populateRootCommands = ''
'';
Expand Down

0 comments on commit 5dd5711

Please sign in to comment.