arch-image-builder/builder/build/grub.py
BigfootACA 067ee2d341 add more comments
Signed-off-by: BigfootACA <bigfoot@classfun.cn>
2024-05-20 09:56:42 +08:00

449 lines
12 KiB
Python

import os
import shutil
from logging import getLogger
from builder.lib.context import ArchBuilderContext
from builder.lib.config import ArchBuilderConfigError
from builder.lib.loop import loop_get_backing, loop_get_offset
from builder.lib.blkid import Blkid
from builder.lib.mount import MountPoint
log = getLogger(__name__)
blkid = Blkid()
modules = [
"part_msdos", "part_gpt", "part_apple", "ext2", "fat", "ntfs", "sleep",
"ufs1", "ufs2", "cpio", "sleep", "search", "search_fs_file", "minicmd",
"search_fs_uuid", "search_label", "reboot", "halt", "gzio", "serial",
"boot", "file", "f2fs", "iso9660", "hfs", "hfsplus", "zfs", "minix",
"memdisk", "sfs", "lvm", "http", "tftp", "udf", "xfs", "date", "echo",
"all_video", "btrfs", "disk", "configfile", "terminal",
]
def get_prop(
ctx: ArchBuilderContext,
name: str,
cfg: dict,
path: bool = False,
multi: bool = False,
) -> str | None:
"""
Get a config value for grub
"""
value = ctx.get(f"kernel.{name}", None)
if name in cfg: value = cfg[name]
if value is None: return None
if type(value) is str:
value = [value]
if len(value) == 0: return None
if path:
# must starts with /
for i in range(len(value)):
if not value[i].startswith("/"):
value[i] = "/" + value[i]
if multi: value = " ".join(value)
else: value = value[0]
return value
def fstype_to_mod(name: str) -> str:
"""
Map filesystem type to GRUB2 modules name
"""
match name:
case "ext3": return "ext2"
case "ext4": return "ext2"
case "vfat": return "fat"
case "fat12": return "fat"
case "fat16": return "fat"
case "fat32": return "fat"
case "msdos": return "fat"
# TODO: add more filesystems
case _: return name
def gen_menuentry(ctx: ArchBuilderContext, cfg: dict) -> str:
"""
Generate a menuentry config for grub
"""
ret = ""
# menuentry name (default to Linux)
name = cfg["name"] if "name" in cfg else "Linux"
# kernel image path
kernel = get_prop(ctx, "kernel", cfg, True)
# initramfs image path (supports multiples)
initramfs = get_prop(ctx, "initramfs", cfg, True, True)
# device tree blob path (supports multiples)
devicetree = get_prop(ctx, "devicetree", cfg, True, True)
# kernel command line
cmdline = get_prop(ctx, "cmdline", cfg, False, True)
# the folder to place these files
path = get_prop(ctx, "path", cfg, False, False)
if kernel is None: raise ArchBuilderConfigError("no kernel for grub")
if cmdline is None: cmdline = ""
ret += f"menuentry '{name}' {{\n"
# if path set: load filesystem module and search to set root
if path:
# find out the mount point in fstab
fs = ctx.fstab.find_target(path)
if fs is None or len(fs) == 0 or fs[0] is None:
raise ArchBuilderConfigError(f"mountpoint {path} not found")
dev = fs[0].source
# map to virtual file system
if dev in ctx.fsmap: dev = ctx.fsmap[dev]
# get the filesystem UUID to search
uuid = blkid.get_tag_value(None, "UUID", dev)
if uuid is None: raise RuntimeError(f"cannot detect uuid for {path}")
# load filesystem module and search target
ret += "\tinsmod %s\n" % fstype_to_mod(fs[0].fstype)
ret += f"\tsearch --no-floppy --fs-uuid --set=root {uuid}\n"
# add device tree blob field
if devicetree:
ret += "\techo 'Loading Device Tree...'\n"
ret += f"\tdevicetree {devicetree}\n"
# add kernel path field and kernel command line
ret += "\techo 'Loading Kernel...'\n"
ret += f"\tlinux {kernel} {cmdline}\n"
# add initramfs field
if initramfs:
ret += "\techo 'Loading Initramfs...'\n"
ret += f"\tinitrd {initramfs}\n"
# boot into linux (not add 'boot' command, its imply by menuentry)
ret += "\techo 'Booting...'\n"
ret += f"}}\n"
return ret
def gen_basic(ctx: ArchBuilderContext) -> str:
ret = ""
# load generic modules
ret += "insmod part_gpt\n"
ret += "insmod part_msdos\n"
ret += "insmod all_video\n"
# setup console and serial
ret += "terminal_input console\n"
ret += "terminal_output console\n"
ret += "if serial --unit=0 --speed=115200; then\n"
ret += "\tterminal_input --append console\n"
ret += "\tterminal_output --append console\n"
ret += "fi\n"
# set grub timeout seconds
ret += "set timeout_style=menu\n"
timeout = ctx.get("bootloader.timeout", 5)
ret += f"set timeout={timeout}\n"
# find out the default entry
default = 0
items = ctx.get("bootloader.items", [])
for idx in range(len(items)):
item = items[idx]
if "default" in item and item["default"]:
default = idx
ret += f"set default={default}\n"
return ret
def mkconfig(ctx: ArchBuilderContext) -> str:
"""
Generate a full grub config for current rootfs
"""
ret = ""
ret += gen_basic(ctx)
for item in ctx.get("bootloader.items", []):
ret += gen_menuentry(ctx, item)
return ret
def proc_targets(ctx: ArchBuilderContext, install: str):
"""
Copy grub target folder directly
"""
copies = [".mod", ".lst"]
folder = os.path.join(ctx.get_rootfs(), "usr/lib/grub")
for target in ctx.get("grub.targets", []):
# target name format: i386-pc, arm64-efi
if "/" in target: raise ArchBuilderConfigError(f"bad target {target}")
base = os.path.join(folder, target)
# at least we need linux.mod
if not os.path.exists(os.path.join(base, "linux.mod")):
raise ArchBuilderConfigError(f"target {target} not found")
dest = os.path.join(install, target)
os.makedirs(dest, mode=0o0755, exist_ok=True)
# copy grub target
for file in os.listdir(base):
if not any((file.endswith(name) for name in copies)):
continue
shutil.copyfile(
os.path.join(base, file),
os.path.join(dest, file),
)
log.info(f"installed grub target {target}")
def proc_config(ctx: ArchBuilderContext, install: str):
"""
Generate a full grub config for current rootfs and write to install folder
"""
content = mkconfig(ctx)
cfg = os.path.join(install, "grub.cfg")
with open(cfg, "w") as f:
f.write(content)
log.info(f"generated grub config {cfg}")
def efi_arch_name(target: str) -> str:
"""
Map grub target name to UEFI arch name
"""
match target:
case "arm64-efi": return "aa64"
case "x86_64-efi": return "x64"
case "arm-efi": return "arm"
case "i386-efi": return "ia32"
case "riscv64-efi": return "riscv64"
case _: raise RuntimeError(
f"unsupported {target} for efi name"
)
def efi_boot_name(target: str) -> str:
"""
Map grub target name to UEFI default boot file name
"""
name = efi_arch_name(target)
return f"boot{name}.efi"
def proc_mkimage_efi(ctx: ArchBuilderContext, target: str):
"""
Create GRUB EFI image for boot
"""
cmds = ["grub-mkimage"]
root = ctx.get_rootfs()
# allowed esp folders
efi_folders = ["/boot", "/boot/efi", "/efi", "/esp"]
# grub2 source folder in rootfs (WORKSPACE/TARGET/rootfs/usr/lib/grub/x86_64-efi)
base = os.path.join(root, "usr/lib/grub", target)
# install path in rootfs (/boot/grub)
install = ctx.get("grub.path", "/boot/grub")
# why this function called proc_mkimage_efi?
if not target.endswith("-efi"):
raise RuntimeError("mkimage efi only for *-efi")
# must ends with /
fdir = os.path.realpath(install) + "/"
# find out requires mount point
esp: MountPoint | None = None # UEFI system partition
grub: MountPoint | None = None # GRUB install folder
for mnt in ctx.fstab:
# esp must be fat
if fstype_to_mod(mnt.fstype) == "fat":
if mnt.target in efi_folders:
esp = mnt
# add an end slash to avoid same prefix (likes /boot /bootfs)
tdir = mnt.target
if not tdir.endswith("/"): tdir += "/"
# grub install folder
if fdir.startswith(tdir):
# find out the deepest mount point
# to avoid / but installed in /boot
if (not grub) or mnt.level >= grub.level:
grub = mnt
if esp is None: raise RuntimeError("efi partition not found")
if grub is None: raise RuntimeError("grub install folder not found")
# grub install target folder (/boot/grub)
if not install.startswith("/"):
install = "/" + install
# must in grub install folder
if not install.startswith(grub.target):
raise RuntimeError("grub install prefix not found")
# get grub install path in target partition
# Mount GRUB Install Prefix
# /boot /boot/grub /grub
# / /boot/grub /boot/grub
prefix = install[len(grub.target):]
if not prefix.startswith("/"): prefix = "/" + prefix
# get UUID of grub installed filesystem UUID
device = (ctx.fsmap[grub.source] if grub.source in ctx.fsmap else grub.source)
uuid = blkid.get_tag_value(None, "UUID", device)
if not uuid: raise RuntimeError(
"failed to detect uuid for grub install path"
)
# esp install target folder (boot/efi)
esp_dest = esp.target
if esp_dest.startswith("/"):
esp_dest = esp_dest[1:]
# esp install target folder in rootfs (WORKSPACE/TARGET/rootfs/boot/efi)
efi_folder = os.path.join(root, esp_dest)
# grub install target folder in rootfs (WORKSPACE/TARGET/rootfs/boot/grub)
grub_folder = os.path.join(root, install[1:])
cmds.append(f"--format={target}")
cmds.append(f"--directory={base}")
cmds.append(f"--prefix={prefix}")
cmds.append("--compression=xz")
# put builtin config into grub install folder
builtin = os.path.join(grub_folder, "grub.builtin.cfg")
with open(builtin, "w") as f:
f.write(f"search --no-floppy --fs-uuid --set=root {uuid}\n")
f.write(f"set prefix=\"($root){prefix}\"\n")
f.write("normal\n")
f.write("echo \"Failed to switch into normal mode\"\n")
f.write("sleep 5\n")
cmds.append(f"--config={builtin}")
# efi boot image install folder (WORKSPACE/TARGET/rootfs/boot/efi/efi/boot)
efi = os.path.join(efi_folder, "efi/boot")
os.makedirs(efi, mode=0o0755, exist_ok=True)
# efi boot image (WORKSPACE/TARGET/rootfs/boot/efi/efi/boot/bootx64.efi)
out = os.path.join(efi, efi_boot_name(target))
cmds.append(f"--output={out}")
if os.path.exists(out): os.remove(out)
cmds.extend(modules)
# run grub-mkimage
ret = ctx.run_external(cmds)
if ret != 0: raise OSError("grub-mkimage failed")
log.info(f"generated grub {target} efi image {out}")
def proc_bootsec(ctx: ArchBuilderContext, target: str):
"""
Install boot sector for x86 PC for grub-install
"""
mods = []
cmds = ["grub-install"]
if target != "i386-pc":
raise RuntimeError("bootsec only for i386-pc")
mount = ctx.get_mount()
root = ctx.get_rootfs()
# get grub install base folder (boot)
install: str = ctx.get("grub.path", "/boot/grub")
if install.startswith("/"): install = install[1:]
if install.endswith("/grub"): install = install[:-5]
grub = os.path.join(root, "usr/lib/grub", target)
cmds.append(f"--target={target}")
cmds.append(f"--directory={grub}")
mods.append("part_msdos")
mods.append("part_gpt")
# grub install base folder in mount (WORKSPACE/TARGET/mount/boot)
mnt_install = os.path.join(mount, install)
cmds.append(f"--boot-directory={mnt_install}")
# find out mount point of rootfs
rootfs = ctx.fstab.find_target("/")
if rootfs is None or len(rootfs) <= 0 or rootfs[0] is None:
raise RuntimeError("rootfs mount point not found")
rootfs = rootfs[0]
# add filesystem module for rootfs
mods.append(fstype_to_mod(rootfs.fstype))
if len(mods) > 0:
cmds.append("--modules=" + (" ".join(mods)))
# detect grub boot sector install device
device = ctx.get("grub.device", None)
if device is None:
source = rootfs.source
if source in ctx.fsmap:
source = ctx.fsmap[source]
# loop setup by builder.disk.image.ImageBuilder
if not source.startswith("/dev/loop"):
raise RuntimeError("no device to detect grub install")
# loop offset partition setup by builder.disk.layout.build.DiskLayoutBuilder
if loop_get_offset(source) <= 0:
raise RuntimeError("no loop part to detect grub install")
# backing as parent disk
device = loop_get_backing(source)
if device is None:
raise RuntimeError("no device for grub install")
cmds.append(device)
# run grub-install
ret = ctx.run_external(cmds)
if ret != 0: raise OSError("grub-install failed")
# copy grub installed target from mount to rootfs
src = os.path.join(mnt_install, "grub")
dst = os.path.join(root, install, "grub")
shutil.copytree(src, dst, dirs_exist_ok=True)
def proc_install(ctx: ArchBuilderContext):
"""
Process GRUB targets install
"""
targets: list[str] = ctx.get("grub.targets", [])
for target in targets:
if target == "i386-pc":
proc_bootsec(ctx, target)
elif target.endswith("-efi"):
proc_mkimage_efi(ctx, target)
else: raise ArchBuilderConfigError(
f"unsupported target {target}"
)
def proc_grub(ctx: ArchBuilderContext):
"""
Install GRUB bootloader
"""
root = ctx.get_rootfs()
# get grub install folder in rootfs (WORKSPACE/TARGET/rootfs/boot/grub)
install: str = ctx.get("grub.path", "/boot/grub")
if install.startswith("/"):
install = install[1:]
install = os.path.join(root, install)
os.makedirs(install, mode=0o0755, exist_ok=True)
proc_config(ctx, install)
proc_targets(ctx, install)
proc_install(ctx)