* [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; 8+ 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] 8+ 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-24 13:41 ` [OE-core] " adrian.freihofer
2026-10-20 13:35 ` [PATCH v2 2/4] oe.fitimage: add metadata for multiple kernels Vahid Gharaee
` (2 subsequent siblings)
3 siblings, 1 reply; 8+ 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] 8+ 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; 8+ 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] 8+ 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-24 13:42 ` [OE-core] " adrian.freihofer
2026-10-20 13:35 ` [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling Vahid Gharaee
3 siblings, 1 reply; 8+ 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] 8+ 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
2026-10-24 13:42 ` [OE-core] " adrian.freihofer
3 siblings, 1 reply; 8+ 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] 8+ messages in thread
* Re: [OE-core] [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection
2026-10-20 13:35 ` [PATCH v2 1/4] oe.fitimage: Support multiple kernel nodes and configuration selection Vahid Gharaee
@ 2026-10-24 13:41 ` adrian.freihofer
0 siblings, 0 replies; 8+ messages in thread
From: adrian.freihofer @ 2026-10-24 13:41 UTC (permalink / raw)
To: vgharaee, openembedded-core
Hi Vahid,
Thanks for working on this, being able to put several kernels into one
FIT image is a useful feature. I had a closer look at the series and
have a few comments.
On Tue, 2026-10-20 at 15:35 +0200, Vahid Gharaee via
lists.openembedded.org wrote:
> From: Vahid Gharaee <v.gharaee@reinhausen.com>
>
> Signed-off-by: Vahid Gharaee <vgharaee@gmail.com>
The author address does not match the Signed-off-by. Please use the
same identity for both.
The commit message has no body. Please explain why the change is
needed. Nothing in this patch uses the new kernel parameter yet, so it
would also help to say that this prepares for the following patches.
For the shortlog, changes to meta/lib/oe/fitimage.py usually use the
"kernel-fit-image:" prefix, e.g.:
kernel-fit-image: support multiple kernel nodes
> ---
> 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 = {}
Do we need both _kernel and _kernels? Dicts keep insertion order, so
the default kernel is always the first entry of _kernels. I'd drop
_kernel and derive the default, e.g.:
@property
def _default_kernel(self):
return next(iter(self._kernels.values()), None)
That avoids keeping two attributes in sync.
> 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:
For a .dtbo configuration, a kernel passed in explicitly is silently
dropped. That's fine for the generated per-DTB configurations. But with
4/4, FIT_CONFIG_KERNEL[x] is ignored without notice if
FIT_CONFIG_DTB[x]
is an overlay. I'd suggest failing in that case, either here or in
fitimage_emit_section_config_custom().
Regards,
Adrian
> 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")
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#248288):
> https://lists.openembedded.org/g/openembedded-core/message/248288
> Mute This Topic: https://lists.openembedded.org/mt/121849065/4454582
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe:
> https://lists.openembedded.org/g/openembedded-core/unsub [
> adrian.freihofer@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [OE-core] [PATCH v2 3/4] oe.selftest: Add tests for multi-kernel FIT config
2026-10-20 13:35 ` [PATCH v2 3/4] oe.selftest: Add tests for multi-kernel FIT config Vahid Gharaee
@ 2026-10-24 13:42 ` adrian.freihofer
0 siblings, 0 replies; 8+ messages in thread
From: adrian.freihofer @ 2026-10-24 13:42 UTC (permalink / raw)
To: vgharaee, openembedded-core
Hi Vahid,
For the shortlog, the usual prefix for this file is
"oe-selftest: fitimage:".
On Tue, 2026-10-20 at 15:35 +0200, Vahid Gharaee via
lists.openembedded.org wrote:
> 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", "")
This only works because of how test-dtbs-as-ext names its files. It
would be more robust to resolve the symlink target, or to keep an
explicit mapping next to _get_dtb_files().
About coverage:
- The FitImagePyTests cases re-implement the do_compile logic in the
test, so they can't catch issues in the class itself, e.g. the
FIT_CONFIG_DTB lookup.
- The only build test (test_fit_image_multiple_kernels) uses
FIT_KERNEL_RECIPE = "virtual/kernel". Prebuilt kernels
(FIT_KERNEL_FILENAME without a recipe), FIT_CONFIG_DTB, and signing
(UBOOT_SIGN_ENABLE / FIT_SIGN_INDIVIDUAL) together with a second
kernel are not built in any test. Could you add at least
FIT_CONFIG_DTB
and signing to the existing build test?
Regards,
Adrian
> + 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)
fitimage_emit_section_config_custom(), fitimage_set_default_config()
and the description= argument of fitimage_emit_section_kernel() are
only added in 4/4. So the new tests fail with this patch applied, and
the series is not bisectable. Please move the tests after the patches
that implement the functionality.
>
> 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"""
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#248290):
> https://lists.openembedded.org/g/openembedded-core/message/248290
> Mute This Topic: https://lists.openembedded.org/mt/121849067/4454582
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe:
> https://lists.openembedded.org/g/openembedded-core/unsub [
> adrian.freihofer@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [OE-core] [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling
2026-10-20 13:35 ` [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling Vahid Gharaee
@ 2026-10-24 13:42 ` adrian.freihofer
0 siblings, 0 replies; 8+ messages in thread
From: adrian.freihofer @ 2026-10-24 13:42 UTC (permalink / raw)
To: vgharaee, openembedded-core
On Tue, 2026-10-20 at 15:35 +0200, Vahid Gharaee via
lists.openembedded.org wrote:
> 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.
This seems to describe the changes compared to an earlier revision of
the series, not to master: fitimage_set_default_config() does not
exist in master, and neither does the "fdt-" name based lookup
mentioned below. On the other hand, the message does not mention the
biggest part of the patch, the FIT_KERNELS / FIT_CONFIGS support in
kernel-fit-image.bbclass.
It might be easier to review with this split:
1. kernel-fit-image: multiple kernels, custom configurations, default
configuration and unique node ids in oe/fitimage.py
2. kernel-fit-image: support FIT_KERNELS, FIT_CONFIGS and
FIT_CONF_DEFAULT (class and image-fitimage.conf together)
3. oe-selftest: fitimage: tests for multiple kernels
>
> 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)
do_compile only reads these settings as flags with non-literal flag
names, e.g. d.getVarFlag('FIT_KERNEL_LOADADDRESS', kernel), and never
reads the values of the variables. Bitbake cannot track such accesses,
so the FIT_KERNEL_* and FIT_CONFIG_* flags are not part of the
do_compile signature. Changing e.g. FIT_KERNEL_LOADADDRESS[kernel-rt]
or FIT_CONFIG_DTB[rt] does not trigger a rebuild, and a stale fitImage
is reused. I verified this by comparing the do_compile basehash with
tinfoil.
FIT_LOADABLE_* has had the same problem since 2535427d8de0
("kernel-fit-image: support arbitrary loadables"), which is also in
wrynose. I'll send a separate patch for master which fixes it for the
loadables by adding the variables to the do_compile vardeps in this
loop:
d.appendVarFlag('do_compile', 'vardeps', ' ' + v)
If you rebase onto it and keep that line in your generic loop,
FIT_KERNEL_* and FIT_CONFIG_* are covered too. I tried this locally:
the hash then changes for both kernel and config flags. Because the
values are read with getVarFlag(v, item, False), something like
FIT_KERNEL_DEPLOYDIR[kernel-rt] = "${TOPDIR}/tmp-rt/..." doesn't leak
absolute paths into the signature. That is a good reason for the
expand=False change, so please mention it in the commit message.
> + 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")
The existing checks in this function raise SkipRecipe, while the new
ones use bb.fatal(), which aborts parsing for every target. Please
either make this consistent or explain why a fatal error is preferred.
> + 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):
Parsing BB_TASKDEPDATA and the sstate-control manifests ties the class
to sstate internals. Since the first manifest found in SSTATE_ARCHS
wins, this could also pick up a stale manifest after a PACKAGE_ARCH
change. I'd suggest dropping this check, or at least turning it into a
warning. The do_deploy dependency already makes sure that the file
exists and that its changes are tracked.
> + 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)
Minor: with KERNEL_DTBVENDORED = "1", a DTB from KERNEL_DEVICETREE is
skipped if it's also in EXTERNAL_KERNEL_DEVICETREE, and is then
registered under its basename. A FIT_CONFIG_DTB written as in
KERNEL_DEVICETREE does not match in that case.
Regards,
Adrian
> + 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):
fitimage_emit_section_config() still picks
configurations.sub_nodes[0] as the default. If a caller emits custom
configurations first, one of them silently becomes the default. A note
in the docstring or a check would help.
As mentioned in 1/4: with an overlay as FIT_CONFIG_DTB[x],
FIT_CONFIG_KERNEL[x] is silently ignored. This function would be a
good place to fail in that case.
> + """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 = [
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#248291):
> https://lists.openembedded.org/g/openembedded-core/message/248291
> Mute This Topic: https://lists.openembedded.org/mt/121849072/4454582
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe:
> https://lists.openembedded.org/g/openembedded-core/unsub [
> adrian.freihofer@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-24 13:42 UTC | newest]
Thread overview: 8+ 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-24 13:41 ` [OE-core] " adrian.freihofer
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-24 13:42 ` [OE-core] " adrian.freihofer
2026-10-20 13:35 ` [PATCH v2 4/4] oe.fitimage: Add custom configurations and harden multi-kernel handling Vahid Gharaee
2026-10-24 13:42 ` [OE-core] " adrian.freihofer
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.