* [PATCH v2 0/4] Add support for multiple kernels in FIT images
@ 2026-10-20 13:35 Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection Vahid Gharaee
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Vahid Gharaee @ 2026-10-20 13:35 UTC (permalink / raw)
To: openembedded-core
From: Vahid Gharaee <vgharaee@gmail.com>
Changes in v2:
- Shortened commit subjects in patches 2/4 and 3/4
- Moved AI disclosure from the subject to the commit body
Vahid Gharaee (4):
oe.fitimage: Support multiple kernel nodes and configuration selection
oe.fitimage: add metadata for multiple kernels
oe.selftest: Add tests for multi-kernel FIT config
oe.fitimage: Add custom configurations and harden multi-kernel
handling
meta/classes-recipe/kernel-fit-image.bbclass | 129 +++++++++++++--
meta/conf/image-fitimage.conf | 55 +++++++
meta/lib/oe/fitimage.py | 84 ++++++++--
meta/lib/oeqa/selftest/cases/fitimage.py | 156 ++++++++++++++++++-
4 files changed, 397 insertions(+), 27 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection
2026-10-20 13:35 [PATCH v2 0/4] Add support for multiple kernels in FIT images Vahid Gharaee
@ 2026-10-20 13:35 ` Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 2/4] oe.fitimage: add metadata for multiple kernels Vahid Gharaee
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Vahid Gharaee @ 2026-10-20 13:35 UTC (permalink / raw)
To: openembedded-core
From: Vahid Gharaee <v.gharaee@reinhausen.com>
Signed-off-by: Vahid Gharaee <vgharaee@gmail.com>
---
meta/lib/oe/fitimage.py | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/meta/lib/oe/fitimage.py b/meta/lib/oe/fitimage.py
index 484e5c5ee0..91ccc31d46 100644
--- a/meta/lib/oe/fitimage.py
+++ b/meta/lib/oe/fitimage.py
@@ -193,6 +193,7 @@ class ItsNodeRootKernel(ItsNode):
self._dtbs = []
self._dtb_alias = []
self._kernel = None
+ self._kernels = {}
self._ramdisk = None
self._bootscr = None
self._setup = None
@@ -268,8 +269,8 @@ class ItsNodeRootKernel(ItsNode):
def fitimage_emit_section_kernel(self, kernel_id, kernel_path, compression,
load, entrypoint, mkimage_kernel_type, entrysymbol=None):
"""Emit the fitImage ITS kernel section"""
- if self._kernel:
- bb.fatal("Kernel section already exists in the ITS file.")
+ if kernel_id in self._kernels:
+ bb.fatal("Kernel section '%s' already exists in the ITS file." % kernel_id)
if entrysymbol:
result = subprocess.run([self._host_prefix + "nm", "vmlinux"], capture_output=True, text=True)
for line in result.stdout.splitlines():
@@ -294,7 +295,10 @@ class ItsNodeRootKernel(ItsNode):
compression,
opt_props,
)
- self._kernel = kernel_node
+ self._kernels[kernel_id] = kernel_node
+
+ if self._kernel is None:
+ self._kernel = kernel_node
def fitimage_emit_section_dtb(self, dtb_id, dtb_path, dtb_loadaddress=None,
dtbo_loadaddress=None, add_compatible=False):
@@ -425,7 +429,7 @@ class ItsNodeRootKernel(ItsNode):
self._loadables.append(loadable_node)
- def _fitimage_emit_one_section_config(self, conf_node_name, dtb=None):
+ def _fitimage_emit_one_section_config(self, conf_node_name, dtb=None, kernel=None):
"""Emit the fitImage ITS configuration section"""
opt_props = {}
conf_desc = []
@@ -436,9 +440,11 @@ class ItsNodeRootKernel(ItsNode):
if dtb and os.path.splitext(dtb.name)[1] == ".dtbo":
include_kernel = 0
- if self._kernel and include_kernel:
+ selected_kernel = kernel if kernel is not None else self._kernel
+
+ if selected_kernel and include_kernel:
conf_desc.append("Linux kernel")
- opt_props["kernel"] = self._kernel.name
+ opt_props["kernel"] = selected_kernel.name
if self._sign_enable:
sign_entries.append("kernel")
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v2 2/4] oe.fitimage: add metadata for multiple kernels
2026-10-20 13:35 [PATCH v2 0/4] Add support for multiple kernels in FIT images Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection Vahid Gharaee
@ 2026-10-20 13:35 ` Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 3/4] oe.selftest: Add tests for multi-kernel FIT config Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling Vahid Gharaee
3 siblings, 0 replies; 5+ messages in thread
From: Vahid Gharaee @ 2026-10-20 13:35 UTC (permalink / raw)
To: openembedded-core
From: Vahid Gharaee <vgharaee@gmail.com>
Signed-off-by: Vahid Gharaee <vgharaee@gmail.com>
---
meta/conf/image-fitimage.conf | 55 +++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/meta/conf/image-fitimage.conf b/meta/conf/image-fitimage.conf
index a06d30e494..21d54ce18d 100644
--- a/meta/conf/image-fitimage.conf
+++ b/meta/conf/image-fitimage.conf
@@ -102,3 +102,58 @@ FIT_UBOOT_ENV ?= ""
# arch, os) if the corresponding flag is not set, while others (load address
# and entry point) will be omitted in the final FIT.
FIT_LOADABLES ?= ""
+
+# Allow user to insert additional kernel images, e.g. a second kernel built by
+# another recipe or multiconfig, or a prebuilt kernel. The default kernel
+# (kernel-1) from virtual/kernel is always included. Each entry is used as the
+# image node name and must not collide with other image nodes, e.g. kernel-1.
+# Entries can be configured through variable flags:
+# FIT_KERNEL_FILENAME[id] - kernel file, relative to FIT_KERNEL_DEPLOYDIR
+# (default: linux.bin, mandatory for prebuilt
+# kernels)
+# FIT_KERNEL_DEPLOYDIR[id] - directory containing the kernel file
+# (default: ${DEPLOY_DIR_IMAGE})
+# FIT_KERNEL_RECIPE[id] - recipe whose do_deploy provides the kernel
+# (default: none, the file must already exist).
+# Without FIT_KERNEL_MULTICONFIG the kernel file
+# must be deployed by this recipe.
+# FIT_KERNEL_MULTICONFIG[id] - multiconfig building FIT_KERNEL_RECIPE[id]
+# (default recipe: virtual/kernel), requires
+# FIT_KERNEL_DEPLOYDIR[id]
+# FIT_KERNEL_COMPRESSION[id] - default: for linux.bin deployed by
+# FIT_KERNEL_RECIPE or FIT_KERNEL_MULTICONFIG the
+# content of linux_comp next to it, else none
+# FIT_KERNEL_LOADADDRESS[id] - default: ${UBOOT_LOADADDRESS}
+# FIT_KERNEL_ENTRYPOINT[id] - default: ${UBOOT_ENTRYPOINT}
+# FIT_KERNEL_TYPE[id] - default: ${UBOOT_MKIMAGE_KERNEL_TYPE}
+# FIT_KERNEL_DESCRIPTION[id] - default: "Linux kernel"
+# Example, a kernel from the "rt" multiconfig with the same MACHINE:
+# FIT_KERNELS = "kernel-rt"
+# FIT_KERNEL_MULTICONFIG[kernel-rt] = "rt"
+# FIT_KERNEL_DEPLOYDIR[kernel-rt] = "${TOPDIR}/tmp-rt/deploy/images/${MACHINE}"
+# Example, a second kernel recipe with KERNEL_PACKAGE_NAME = "kernel-rt":
+# FIT_KERNELS = "kernel-rt"
+# FIT_KERNEL_RECIPE[kernel-rt] = "linux-yocto-rt"
+# FIT_KERNEL_DEPLOYDIR[kernel-rt] = "${DEPLOY_DIR_IMAGE}/kernel-rt"
+# Additional kernels are only referenced by configurations from FIT_CONFIGS.
+FIT_KERNELS ?= ""
+
+# Allow user to add configurations in addition to the ones generated per DTB
+# (or conf-1 without DTBs). Each entry results in a ${FIT_CONF_PREFIX}<id>
+# configuration node which can be configured through variable flags:
+# FIT_CONFIG_KERNEL[id] - kernel image node, e.g. one of FIT_KERNELS
+# (default: kernel-1)
+# FIT_CONFIG_DTB[id] - DTB name as in KERNEL_DEVICETREE or a file name in
+# EXTERNAL_KERNEL_DEVICETREE (default: none)
+# The ramdisk, u-boot script, setup and loadables are referenced by all
+# configurations.
+# Example:
+# FIT_CONFIGS = "rt"
+# FIT_CONFIG_KERNEL[rt] = "kernel-rt"
+# FIT_CONFIG_DTB[rt] = "am335x-boneblack.dtb"
+FIT_CONFIGS ?= ""
+
+# Allow user to select the default configuration by name without
+# FIT_CONF_PREFIX, e.g. "rt" for "conf-rt". Takes precedence over
+# FIT_CONF_DEFAULT_DTB.
+FIT_CONF_DEFAULT ?= ""
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v2 3/4] oe.selftest: Add tests for multi-kernel FIT config
2026-10-20 13:35 [PATCH v2 0/4] Add support for multiple kernels in FIT images Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 2/4] oe.fitimage: add metadata for multiple kernels Vahid Gharaee
@ 2026-10-20 13:35 ` Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling Vahid Gharaee
3 siblings, 0 replies; 5+ messages in thread
From: Vahid Gharaee @ 2026-10-20 13:35 UTC (permalink / raw)
To: openembedded-core
From: Vahid Gharaee <vgharaee@gmail.com>
Add selftests for FIT images containing multiple kernel nodes
and configurations.
AI-Generated: Uses GitHub copilot (Claude Opus 5.5)
Signed-off-by: Vahid Gharaee <vgharaee@gmail.com>
---
meta/lib/oeqa/selftest/cases/fitimage.py | 156 ++++++++++++++++++++++-
1 file changed, 155 insertions(+), 1 deletion(-)
diff --git a/meta/lib/oeqa/selftest/cases/fitimage.py b/meta/lib/oeqa/selftest/cases/fitimage.py
index 5d77e6da09..cd099a7ce4 100644
--- a/meta/lib/oeqa/selftest/cases/fitimage.py
+++ b/meta/lib/oeqa/selftest/cases/fitimage.py
@@ -461,6 +461,30 @@ class FitImageTestCase(OESelftestTestCase):
loadables += var_loadables.split()
return loadables
+ @staticmethod
+ def _get_flag_values(bb_vars, var, items):
+ """Map items to the per-item values of a synthetic "a ? b" flag variable"""
+ values = (bb_vars.get(var) or "").split("?")
+ return {item: values[index].strip() if index < len(values) else "" for index, item in enumerate(items)}
+
+ @staticmethod
+ def _get_extra_configs(bb_vars):
+ """Return a list of (conf, kernel, dtb image) tuples for FIT_CONFIGS"""
+ configs = (bb_vars.get('FIT_CONFIGS') or "").split()
+ kernels = FitImageTestCase._get_flag_values(bb_vars, 'FIT_CONFIG_KERNEL', configs)
+ dtbs = FitImageTestCase._get_flag_values(bb_vars, 'FIT_CONFIG_DTB', configs)
+ _, dtb_symlinks = FitImageTestCase._get_dtb_files(bb_vars)
+ extra_configs = []
+ for conf in configs:
+ dtb = dtbs[conf]
+ if dtb in dtb_symlinks:
+ # Symlinks of test-dtbs-as-ext point to the DTB without "-alias"
+ dtb = dtb.replace("-alias", "")
+ elif dtb:
+ dtb = FitImageTestCase._apply_dtbvendored(bb_vars, dtb)
+ extra_configs.append((conf, kernels[conf] or "kernel-1", dtb))
+ return extra_configs
+
def _is_req_dict_in_dict(self, found_dict, req_dict):
"""
Check if all key-value pairs in the required dictionary are present in the found dictionary.
@@ -670,11 +694,16 @@ class KernelFitImageBase(FitImageTestCase):
internal_used = {
'DEPLOY_DIR_IMAGE',
'FIT_CONF_DEFAULT_DTB',
+ 'FIT_CONF_DEFAULT',
'FIT_CONF_MAPPINGS',
'FIT_CONF_PREFIX',
+ 'FIT_CONFIGS',
+ 'FIT_CONFIG_DTB',
+ 'FIT_CONFIG_KERNEL',
'FIT_DESC',
'FIT_HASH_ALG',
'FIT_KERNEL_COMP_ALG',
+ 'FIT_KERNELS',
'FIT_LOADABLES',
'FIT_LOADABLE_ENTRYPOINT',
'FIT_LOADABLE_LOADADDRESS',
@@ -835,6 +864,8 @@ class KernelFitImageBase(FitImageTestCase):
if loadables:
images += loadables
+ images += (bb_vars.get('FIT_KERNELS') or "").split()
+
# configuration nodes (one per DTB, symlink, and mappings)
configurations = []
if dtb_files:
@@ -884,6 +915,8 @@ class KernelFitImageBase(FitImageTestCase):
else:
configurations = [bb_vars['FIT_CONF_PREFIX'] + '1']
+ configurations += [bb_vars['FIT_CONF_PREFIX'] + conf for conf, _, _ in FitImageTestCase._get_extra_configs(bb_vars)]
+
# Create a list of paths for all image and configuration nodes
req_its_paths = []
for image in images:
@@ -926,8 +959,12 @@ class KernelFitImageBase(FitImageTestCase):
if uboot_rd_entrypoint:
its_field_check.append("entry = <%s>;" % uboot_rd_entrypoint)
+ fit_conf_default = bb_vars.get('FIT_CONF_DEFAULT')
fit_conf_default_dtb = bb_vars.get('FIT_CONF_DEFAULT_DTB')
- if fit_conf_default_dtb:
+ if fit_conf_default:
+ fit_conf_prefix = bb_vars.get('FIT_CONF_PREFIX', "conf-")
+ its_field_check.append('default = "' + fit_conf_prefix + fit_conf_default + '";')
+ elif fit_conf_default_dtb:
fit_conf_prefix = bb_vars.get('FIT_CONF_PREFIX', "conf-")
its_field_check.append('default = "' + fit_conf_prefix + self._apply_dtbvendored(bb_vars, fit_conf_default_dtb) + '";')
@@ -951,6 +988,12 @@ class KernelFitImageBase(FitImageTestCase):
if initramfs_image and initramfs_image_bundle != "1":
its_field_check.append('ramdisk = "ramdisk-1";')
+ # Additional configurations are emitted after the generated ones
+ for _, kernel, dtb in FitImageTestCase._get_extra_configs(bb_vars):
+ its_field_check.append('kernel = "%s";' % kernel)
+ if dtb:
+ its_field_check.append('fdt = "fdt-%s";' % dtb)
+
return its_field_check
def _get_req_sigvalues_config(self, bb_vars):
@@ -1062,6 +1105,15 @@ class KernelFitImageBase(FitImageTestCase):
if loadables:
req_sections[conf_name]['Loadables'] = ",".join(loadables)
+ # Additional kernels and configurations
+ for kernel in (bb_vars.get('FIT_KERNELS') or "").split():
+ req_sections[kernel] = { "Type": "Kernel Image" }
+ for conf, kernel, dtb in FitImageTestCase._get_extra_configs(bb_vars):
+ conf_name = bb_vars['FIT_CONF_PREFIX'] + conf
+ req_sections[conf_name] = { "Kernel": kernel }
+ if dtb:
+ req_sections[conf_name]['FDT'] = 'fdt-' + dtb
+
# Add signing related properties if needed
if uboot_sign_enable == "1":
for section in req_sections:
@@ -1208,6 +1260,32 @@ FIT_LOADABLE_TYPE[loadable2] = "firmware"
self._gen_atf_tee_dummy_images(bb_vars)
self._test_fitimage(bb_vars)
+ def test_fit_image_multiple_kernels(self):
+ """
+ Summary: Check FIT_KERNELS, FIT_CONFIGS and FIT_CONF_DEFAULT
+ Expected: An additional kernel image node and a configuration
+ referring to it are present and the configuration is
+ the default one.
+ Product: oe-core
+ """
+ machine = self._get_machine_or_skip()
+ config = """
+KERNEL_IMAGETYPE = "Image"
+UBOOT_LOADADDRESS = "0x80080000"
+UBOOT_ENTRYPOINT = "0x80080000"
+# Use the linux.bin of virtual/kernel also as second kernel to avoid building another kernel
+FIT_KERNELS = "kernel-2"
+FIT_KERNEL_RECIPE[kernel-2] = "virtual/kernel"
+FIT_CONFIGS = "second"
+FIT_CONFIG_KERNEL[second] = "kernel-2"
+FIT_CONF_DEFAULT = "second"
+"""
+ config = FitImageTestCase._config_add_machine_settings(config, machine, keys=["KERNEL_DEVICETREE"])
+ config = self._config_add_kernel_classes(config)
+ self.write_config(config)
+ bb_vars = self._fit_get_bb_vars()
+ self._test_fitimage(bb_vars)
+
def test_get_compatible_from_dtb(self):
"""Test the oe.fitimage.get_compatible_from_dtb function
@@ -1501,11 +1579,14 @@ class FitImagePyTests(KernelFitImageBase):
# image-fitimage.conf
'FIT_ADDRESS_CELLS': "1",
'FIT_CONF_DEFAULT_DTB': "",
+ 'FIT_CONF_DEFAULT': "",
'FIT_CONF_MAPPINGS': "",
'FIT_CONF_PREFIX': "conf-",
+ 'FIT_CONFIGS': "",
'FIT_DESC': "Kernel fitImage for a dummy distro",
'FIT_GENERATE_KEYS': "0",
'FIT_HASH_ALG': "sha256",
+ 'FIT_KERNELS': "",
'FIT_KEY_GENRSA_ARGS': "-F4",
'FIT_KEY_REQ_ARGS': "-batch -new",
'FIT_KEY_SIGN_PKCS': "-x509",
@@ -1566,6 +1647,11 @@ class FitImagePyTests(KernelFitImageBase):
bb_vars.get('UBOOT_MKIMAGE_KERNEL_TYPE'), bb_vars.get("UBOOT_ENTRYSYMBOL")
)
+ for kernel in bb_vars['FIT_KERNELS'].split():
+ root_node.fitimage_emit_section_kernel(kernel, "linux-%s.bin" % kernel, "none",
+ bb_vars.get('UBOOT_LOADADDRESS'), bb_vars.get('UBOOT_ENTRYPOINT'),
+ bb_vars.get('UBOOT_MKIMAGE_KERNEL_TYPE'))
+
dtb_files, dtb_symlinks = FitImageTestCase._get_dtb_files(bb_vars)
for dtb in dtb_files:
root_node.fitimage_emit_section_dtb(dtb, os.path.join("a-dir", dtb),
@@ -1596,9 +1682,19 @@ class FitImagePyTests(KernelFitImageBase):
"loadable-type")
root_node.fitimage_emit_section_config(bb_vars['FIT_CONF_DEFAULT_DTB'], bb_vars.get('FIT_CONF_MAPPINGS'))
+ configs = bb_vars['FIT_CONFIGS'].split()
+ conf_kernels = FitImageTestCase._get_flag_values(bb_vars, 'FIT_CONFIG_KERNEL', configs)
+ conf_dtbs = FitImageTestCase._get_flag_values(bb_vars, 'FIT_CONFIG_DTB', configs)
+ for conf in configs:
+ root_node.fitimage_emit_section_config_custom(conf, conf_kernels[conf], conf_dtbs[conf])
+ if bb_vars['FIT_CONF_DEFAULT']:
+ root_node.fitimage_set_default_config(bb_vars['FIT_CONF_DEFAULT'])
root_node.write_its_file(fitimage_its_path)
self.assertExists(fitimage_its_path, "%s image tree source doesn't exist" % (fitimage_its_path))
+ with open(fitimage_its_path) as its_file:
+ default_lines = [line for line in its_file if line.strip().startswith("default = ")]
+ self.assertEqual(len(default_lines), 1, "Expected exactly one default configuration: %s" % default_lines)
self.logger.debug("Checking its: %s" % fitimage_its_path)
self._check_its_file(bb_vars, fitimage_its_path)
@@ -1665,6 +1761,64 @@ class FitImagePyTests(KernelFitImageBase):
}
self._test_fitimage_py(bb_vars_overrides)
+ def test_fitimage_py_multiple_kernels(self):
+ """Test FIT_KERNELS, FIT_CONFIGS and FIT_CONF_DEFAULT"""
+ bb_vars_overrides = {
+ 'KERNEL_DEVICETREE': "one.dtb two.dtb",
+ 'FIT_CONF_DEFAULT_DTB': "one.dtb",
+ 'FIT_KERNELS': "kernel-rt",
+ 'FIT_CONFIGS': "rt rt-two default-two",
+ 'FIT_CONFIG_KERNEL': "kernel-rt ? kernel-rt ? ",
+ 'FIT_CONFIG_DTB': " ? two.dtb ? two.dtb",
+ 'FIT_CONF_DEFAULT': "rt-two",
+ }
+ self._test_fitimage_py(bb_vars_overrides)
+
+ def test_fitimage_py_multiple_kernels_unknown_kernel_error(self):
+ """Test that FIT_CONFIG_KERNEL referring to an unknown kernel causes a fatal error"""
+ bb_vars_overrides = {
+ 'FIT_CONFIGS': "rt",
+ 'FIT_CONFIG_KERNEL': "kernel-rt",
+ }
+ with self.assertRaises(BBHandledException):
+ self._test_fitimage_py(bb_vars_overrides)
+
+ def test_fitimage_py_conf_default_unknown_error(self):
+ """Test that FIT_CONF_DEFAULT referring to an unknown configuration causes a fatal error"""
+ bb_vars_overrides = {
+ 'FIT_CONF_DEFAULT': "nonexistent",
+ }
+ with self.assertRaises(BBHandledException):
+ self._test_fitimage_py(bb_vars_overrides)
+
+ def test_fitimage_py_multiple_kernels_dtb_alias(self):
+ """Test FIT_CONFIG_DTB referring to an external DTB symlink"""
+ bb_vars_overrides = {
+ 'PREFERRED_PROVIDER_virtual/dtb': "test-dtbs-as-ext",
+ 'FIT_KERNELS': "kernel-rt",
+ 'FIT_CONFIGS': "rt",
+ 'FIT_CONFIG_KERNEL': "kernel-rt",
+ 'FIT_CONFIG_DTB': "test-ext-alias.dtb",
+ }
+ self._test_fitimage_py(bb_vars_overrides)
+
+ def test_fitimage_py_multiple_kernels_kernel_1_error(self):
+ """Test that a FIT_KERNELS entry colliding with kernel-1 causes a fatal error"""
+ bb_vars_overrides = {
+ 'FIT_KERNELS': "kernel-1",
+ }
+ with self.assertRaises(BBHandledException):
+ self._test_fitimage_py(bb_vars_overrides)
+
+ def test_fitimage_py_multiple_kernels_image_collision_error(self):
+ """Test that a FIT_KERNELS entry colliding with another image node causes a fatal error"""
+ bb_vars_overrides = {
+ 'FIT_KERNELS': "my-loadable",
+ 'FIT_LOADABLES': "my-loadable",
+ }
+ with self.assertRaises(BBHandledException):
+ self._test_fitimage_py(bb_vars_overrides)
+
class UBootFitImageTests(FitImageTestCase):
"""Test cases for the uboot-sign bbclass"""
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling
2026-10-20 13:35 [PATCH v2 0/4] Add support for multiple kernels in FIT images Vahid Gharaee
` (2 preceding siblings ...)
2026-10-20 13:35 ` [PATCH v2 3/4] oe.selftest: Add tests for multi-kernel FIT config Vahid Gharaee
@ 2026-10-20 13:35 ` Vahid Gharaee
3 siblings, 0 replies; 5+ messages in thread
From: Vahid Gharaee @ 2026-10-20 13:35 UTC (permalink / raw)
To: openembedded-core
From: Vahid Gharaee <vgharaee@gmail.com>
One can now add configs that point to any kernel node and,
optionally, a DTB with fitimage_emit_section_config_custom(). The default
config still can be chosen by name with fitimage_set_default_config().
The default configuration is now stored and written to the ITS once, in
write_its_file(). Before, fitimage_set_default_config() had to overwrite
the property that fitimage_emit_section_config() had already set.
DTBs and DTB symlink aliases are now looked up by the id they were added
with. Before, the lookup depended on the "fdt-" node name or the alias
name.
Tested with oe-selftest -r fitimage.FitImagePyTests.
Signed-off-by: Vahid Gharaee <vgharaee@gmail.com>
---
meta/classes-recipe/kernel-fit-image.bbclass | 129 +++++++++++++++++--
meta/lib/oe/fitimage.py | 66 +++++++++-
2 files changed, 175 insertions(+), 20 deletions(-)
diff --git a/meta/classes-recipe/kernel-fit-image.bbclass b/meta/classes-recipe/kernel-fit-image.bbclass
index 7b2bcd4673..4abdab0a4a 100644
--- a/meta/classes-recipe/kernel-fit-image.bbclass
+++ b/meta/classes-recipe/kernel-fit-image.bbclass
@@ -29,20 +29,52 @@ python () {
d.appendVarFlag('do_compile', 'depends', ' virtual/dtb:do_populate_sysroot')
d.setVar('EXTERNAL_KERNEL_DEVICETREE', "${RECIPE_SYSROOT}/boot/devicetree")
- # Parse loadables config
- loadables = d.getVar('FIT_LOADABLES')
- if loadables:
- # Check that the loadables configuration variables are not set directly
- # directly, then assign them a synthetic value for testability purposes
- for v in ['FIT_LOADABLE_ARCH', 'FIT_LOADABLE_COMPRESSION',
- 'FIT_LOADABLE_DESCRIPTION', 'FIT_LOADABLE_ENTRYPOINT',
- 'FIT_LOADABLE_FILENAME', 'FIT_LOADABLE_LOADADDRESS',
- 'FIT_LOADABLE_OS', 'FIT_LOADABLE_TYPE']:
- if d.getVar(v):
- raise bb.parse.SkipRecipe("You cannot use %s as a variable, you can only set flags." % v)
-
- synt_value = " ? ".join([ d.getVarFlag(v, loadable) or "" for loadable in loadables.split() ])
- d.setVar(v, synt_value)
+ # Parse loadables, additional kernels and additional configurations
+ for items_var, flag_vars in (
+ ('FIT_LOADABLES', ['FIT_LOADABLE_ARCH', 'FIT_LOADABLE_COMPRESSION',
+ 'FIT_LOADABLE_DESCRIPTION', 'FIT_LOADABLE_ENTRYPOINT',
+ 'FIT_LOADABLE_FILENAME', 'FIT_LOADABLE_LOADADDRESS',
+ 'FIT_LOADABLE_OS', 'FIT_LOADABLE_TYPE']),
+ ('FIT_KERNELS', ['FIT_KERNEL_COMPRESSION', 'FIT_KERNEL_DEPLOYDIR',
+ 'FIT_KERNEL_DESCRIPTION', 'FIT_KERNEL_ENTRYPOINT',
+ 'FIT_KERNEL_FILENAME', 'FIT_KERNEL_LOADADDRESS',
+ 'FIT_KERNEL_MULTICONFIG', 'FIT_KERNEL_RECIPE',
+ 'FIT_KERNEL_TYPE']),
+ ('FIT_CONFIGS', ['FIT_CONFIG_DTB', 'FIT_CONFIG_KERNEL'])):
+ items = d.getVar(items_var)
+ if items:
+ # Check that the configuration variables are not set directly,
+ # then assign them a synthetic value for testability purposes
+ for v in flag_vars:
+ if d.getVar(v):
+ raise bb.parse.SkipRecipe("You cannot use %s as a variable, you can only set flags." % v)
+
+ synt_value = " ? ".join([ d.getVarFlag(v, item, False) or "" for item in items.split() ])
+ d.setVar(v, synt_value)
+
+ # Additional kernels may be deployed by another recipe, by a recipe of
+ # another multiconfig or be provided as prebuilt files
+ kernels = (d.getVar('FIT_KERNELS') or "").split()
+ if "kernel-1" in kernels:
+ bb.fatal("FIT_KERNELS must not contain kernel-1, it is reserved for virtual/kernel")
+ duplicates = sorted(set(k for k in kernels if kernels.count(k) > 1))
+ if duplicates:
+ bb.fatal("FIT_KERNELS contains duplicate entries: %s" % ' '.join(duplicates))
+ for kernel in kernels:
+ recipe = d.getVarFlag('FIT_KERNEL_RECIPE', kernel)
+ mc = d.getVarFlag('FIT_KERNEL_MULTICONFIG', kernel)
+ if mc:
+ if not d.getVarFlag('FIT_KERNEL_DEPLOYDIR', kernel):
+ bb.fatal("FIT_KERNEL_DEPLOYDIR[%s] must be set if FIT_KERNEL_MULTICONFIG[%s] is set" % (kernel, kernel))
+ d.appendVarFlag('do_compile', 'mcdepends', ' mc:%s:%s:%s:do_deploy' %
+ (d.getVar('BB_CURRENT_MC'), mc, recipe or 'virtual/kernel'))
+ elif recipe:
+ d.appendVarFlag('do_compile', 'depends', ' %s:do_deploy' % recipe)
+ else:
+ if not d.getVarFlag('FIT_KERNEL_FILENAME', kernel):
+ bb.fatal("FIT_KERNEL_FILENAME[%s] must be set if neither FIT_KERNEL_RECIPE[%s] nor FIT_KERNEL_MULTICONFIG[%s] is set" % (kernel, kernel, kernel))
+ kernel_path = fit_kernel_path(d, kernel)
+ d.appendVarFlag('do_compile', 'file-checksums', ' %s:%s' % (kernel_path, os.path.exists(kernel_path)))
}
do_configure[noexec] = "1"
@@ -62,6 +94,30 @@ def apply_dtbvendored(d,dtb):
mapped_name = dtb.replace('/', '_')
return mapped_name
+def fit_kernel_path(d, kernel):
+ filename = d.getVarFlag('FIT_KERNEL_FILENAME', kernel) or "linux.bin"
+ deploydir = d.getVarFlag('FIT_KERNEL_DEPLOYDIR', kernel) or d.getVar('DEPLOY_DIR_IMAGE')
+ return os.path.join(deploydir, filename)
+
+def fit_kernel_deployed_files(d, recipe):
+ """Return the real paths of all files deployed by do_deploy of recipe (PN or PROVIDES) in this multiconfig"""
+ import bb.runqueue
+ mc = d.getVar('BB_CURRENT_MC') or ""
+ taskdepdata = d.getVar('BB_TASKDEPDATA', False)
+ pns = set(dep.pn for tid, dep in taskdepdata.items()
+ if dep.taskname == "do_deploy" and bb.runqueue.mc_from_tid(tid) == mc and
+ (dep.pn == recipe or recipe in dep.provides))
+ if len(pns) != 1:
+ bb.fatal("Could not identify the do_deploy dependency providing %s (found: %s)" % (recipe, ' '.join(sorted(pns)) or "none"))
+ pn = pns.pop()
+ for arch in d.getVar('SSTATE_ARCHS').split():
+ manifest = os.path.join(d.getVar('SSTATE_MANIFESTS'), "manifest-%s-%s.deploy" % (arch, pn))
+ if os.path.exists(manifest):
+ with open(manifest) as f:
+ return set(os.path.realpath(line) for line in f.read().splitlines() if line and not line.endswith('/'))
+ bb.fatal("No deploy manifest found for %s" % pn)
+fit_kernel_deployed_files[vardepsexclude] = "BB_TASKDEPDATA SSTATE_ARCHS"
+
python do_compile() {
import shutil
import oe.fitimage
@@ -95,6 +151,39 @@ python do_compile() {
d.getVar('UBOOT_LOADADDRESS'), d.getVar('UBOOT_ENTRYPOINT'),
d.getVar('UBOOT_MKIMAGE_KERNEL_TYPE'), d.getVar("UBOOT_ENTRYSYMBOL"))
+ # Prepare additional kernel image sections
+ for kernel in d.getVar('FIT_KERNELS').split():
+ recipe = d.getVarFlag('FIT_KERNEL_RECIPE', kernel)
+ mc = d.getVarFlag('FIT_KERNEL_MULTICONFIG', kernel)
+ kernel_path = fit_kernel_path(d, kernel)
+ if not os.path.exists(kernel_path):
+ bb.fatal("File for kernel %s not found at %s" % (kernel, kernel_path))
+ # Artifacts of another multiconfig cannot be verified, its FIT_KERNEL_DEPLOYDIR is trusted
+ deployed = None
+ if recipe and not mc:
+ deployed = fit_kernel_deployed_files(d, recipe)
+ if os.path.realpath(kernel_path) not in deployed:
+ bb.fatal("File for kernel %s at %s is not deployed by %s" % (kernel, kernel_path, recipe))
+ kernel_file = "linux-%s.bin" % kernel
+ shutil.copyfile(kernel_path, kernel_file)
+
+ kernel_comp = d.getVarFlag('FIT_KERNEL_COMPRESSION', kernel)
+ if not kernel_comp:
+ kernel_comp = "none"
+ # linux_comp only describes linux.bin written by kernel-fit-extra-artifacts of the same recipe
+ if (recipe or mc) and os.path.basename(kernel_path) == "linux.bin":
+ kernel_comp_path = os.path.join(os.path.dirname(kernel_path), "linux_comp")
+ if not os.path.exists(kernel_comp_path) or \
+ (deployed is not None and os.path.realpath(kernel_comp_path) not in deployed):
+ bb.fatal("%s not found or not deployed by the kernel recipe, set FIT_KERNEL_COMPRESSION[%s]" % (kernel_comp_path, kernel))
+ with open(kernel_comp_path) as kernel_comp_f:
+ kernel_comp = kernel_comp_f.read().strip()
+ root_node.fitimage_emit_section_kernel(kernel, kernel_file, kernel_comp,
+ d.getVarFlag('FIT_KERNEL_LOADADDRESS', kernel) or d.getVar('UBOOT_LOADADDRESS'),
+ d.getVarFlag('FIT_KERNEL_ENTRYPOINT', kernel) or d.getVar('UBOOT_ENTRYPOINT'),
+ d.getVarFlag('FIT_KERNEL_TYPE', kernel) or d.getVar('UBOOT_MKIMAGE_KERNEL_TYPE'),
+ description=d.getVarFlag('FIT_KERNEL_DESCRIPTION', kernel) or "Linux kernel")
+
# Prepare a DTB image section
kernel_devicetree = d.getVar('KERNEL_DEVICETREE')
external_kernel_devicetree = d.getVar("EXTERNAL_KERNEL_DEVICETREE")
@@ -210,6 +299,18 @@ python do_compile() {
# Generate the configuration section
root_node.fitimage_emit_section_config(default_dtb, d.getVar("FIT_CONF_MAPPINGS"))
+ # Generate additional configurations, e.g. referring to additional kernels
+ for conf in d.getVar('FIT_CONFIGS').split():
+ conf_dtb = d.getVarFlag('FIT_CONFIG_DTB', conf)
+ if conf_dtb:
+ conf_dtb = apply_dtbvendored(d, conf_dtb)
+ root_node.fitimage_emit_section_config_custom(conf,
+ d.getVarFlag('FIT_CONFIG_KERNEL', conf), conf_dtb)
+
+ fit_conf_default = d.getVar('FIT_CONF_DEFAULT')
+ if fit_conf_default:
+ root_node.fitimage_set_default_config(fit_conf_default)
+
# Write the its file
root_node.write_its_file(itsfile)
diff --git a/meta/lib/oe/fitimage.py b/meta/lib/oe/fitimage.py
index 91ccc31d46..c062413f52 100644
--- a/meta/lib/oe/fitimage.py
+++ b/meta/lib/oe/fitimage.py
@@ -147,8 +147,11 @@ class ItsNodeConfiguration(ItsNode):
class ItsNodeRootKernel(ItsNode):
"""Create FIT images for the kernel
- Currently only a single kernel (no less or more) can be added to the FIT
- image along with 0 or more device trees and 0 or 1 ramdisk.
+ One or more kernels can be added to the FIT image along with 0 or more
+ device trees and 0 or 1 ramdisk. The first kernel added is the default
+ kernel referenced by the automatically generated configurations.
+ Additional configurations referring to other kernels can be added with
+ fitimage_emit_section_config_custom().
If a device tree included in the FIT image, the default configuration is the
firt DTB. If there is no dtb present than the default configuation the kernel.
@@ -192,6 +195,9 @@ class ItsNodeRootKernel(ItsNode):
self._dtbs = []
self._dtb_alias = []
+ # DTB ids as passed to fitimage_emit_section_dtb(_alias) mapped to their nodes
+ self._dtbs_by_id = {}
+ self._default_conf = None
self._kernel = None
self._kernels = {}
self._ramdisk = None
@@ -213,11 +219,18 @@ class ItsNodeRootKernel(ItsNode):
bb.fatal("FIT image signing is enabled for individual images but no image key name is provided.")
def write_its_file(self, itsfile):
+ if self._default_conf:
+ self.configurations.add_property('default', self._default_conf)
with open(itsfile, 'w') as f:
f.write("/dts-v1/;\n\n")
self.emit(f, 0)
+ def _check_image_id_unique(self, image_id):
+ if any(node.name == image_id for node in self.images.sub_nodes):
+ bb.fatal("Image node '%s' already exists in the ITS file." % image_id)
+
def its_add_node_image(self, image_id, description, image_type, compression, opt_props):
+ self._check_image_id_unique(image_id)
image_node = ItsNodeImage(
image_id,
self.images,
@@ -242,6 +255,7 @@ class ItsNodeRootKernel(ItsNode):
return image_node
def its_add_node_dtb(self, image_id, description, image_type, compression, opt_props, compatible):
+ self._check_image_id_unique(image_id)
dtb_node = ItsNodeDtb(
image_id,
self.images,
@@ -267,7 +281,7 @@ class ItsNodeRootKernel(ItsNode):
return dtb_node
def fitimage_emit_section_kernel(self, kernel_id, kernel_path, compression,
- load, entrypoint, mkimage_kernel_type, entrysymbol=None):
+ load, entrypoint, mkimage_kernel_type, entrysymbol=None, description="Linux kernel"):
"""Emit the fitImage ITS kernel section"""
if kernel_id in self._kernels:
bb.fatal("Kernel section '%s' already exists in the ITS file." % kernel_id)
@@ -290,7 +304,7 @@ class ItsNodeRootKernel(ItsNode):
kernel_node = self.its_add_node_image(
kernel_id,
- "Linux kernel",
+ description,
mkimage_kernel_type,
compression,
opt_props,
@@ -303,6 +317,8 @@ class ItsNodeRootKernel(ItsNode):
def fitimage_emit_section_dtb(self, dtb_id, dtb_path, dtb_loadaddress=None,
dtbo_loadaddress=None, add_compatible=False):
"""Emit the fitImage ITS DTB section"""
+ if dtb_id in self._dtbs_by_id:
+ bb.fatal("DTB '%s' already exists in the ITS file." % dtb_id)
load=None
dtb_ext = os.path.splitext(dtb_path)[1]
if dtb_ext == ".dtbo":
@@ -332,9 +348,12 @@ class ItsNodeRootKernel(ItsNode):
compatible
)
self._dtbs.append(dtb_node)
+ self._dtbs_by_id[dtb_id] = dtb_node
def fitimage_emit_section_dtb_alias(self, dtb_alias_id, dtb_path, add_compatible=False):
"""Add a configuration node referring to another DTB"""
+ if dtb_alias_id in self._dtbs_by_id:
+ bb.fatal("DTB '%s' already exists in the ITS file." % dtb_alias_id)
# Preserve the DTB's compatible string to be added to the configuration node
compatible = None
if add_compatible:
@@ -343,6 +362,7 @@ class ItsNodeRootKernel(ItsNode):
dtb_id = os.path.basename(dtb_path)
dtb_alias_node = ItsNodeDtbAlias("fdt-" + dtb_id, dtb_alias_id, compatible)
self._dtb_alias.append(dtb_alias_node)
+ self._dtbs_by_id[dtb_alias_id] = dtb_alias_node
def fitimage_emit_section_boot_script(self, bootscr_id, bootscr_path):
"""Emit the fitImage ITS u-boot script section"""
@@ -567,13 +587,47 @@ class ItsNodeRootKernel(ItsNode):
)
bb.fatal("FIT configuration mapping(s) not matched with any DTB: %s" % ', '.join(unused_conf))
else:
- # Currently exactly one kernel is supported.
+ # Without DTBs, emit one configuration for the default kernel
self._fitimage_emit_one_section_config(self._conf_prefix + "1")
default_conf = self.configurations.sub_nodes[0].name
if default_dtb_image and self._dtbs:
default_conf = self._conf_prefix + default_dtb_image
- self.configurations.add_property('default', default_conf)
+ self._default_conf = default_conf
+
+ def fitimage_emit_section_config_custom(self, conf_id, kernel_id=None, dtb_id=None):
+ """Emit an additional configuration node for a specific kernel and optional DTB
+
+ kernel_id is the name of a kernel image node, the default kernel is used if not set.
+ dtb_id is the id passed to fitimage_emit_section_dtb or fitimage_emit_section_dtb_alias.
+ """
+ conf_node_name = self._conf_prefix + conf_id
+ if any(conf.name == conf_node_name for conf in self.configurations.sub_nodes):
+ bb.fatal("FIT configuration '%s' already exists in the ITS file." % conf_node_name)
+
+ kernel = None
+ if kernel_id:
+ kernel = self._kernels.get(kernel_id)
+ if not kernel:
+ bb.fatal("FIT configuration '%s' refers to unknown kernel '%s'. Available kernels: %s" %
+ (conf_node_name, kernel_id, ' '.join(self._kernels)))
+
+ dtb = None
+ if dtb_id:
+ dtb = self._dtbs_by_id.get(dtb_id)
+ if not dtb:
+ bb.fatal("FIT configuration '%s' refers to unknown DTB '%s'. Available DTBs: %s" %
+ (conf_node_name, dtb_id, ' '.join(self._dtbs_by_id)))
+
+ self._fitimage_emit_one_section_config(conf_node_name, dtb, kernel)
+
+ def fitimage_set_default_config(self, conf_id):
+ """Select the default configuration, overriding the one chosen by fitimage_emit_section_config"""
+ conf_node_name = self._conf_prefix + conf_id
+ if not any(conf.name == conf_node_name for conf in self.configurations.sub_nodes):
+ bb.fatal("FIT default configuration '%s' does not exist. Available configurations: %s" %
+ (conf_node_name, ' '.join(conf.name for conf in self.configurations.sub_nodes)))
+ self._default_conf = conf_node_name
def run_mkimage_assemble(self, itsfile, fitfile):
cmd = [
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-20 13:35 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-20 13:35 [PATCH v2 0/4] Add support for multiple kernels in FIT images Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 2/4] oe.fitimage: add metadata for multiple kernels Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 3/4] oe.selftest: Add tests for multi-kernel FIT config Vahid Gharaee
2026-10-20 13:35 ` [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling Vahid Gharaee
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox