[v2,1/2] image: remove unrequested intermediate image types

Message ID 20261104072600.3229817-1-clara.kowalsky@siemens.com
State New
Headers show
Series [v2,1/2] image: remove unrequested intermediate image types | expand

Commit Message

Clara Kowalsky Nov. 4, 2026, 7:25 a.m. UTC
Images built only as dependencies currently remain in the deploy
directory even when they are not listed in IMAGE_FSTYPES.
Fix this by keeping only explicitly requested image outputs and removing
dependency-only images and conversion intermediates. Users who need a
dependency image retained should include it in IMAGE_FSTYPES.

Since dependencies are now also removed, run cleanup after all image
tasks finish rather than at the end of each producing task. This keeps
dependency images available until their consumers have completed.
Expanded file paths are recorded, so cleanup also handles type-specific
filenames and deploy directories.
Unittests are added to test dependency cleanup and retention of
requested outputs.

Signed-off-by: Clara Kowalsky <clara.kowalsky@siemens.com>
---
 meta/classes-recipe/image.bbclass             | 24 ++---
 .../unittests/test_image_type_cleanup.py      | 92 +++++++++++++++++++
 2 files changed, 104 insertions(+), 12 deletions(-)
 create mode 100644 testsuite/unittests/test_image_type_cleanup.py

Patch

diff --git a/meta/classes-recipe/image.bbclass b/meta/classes-recipe/image.bbclass
index ce19ae1c..229a37fe 100644
--- a/meta/classes-recipe/image.bbclass
+++ b/meta/classes-recipe/image.bbclass
@@ -187,6 +187,8 @@  SUDO_CHROOT = "imager_run -d ${PP_ROOTFS} -u root --"
 # hook up IMAGE_CMD_*
 python() {
     image_types = (d.getVar('IMAGE_FSTYPES') or '').split()
+    requested_types = set(image_types)
+    intermediate_files = set()
     conversions = set(d.getVar('IMAGE_CONVERSIONS').split())
     chroot_mode = d.getVar('ISAR_CHROOT_MODE')
 
@@ -285,8 +287,10 @@  python() {
 
         # add conversions
         conversion_depends = set()
-        rm_images = set()
         def create_conversions(t):
+            localdata.setVar('type', t)
+            if t not in requested_types:
+                intermediate_files.add(localdata.getVar('IMAGE_FILE_HOST'))
             for c in sorted(conversions):
                 if t.endswith('.' + c):
                     t = t[:-len(c) - 1]
@@ -301,20 +305,9 @@  python() {
                     for dep in (localdata.getVar('CONVERSION_DEPS:' + c) or '').split():
                         conversion_install.add(dep)
                         local_conversion_install.add(dep)
-                    # remove temporary image files
-                    if t not in image_types:
-                        rm_images.add(localdata.expand('${IMAGE_FILE_HOST}'))
-
         for t in basetypes[bt]:
             create_conversions(t)
 
-        if bt not in image_types:
-            localdata.setVar('type', t)
-            rm_images.add(localdata.expand('${IMAGE_FILE_HOST}'))
-
-        for image in rm_images:
-            cmds.append('\trm ' + image)
-
         # image type dependencies
         after = 'do_image_tools'
         for dep in typedeps[bt]:
@@ -340,6 +333,7 @@  python() {
         d.appendVar('INSTALL_image_%s' % bt_clean, ' ' + ' '.join(sorted(local_imager_install | local_conversion_install)))
         d.appendVarFlag(task, 'vardeps', ' INSTALL_image_%s' % bt_clean)
 
+    d.setVar('IMAGE_INTERMEDIATE_FILES', ' '.join(sorted(intermediate_files)))
     d.appendVar('IMAGER_INSTALL', ' ' + ' '.join(sorted(imager_install | conversion_install)))
     d.appendVar('IMAGER_BOM', ' ' + ' '.join(sorted(imager_bom)))
     d.appendVar('IMAGER_BUILD_DEPS', ' ' + ' '.join(sorted(imager_build_deps)))
@@ -429,6 +423,12 @@  python do_image() {
     """Virtual task"""
     pass
 }
+python cleanup_image_intermediates() {
+    for image in (d.getVar('IMAGE_INTERMEDIATE_FILES') or '').split():
+        bb.utils.remove(image)
+}
+do_image[postfuncs] += "cleanup_image_intermediates"
+do_image[vardeps] += "IMAGE_INTERMEDIATE_FILES"
 addtask image before do_build after do_image_tools
 
 python do_deploy() {
diff --git a/testsuite/unittests/test_image_type_cleanup.py b/testsuite/unittests/test_image_type_cleanup.py
new file mode 100644
index 00000000..1e2ad5b9
--- /dev/null
+++ b/testsuite/unittests/test_image_type_cleanup.py
@@ -0,0 +1,92 @@ 
+# This software is a part of Isar.
+# Copyright (C) Siemens AG, 2026
+#
+# SPDX-License-Identifier: MIT
+
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+root = Path(__file__).resolve().parents[2]
+sys.path.extend([
+    str(root / 'bitbake/lib'),
+    str(root / 'meta/lib'),
+])
+
+import bb.data
+from bb.build import exec_func
+from bb.parse import handle
+
+
+data = bb.data.init()
+data.setVar('BBPATH', f'{root}/meta:{root}/bitbake')
+data.setVar('__bbclasstype', 'recipe')
+data.setVar('IMGCLASSES', '')
+parsed = handle(str(root / 'meta/classes-recipe/image.bbclass'), data)
+data.setVar('IMAGE_TYPEDEP:ova', 'vmdk')
+data.setVar('IMAGE_TYPEDEP:vmdk', 'wic')
+data.setVar('IMAGE_CMD:vmdk', ':')
+namespace = {'bb': bb}
+exec(parsed['get_base_type'], namespace)
+image_tasks = next(
+    function for function in parsed.getVar('__BBANONFUNCS', False)
+    if 'def collect_image_type(' in parsed.getVar(function, False)
+)
+exec(parsed[image_tasks], namespace)
+
+
+class TestImageTypeCleanup(unittest.TestCase):
+    def generate_image_tasks(self, image_types, deploy_dir='/deploy'):
+        d = bb.data.createCopy(data)
+        d.setVar('IMAGE_FSTYPES', image_types)
+        d.setVar('DEPLOY_DIR_IMAGE', str(deploy_dir))
+        d.setVar('IMAGE_FULLNAME', 'image')
+        namespace[image_tasks](d)
+        return d
+
+    def assert_intermediates(self, image_types, expected):
+        d = self.generate_image_tasks(image_types)
+        self.assertEqual(
+            set(d.getVar('IMAGE_INTERMEDIATE_FILES').split()),
+            {'/deploy/image.' + image_type for image_type in expected},
+        )
+
+    def test_only_unrequested_types_are_intermediates(self):
+        self.assert_intermediates('ova.zst', {'ova', 'vmdk', 'wic'})
+        self.assert_intermediates('vmdk ova.zst', {'ova', 'wic'})
+        self.assert_intermediates('ova ova.zst vmdk wic', set())
+
+    def test_unrequested_conversion_stages_are_cleaned(self):
+        self.assert_intermediates('tar.gz.xz', {'tar', 'tar.gz'})
+        self.assert_intermediates('tar.gz tar.gz.xz', {'tar'})
+
+    def test_cleanup_uses_image_type_filename_overrides(self):
+        d = bb.data.createCopy(data)
+        d.setVar('IMAGE_FSTYPES', 'tar.gz')
+        d.setVar('IMAGE_FULLNAME:tar', 'custom-image')
+        d.setVar('DEPLOY_DIR_IMAGE:tar', '/custom-deploy')
+        namespace[image_tasks](d)
+        self.assertEqual(
+            d.getVar('IMAGE_INTERMEDIATE_FILES'),
+            '/custom-deploy/custom-image.tar',
+        )
+
+    def test_cleanup_removes_only_intermediate_files(self):
+        with tempfile.TemporaryDirectory() as deploy_dir:
+            deploy_dir = Path(deploy_dir)
+            for image_type in ('ova', 'wic', 'vmdk', 'ova.zst'):
+                (deploy_dir / ('image.' + image_type)).touch()
+
+            d = self.generate_image_tasks('vmdk ova.zst', deploy_dir)
+            d.setVar('T', str(deploy_dir / 'temp'))
+            exec_func('cleanup_image_intermediates', d)
+
+            self.assertEqual(
+                {path.name for path in deploy_dir.glob('image.*')},
+                {'image.vmdk', 'image.ova.zst'},
+            )
+
+
+if __name__ == '__main__':
+    unittest.main()