mirror of
https://github.com/BigfootACA/arch-image-builder.git
synced 2024-11-11 07:57:53 +08:00
449 lines
12 KiB
Python
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)
|