# Unified kernel image (UKI) class
#
# This bbclass merges kernel, initrd etc as a UKI standard UEFI binary,
# to be loaded with UEFI firmware and systemd-boot on target HW.
# TPM PCR pre-calculation is not supported since systemd-measure tooling
# is meant to run on target, not in cross compile environment.
#
# See:
# https://www.freedesktop.org/software/systemd/man/latest/ukify.html
# https://uapi-group.org/specifications/specs/unified_kernel_image/
#
# The UKI contains:
#
#   - UEFI stub
#     The linux kernel can generate a UEFI stub, however the one from systemd-boot can fetch
#     the command line from a separate section of the EFI application, avoiding the need to
#     rebuild the kernel.
#   - kernel
#   - initramfs
#   - kernel command line
#   - uname -r kernel version
#   - /etc/os-release to create a boot menu with version details
#   - optionally secure boot signature(s)
#   - other metadata (e.g. TPM PCR measurements)
#
# Usage instructions:
#
#   - requires UEFI compatible firmware on target, e.g. qemuarm64-secureboot u-boot based
#     from meta-arm or qemux86 ovmf/edk2 based firmware for x86_64
#
#   - Distro/build config:
#
#     INIT_MANAGER = "systemd"
#     MACHINE_FEATURES:append = " efi"
#     EFI_PROVIDER = "systemd-boot"
#     INITRAMFS_IMAGE = "core-image-minimal-initramfs"
#
#   - image recipe:
#
#     inherit uki
#
#   - qemuboot/runqemu changes in image recipe or build config:
#
#     # Kernel command line must be inside the signed uki
#     QB_KERNEL_ROOT = ""
#     # kernel is in the uki image, not loaded separately
#     QB_DEFAULT_KERNEL = "none"
#
#   - for UEFI secure boot, systemd-boot and uki (including kernel) can
#     be signed but require sbsign-tool-native (recipe available from meta-secure-core,
#     see also qemuarm64-secureboot from meta-arm). Set variable
#     UKI_SB_KEY to path of private key and UKI_SB_CERT for certificate.
#     Note that systemd-boot also need to be signed with the same key.
#
#   - at runtime, UEFI firmware will load and boot systemd-boot which
#     creates a menu from all detected uki binaries. No need to manually
#     setup boot menu entries.
#
#   - see efi-uki-bootdisk.wks.in how to create ESP partition which hosts systemd-boot,
#     config file(s) for systemd-boot and the UKI binaries.
#

DEPENDS += "\
    os-release \
    systemd-boot \
    systemd-boot-native \
    virtual/cross-binutils \
    virtual/kernel \
"

inherit image-artifact-names
require ../conf/image-uefi.conf

INITRAMFS_IMAGE ?= "core-image-minimal-initramfs"

INITRD_ARCHIVE ?= "${INITRAMFS_IMAGE}-${MACHINE}.${INITRAMFS_FSTYPES}"

do_image_complete[depends] += "${INITRAMFS_IMAGE}:do_image_complete"

UKIFY_CMD ?= "ukify build"
UKI_CONFIG_FILE ?= "${UNPACKDIR}/uki.conf"
UKI_FILENAME ?= "uki.efi"
UKI_KERNEL_FILENAME ?= "${KERNEL_IMAGETYPE}"
UKI_CMDLINE ?= "rootwait root=LABEL=root"
# secure boot keys and cert, needs sbsign-tools-native (meta-secure-core)
#UKI_SB_KEY ?= ""
#UKI_SB_CERT ?= ""

IMAGE_EFI_BOOT_FILES ?= "${UKI_FILENAME};EFI/Linux/${UKI_FILENAME}"

do_uki[depends] += " \
                        systemd-boot:do_deploy \
                        virtual/kernel:do_deploy \
                     "
do_uki[depends] += "${@ '${INITRAMFS_IMAGE}:do_image_complete' if d.getVar('INITRAMFS_IMAGE') else ''}"

# ensure that the build directory is empty everytime we generate a newly-created uki
do_uki[cleandirs] = "${B}"
# influence the build directory at the start of the builds
do_uki[dirs] = "${B}"

# we want to allow specifying files in SRC_URI, such as for signing the UKI
python () {
    d.delVarFlag("do_fetch","noexec")
    d.delVarFlag("do_unpack","noexec")
}

# main task
python do_uki() {
    import glob
    import bb.process

    # base ukify command, can be extended if needed
    ukify_cmd = d.getVar('UKIFY_CMD')

    deploy_dir_image = d.getVar('DEPLOY_DIR_IMAGE')

    # architecture
    target_arch = d.getVar('EFI_ARCH')
    if target_arch:
        ukify_cmd += " --efi-arch %s" % (target_arch)

    # systemd stubs
    stub = "%s/linux%s.efi.stub" % (d.getVar('DEPLOY_DIR_IMAGE'), target_arch)
    if not os.path.exists(stub):
        bb.fatal(f"ERROR: cannot find {stub}.")
    ukify_cmd += " --stub %s" % (stub)

    # initrd
    initramfs_image = "%s" % (d.getVar('INITRD_ARCHIVE'))
    ukify_cmd += " --initrd=%s" % (os.path.join(deploy_dir_image, initramfs_image))

    # kernel
    kernel_filename = d.getVar('UKI_KERNEL_FILENAME') or None
    if kernel_filename:
        kernel = "%s/%s" % (deploy_dir_image, kernel_filename)
        if not os.path.exists(kernel):
            bb.fatal(f"ERROR: cannot find %s" % (kernel))
        ukify_cmd += " --linux=%s" % (kernel)
        # not always needed, ukify can detect version from kernel binary
        kernel_version = d.getVar('KERNEL_VERSION')
        if kernel_version:
            ukify_cmd += "--uname %s" % (kernel_version)
    else:
        bb.fatal("ERROR - UKI_KERNEL_FILENAME not set")

    # command line
    cmdline = d.getVar('UKI_CMDLINE')
    if cmdline:
        ukify_cmd += " --cmdline='%s'" % (cmdline)

    # dtb
    if d.getVar('KERNEL_DEVICETREE'):
        for dtb in d.getVar('KERNEL_DEVICETREE').split():
            dtb_path = "%s/%s" % (deploy_dir_image, dtb)
            if not os.path.exists(dtb_path):
                bb.fatal(f"ERROR: cannot find {dtb_path}.")
            ukify_cmd += " --devicetree %s" % (dtb_path)

    # custom config for ukify
    if os.path.exists(d.getVar('UKI_CONFIG_FILE')):
        ukify_cmd += " --config=%s" % (d.getVar('UKI_CONFIG_FILE'))

    # systemd tools
    ukify_cmd += " --tools=%s%s/lib/systemd/tools" % \
        (d.getVar("RECIPE_SYSROOT_NATIVE"), d.getVar("prefix"))

    # version
    ukify_cmd += " --os-release=@%s%s/lib/os-release" % \
        (d.getVar("RECIPE_SYSROOT"), d.getVar("prefix"))

    # TODO: tpm2 measure for secure boot, depends on systemd-native and TPM tooling
    # needed in systemd > 254 to fulfill ConditionSecurity=measured-uki
    # Requires TPM device on build host, thus not supported at build time.
    #ukify_cmd += " --measure"

    # securebooot signing, also for kernel
    key = d.getVar('UKI_SB_KEY')
    if key:
        ukify_cmd += " --sign-kernel --secureboot-private-key='%s'" % (key)
    cert = d.getVar('UKI_SB_CERT')
    if cert:
        ukify_cmd += " --secureboot-certificate='%s'" % (cert)

    # custom output UKI filename
    output = " --output=%s/%s" % (d.getVar('DEPLOY_DIR_IMAGE'), d.getVar('UKI_FILENAME'))
    ukify_cmd += " %s" % (output)

    # Run the ukify command
    bb.debug(2, "uki: running command: %s" % (ukify_cmd))
    out, err = bb.process.run(ukify_cmd, shell=True)
    bb.debug(2, "%s\n%s" % (out, err))
}
addtask uki after do_rootfs before do_deploy do_image_complete do_image_wic
