diff --git a/meta-arm/classes/uefi_capsule.bbclass b/meta-arm/classes/uefi_capsule.bbclass index a0709c0f..9d98959d 100644 --- a/meta-arm/classes/uefi_capsule.bbclass +++ b/meta-arm/classes/uefi_capsule.bbclass @@ -1,51 +1,94 @@ # This class generates UEFI capsules -# The current class supports generating a capsule with single firmware binary +# The current class supports generating a capsule with multiple firmware binaries IMAGE_TYPES += "uefi_capsule" -# u-boot-tools should be installed in the native sysroot directory -do_image_uefi_capsule[depends] += "u-boot-tools-native:do_populate_sysroot" +# edk2-basetools should be installed in the native sysroot directory +do_image_uefi_capsule[depends] += "edk2-basetools-native:do_populate_sysroot" # By default the wic image is used to create a capsule CAPSULE_IMGTYPE ?= "wic" # IMGDEPLOYDIR is used as the default location of firmware binary for which the capsule needs to be created -CAPSULE_IMGLOCATION ?= "${IMGDEPLOYDIR}" +CAPSULE_IMG_LOCATION ?= "${IMGDEPLOYDIR}" # The generated capsule by default has uefi.capsule extension CAPSULE_EXTENSION ?= "uefi.capsule" # The generated capsule's name by default is the same as UEFI_FIRMWARE_BINARY -CAPSULE_NAME ?= "${UEFI_FIRMWARE_BINARY}" +CAPSULE_NAME ??= "${UEFI_FIRMWARE_BINARY}" + +# The generated capsule configuration file extension +CAPSULE_CONFIG_FILE_EXTENSION ?= "json" + +# The generated capsule configuration file +CAPSULE_CONFIG_FILE ?= "${IMGDEPLOYDIR}/${CAPSULE_NAME}.${CAPSULE_CONFIG_FILE_EXTENSION}" + +# Path to the script that generates the UEFI capsule payloads JSON +UEFI_CAPSULE_CONFIG_GENERATOR_SCRIPT ?= "${META_ARM_LAYER_DIR}/scripts/generate_capsule_json_multiple.py" + +# Additional variables for capsule component filtering +CAPSULE_ALL_COMPONENTS ?= "" +CAPSULE_SELECTED_COMPONENTS ??= "" + +# Variables required by the EDK2 GenerateCapsule tool. +CAPSULE_CERTIFICATE_PATHS ?= "" +CAPSULE_FW_VERSIONS ?= "" +CAPSULE_GUIDS ?= "" +CAPSULE_INDEXES ?= "" +CAPSULE_HARDWARE_INSTANCES ?= "" +CAPSULE_LOWEST_SUPPORTED_VERSIONS ?= "" +CAPSULE_MONOTONIC_COUNTS ?= "" +CAPSULE_PRIVATE_KEY_PATHS ?= "" +UEFI_FIRMWARE_BINARIES ?= "" +PAYLOAD_CERTIFICATE_PATH ?= "" +PAYLOAD_PRIVATE_KEY_PATH ?= "" -# The following variables must be set to be able to generate a capsule update -CAPSULE_CERTIFICATE_PATH ?= "" -CAPSULE_FW_VERSION ?= "" -CAPSULE_GUID ?= "" -CAPSULE_INDEX ?= "" -CAPSULE_MONOTONIC_COUNT ?= "" -CAPSULE_PRIVATE_KEY_PATH ?= "" -UEFI_FIRMWARE_BINARY ?= "" # Check if the required variables are set python() { - for var in ["CAPSULE_CERTIFICATE_PATH", "CAPSULE_FW_VERSION", \ - "CAPSULE_GUID", "CAPSULE_INDEX", \ - "CAPSULE_MONOTONIC_COUNT", "CAPSULE_PRIVATE_KEY_PATH", \ - "UEFI_FIRMWARE_BINARY"]: + for var in ["CAPSULE_CERTIFICATE_PATHS", "CAPSULE_FW_VERSIONS", \ + "CAPSULE_GUIDS", "CAPSULE_INDEXES", \ + "CAPSULE_HARDWARE_INSTANCES", \ + "CAPSULE_LOWEST_SUPPORTED_VERSIONS", \ + "CAPSULE_MONOTONIC_COUNTS", "CAPSULE_PRIVATE_KEY_PATHS", \ + "UEFI_FIRMWARE_BINARIES", \ + "UEFI_CAPSULE_CONFIG_GENERATOR_SCRIPT", \ + "CAPSULE_ALL_COMPONENTS", \ + "CAPSULE_SELECTED_COMPONENTS", \ + "PAYLOAD_CERTIFICATE_PATH", \ + "PAYLOAD_PRIVATE_KEY_PATH"]: if not d.getVar(var): raise bb.parse.SkipRecipe(f"{var} not set") } IMAGE_CMD:uefi_capsule(){ - mkeficapsule --certificate ${CAPSULE_CERTIFICATE_PATH} \ - --fw-version ${CAPSULE_FW_VERSION} \ - --guid ${CAPSULE_GUID} \ - --index ${CAPSULE_INDEX} \ - --monotonic-count ${CAPSULE_MONOTONIC_COUNT} \ - --private-key ${CAPSULE_PRIVATE_KEY_PATH} \ - ${UEFI_FIRMWARE_BINARY} \ - ${CAPSULE_IMGLOCATION}/${CAPSULE_NAME}.${CAPSULE_EXTENSION} + # Generates the UEFI capsule payloads JSON + ${PYTHON} ${UEFI_CAPSULE_CONFIG_GENERATOR_SCRIPT} \ + --selected_components ${CAPSULE_SELECTED_COMPONENTS}\ + --components ${CAPSULE_ALL_COMPONENTS}\ + --fw_versions ${CAPSULE_FW_VERSIONS} \ + --guids ${CAPSULE_GUIDS} \ + --hardware_instances ${CAPSULE_HARDWARE_INSTANCES} \ + --lowest_supported_versions ${CAPSULE_LOWEST_SUPPORTED_VERSIONS} \ + --monotonic_counts ${CAPSULE_MONOTONIC_COUNTS} \ + --payloads ${UEFI_FIRMWARE_BINARIES} \ + --update_image_indexes ${CAPSULE_INDEXES} \ + --private_keys ${CAPSULE_PRIVATE_KEY_PATHS} \ + --certificates ${CAPSULE_CERTIFICATE_PATHS} \ + --output ${CAPSULE_CONFIG_FILE} + + # Force the GenerateCapsule script to use python3 + export PYTHON_COMMAND=${PYTHON} + + # Append the certificate to the private key to create a PEM bundle compatible with EDK2 tools + cat ${PAYLOAD_CERTIFICATE_PATH} >> ${PAYLOAD_PRIVATE_KEY_PATH} + + # Generate the UEFI capsule image using the EDK2 GenerateCapsule tool + ${STAGING_BINDIR_NATIVE}/edk2-BaseTools/BinWrappers/PosixLike/GenerateCapsule \ + -e -j ${CAPSULE_CONFIG_FILE} \ + ${CAPSULE_EXTRA_ARGS} \ + -o ${CAPSULE_IMG_LOCATION}/${CAPSULE_NAME}.${CAPSULE_EXTENSION} } # The firmware binary should be created before generating the capsule diff --git a/meta-arm/conf/layer.conf b/meta-arm/conf/layer.conf index 753f5259..8f7e1a43 100644 --- a/meta-arm/conf/layer.conf +++ b/meta-arm/conf/layer.conf @@ -21,3 +21,6 @@ HOSTTOOLS_NONFATAL += "telnet" addpylib ${LAYERDIR}/lib oeqa WARN_QA:append:layer-meta-arm = " patch-status" + +# Define base directory for meta-arm +META_ARM_LAYER_DIR := "${LAYERDIR}" diff --git a/meta-arm/scripts/generate_capsule_json_multiple.py b/meta-arm/scripts/generate_capsule_json_multiple.py new file mode 100644 index 00000000..13425748 --- /dev/null +++ b/meta-arm/scripts/generate_capsule_json_multiple.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: Copyright 2025 Arm Limited and/or its +# affiliates +# +# SPDX-License-Identifier: MIT + +""" +Capsule Payload JSON Generator + +This script creates a JSON file that defines multiple capsule payloads. +Each payload is constructed using command-line input and includes key +metadata like firmware version, GUID, hardware instance, and more. + +Usage: + python generate_capsule_json_multiple.py \ + --fw_versions 1 1 2 \ + --guids guid1 guid2 guid3 \ + --hardware_instances 1 1 1 \ + --lowest_supported_versions 0 0 0 \ + --monotonic_counts 1 1 1 \ + --payloads bl2.bin initramfs.bin tfm_s.bin \ + --update_image_indexes 1 4 2 \ + --private_keys key.key key.key key.key \ + --certificates cert.crt cert.crt cert.crt \ + --components bl2 initramfs tfm_s \ + --selected_components bl2 \ + --output capsule_generation_config.json +""" + +import json +import argparse +from typing import List + + +def parse_arguments() -> argparse.Namespace: + """Parses command-line arguments.""" + parser = argparse.ArgumentParser( + description="Generate a JSON file for multiple Capsule Payloads." + ) + + parser.add_argument( + "--selected_components", default=[], nargs="*", required=False, + help=( + "Filters the payloads to include only those for the selected " + "components (e.g., bl2, initramfs)." + "All components are included when not specified." + ) + ) + + parser.add_argument( + "--output", default="capsule_payloads.json", help="Output JSON file name" + ) + + # Required arguments for each payload entry + required_args = { + "components": "List of components", + "fw_versions": "List of firmware versions", + "guids": "List of GUIDs", + "hardware_instances": "List of hardware instances", + "lowest_supported_versions": "List of lowest supported firmware versions", + "monotonic_counts": "List of monotonic counts", + "payloads": "List of payload file paths", + "update_image_indexes": "List of update image indexes", + "private_keys": "List of private key file paths", + "certificates": "List of certificate file paths", + } + + for arg, desc in required_args.items(): + parser.add_argument(f"--{arg}", nargs="+", required=True, help=desc) + + return parser.parse_args() + + +def validate_input_lengths(args: argparse.Namespace) -> None: + """Ensures all required input lists have the same length (excluding output and selected_components).""" + list_lengths = { + attr: len(getattr(args, attr)) + for attr in vars(args) + if attr not in {"output", "selected_components"} # Ignore optional fields + } + + for attr, length in list_lengths.items(): + if length == 0: + raise ValueError(f"Input list '{attr}' cannot be empty!") + + if len(set(list_lengths.values())) != 1: + raise ValueError("All input lists must have the same length!") + +def create_payloads(args: argparse.Namespace) -> List[dict]: + """Generates the list of payload dictionaries to include in the final JSON.""" + num_payloads = len(args.components) + selected_payloads = [] + + for i in range(num_payloads): + + # If filtering is enabled, skip if not in the allowed components list + if args.components[i] not in args.selected_components: + continue + + payload = { + "Component": args.components[i], + "FwVersion": args.fw_versions[i], + "Guid": args.guids[i], + "HardwareInstance": args.hardware_instances[i], + "LowestSupportedVersion": args.lowest_supported_versions[i], + "MonotonicCount": args.monotonic_counts[i], + "Payload": args.payloads[i], + "UpdateImageIndex": args.update_image_indexes[i], + "OpenSslSignerPrivateCertFile": args.private_keys[i], + "OpenSslTrustedPublicCertFile": args.certificates[i], + "OpenSslOtherPublicCertFile": args.certificates[i], + } + + selected_payloads.append(payload) + + if not selected_payloads: + raise ValueError("None of the provided components match the selected_components list!") + + return selected_payloads + + +def write_json_file(output_path: str, payloads: List[dict]) -> None: + """Writes the list of payloads to a JSON file with basic error handling.""" + try: + with open(output_path, "w", encoding="utf-8") as file: + json.dump({"Payloads": payloads}, file, indent=4) + print(f"JSON file created: {output_path}") + except (OSError) as e: + print(f"Failed to write JSON file to {output_path}: {e}") + except TypeError as e: + print(f"Invalid data format in payloads: {e}") + + +def main() -> None: + """Main script entry point.""" + args = parse_arguments() + validate_input_lengths(args) + payloads = create_payloads(args) + write_json_file(args.output, payloads) + + +if __name__ == "__main__": + main()