@@ -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
@@ -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}"
new file mode 100644
@@ -0,0 +1,142 @@
+# SPDX-FileCopyrightText: <text>Copyright 2025 Arm Limited and/or its
+# affiliates <open-source-office@arm.com></text>
+#
+# 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()