diff mbox series

[v3,1/1] wic: extra partition plugin

Message ID 20250930094344.3049278-2-pierre-loup.gosse@smile.fr
State New
Headers show
Series wic: extra partition plugin | expand

Commit Message

Pierre-loup GOSSE Sept. 30, 2025, 9:43 a.m. UTC
From: Pierre-Loup GOSSE <pierre-loup.gosse@smile.fr>

The extra_partition plugin allows populating an extra partition with
files listed in the new IMAGE_EXTRA_FILES variable. The implementation
is similar to the bootimg_partition plugin.

This plugin provides an easy way to install files that are not part of
the rootfs.

Signed-off-by: Pierre-Loup GOSSE <pierre-loup.gosse@smile.fr>
Reviewed-by: Yoann CONGAL <yoann.congal@smile.fr>
---
changes in v2:
- missing signed-off-by
- typo
- assertion message

changes in v3:
- fix WIC test_image_env
---
 meta/classes-recipe/image_types_wic.bbclass   |   1 +
 meta/lib/oeqa/selftest/cases/wic.py           |  43 +++++-
 .../lib/wic/plugins/source/extra_partition.py | 134 ++++++++++++++++++
 3 files changed, 177 insertions(+), 1 deletion(-)
 create mode 100644 scripts/lib/wic/plugins/source/extra_partition.py

Comments

Yoann Congal Oct. 2, 2025, 10:18 a.m. UTC | #1
Hello Pierre-Loup,

Le mar. 30 sept. 2025 à 11:43, <pierre-loup.gosse@smile.fr> a écrit :

> From: Pierre-Loup GOSSE <pierre-loup.gosse@smile.fr>
>
> The extra_partition plugin allows populating an extra partition with
> files listed in the new IMAGE_EXTRA_FILES variable. The implementation
>

From the patch review call:
The name IMAGE_EXTRA_FILES is a bit too generic to be put in the global
variable namespace.
The name "IMAGE_EXTRA_PARTITION_FILES" would avoid confusion or clashing.
Can you make the change?

The rest of the patch looks good :)


> is similar to the bootimg_partition plugin.
>
> This plugin provides an easy way to install files that are not part of
> the rootfs.
>
> Signed-off-by: Pierre-Loup GOSSE <pierre-loup.gosse@smile.fr>
> Reviewed-by: Yoann CONGAL <yoann.congal@smile.fr>
> ---
> changes in v2:
> - missing signed-off-by
> - typo
> - assertion message
>
> changes in v3:
> - fix WIC test_image_env
> ---
>  meta/classes-recipe/image_types_wic.bbclass   |   1 +
>  meta/lib/oeqa/selftest/cases/wic.py           |  43 +++++-
>  .../lib/wic/plugins/source/extra_partition.py | 134 ++++++++++++++++++
>  3 files changed, 177 insertions(+), 1 deletion(-)
>  create mode 100644 scripts/lib/wic/plugins/source/extra_partition.py
>
> diff --git a/meta/classes-recipe/image_types_wic.bbclass
> b/meta/classes-recipe/image_types_wic.bbclass
> index 6180874a4c..549a69db60 100644
> --- a/meta/classes-recipe/image_types_wic.bbclass
> +++ b/meta/classes-recipe/image_types_wic.bbclass
> @@ -17,6 +17,7 @@ WICVARS ?= "\
>         IMAGE_BOOT_FILES \
>         IMAGE_CLASSES \
>         IMAGE_EFI_BOOT_FILES \
> +       IMAGE_EXTRA_FILES \
>         IMAGE_LINK_NAME \
>         IMAGE_ROOTFS \
>         IMGDEPLOYDIR \
> diff --git a/meta/lib/oeqa/selftest/cases/wic.py
> b/meta/lib/oeqa/selftest/cases/wic.py
> index b1c318bd4e..1b59980f1c 100644
> --- a/meta/lib/oeqa/selftest/cases/wic.py
> +++ b/meta/lib/oeqa/selftest/cases/wic.py
> @@ -18,6 +18,7 @@ from glob import glob
>  from shutil import rmtree, copy
>  from tempfile import NamedTemporaryFile
>  from tempfile import TemporaryDirectory
> +from textwrap import dedent
>
>  from oeqa.selftest.case import OESelftestTestCase
>  from oeqa.core.decorator import OETestTag
> @@ -1021,7 +1022,7 @@ class Wic2(WicTestCase):
>          wicvars = wicvars.difference(('DEPLOY_DIR_IMAGE',
> 'IMAGE_BOOT_FILES',
>                                        'INITRD', 'INITRD_LIVE',
> 'ISODIR','INITRAMFS_IMAGE',
>                                        'INITRAMFS_IMAGE_BUNDLE',
> 'INITRAMFS_LINK_NAME',
> -                                      'APPEND', 'IMAGE_EFI_BOOT_FILES'))
> +                                      'APPEND', 'IMAGE_EFI_BOOT_FILES',
> 'IMAGE_EXTRA_FILES'))
>          with open(path) as envfile:
>              content = dict(line.split("=", 1) for line in envfile)
>              # test if variables used by wic present in the .env file
> @@ -1647,6 +1648,46 @@ INITRAMFS_IMAGE = "core-image-initramfs-boot"
>              status, output = qemu.run_serial(cmd)
>              self.assertEqual(1, status, 'Failed to run command "%s": %s'
> % (cmd, output))
>
> +    def test_extra_partition_plugin(self):
> +        """Test extra partition plugin"""
> +        config = dedent("""\
> +        IMAGE_EXTRA_FILES_label-foo = "bar.conf;foo.conf"
> +        IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d =
> "bar.conf;foobar.conf"
> +        IMAGE_EXTRA_FILES = "foo/*"
> +        WICVARS:append = "\
> +            IMAGE_EXTRA_FILES_label-foo \
> +            IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d \
> +        "
> +        """)
> +        self.append_config(config)
> +
> +        deploy_dir = get_bb_var('DEPLOY_DIR_IMAGE')
> +
> +        testfile = open(os.path.join(deploy_dir, "bar.conf"), "w")
> +        testfile.write("test")
> +        testfile.close()
> +
> +        os.mkdir(os.path.join(deploy_dir, "foo"))
> +        testfile = open(os.path.join(deploy_dir, "foo", "bar.conf"), "w")
> +        testfile.write("test")
> +        testfile.close()
> +
> +        with NamedTemporaryFile("w", suffix=".wks") as wks:
> +            wks.writelines(['part / --source extra_partition --ondisk sda
> --fstype=ext4 --label foo --align 4 --size 5M\n',
> +                            'part / --source extra_partition --ondisk sda
> --fstype=ext4 --uuid e7d0824e-cda3-4bed-9f54-9ef5312d105d --align 4 --size
> 5M\n',
> +                            'part / --source extra_partition --ondisk sda
> --fstype=ext4 --label bar --align 4 --size 5M\n'])
> +            wks.flush()
> +            _, wicimg = self._get_wic(wks.name)
> +
> +            result = runCmd("wic ls %s | wc -l" % wicimg)
> +            self.assertEqual('4', result.output, msg="Expect 3
> partitions, not %s" % result.output)
> +
> +            for part, file in enumerate(["foo.conf", "foobar.conf",
> "bar.conf"]):
> +                result = runCmd("wic ls %s:%d | grep -q \"%s\"" %
> (wicimg, part + 1, file))
> +                self.assertEqual(0, result.status, msg="File '%s' not
> found in the partition #%d" % (file, part))
> +
> +        self.remove_config(config)
> +
>      def test_fs_types(self):
>          """Test filesystem types for empty and not empty partitions"""
>          img = 'core-image-minimal'
> diff --git a/scripts/lib/wic/plugins/source/extra_partition.py
> b/scripts/lib/wic/plugins/source/extra_partition.py
> new file mode 100644
> index 0000000000..499bede280
> --- /dev/null
> +++ b/scripts/lib/wic/plugins/source/extra_partition.py
> @@ -0,0 +1,134 @@
> +import logging
> +import os
> +import re
> +
> +from glob import glob
> +
> +from wic import WicError
> +from wic.pluginbase import SourcePlugin
> +from wic.misc import exec_cmd, get_bitbake_var
> +
> +logger = logging.getLogger('wic')
> +
> +class ExtraPartitionPlugin(SourcePlugin):
> +    """
> +    Populates an extra partition with files listed in the
> IMAGE_EXTRA_FILES
> +    BitBake variable. Files should be deployed to the DEPLOY_DIR_IMAGE
> directory.
> +
> +    The plugin supports:
> +    - Glob pattern matching for file selection.
> +    - File renaming.
> +    - Suffixes to specify the target partition (by label, UUID, or
> partname),
> +      enabling multiple extra partitions to coexist.
> +
> +    For example:
> +
> +        IMAGE_EXTRA_FILES_label-foo = "bar.conf;foo.conf"
> +        IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d =
> "bar.conf;foobar.conf"
> +        IMAGE_EXTRA_FILES = "foo/*"
> +        WICVARS:append = "\
> +            IMAGE_EXTRA_FILES_label-foo \
> +            IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d \
> +        "
> +
> +    """
> +
> +    name = 'extra_partition'
> +    image_extra_files_var_name = 'IMAGE_EXTRA_FILES'
> +
> +    @classmethod
> +    def do_configure_partition(cls, part, source_params, cr, cr_workdir,
> +                             oe_builddir, bootimg_dir, kernel_dir,
> +                             native_sysroot):
> +        """
> +        Called before do_prepare_partition(), list the files to copy
> +        """
> +        extradir = "%s/extra.%d" % (cr_workdir, part.lineno)
> +        install_cmd = "install -d %s" % extradir
> +        exec_cmd(install_cmd)
> +
> +        if not kernel_dir:
> +            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
> +            if not kernel_dir:
> +                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
> +
> +        extra_files = None
> +        for (fmt, id) in (("_uuid-%s", part.uuid), ("_label-%s",
> part.label), ("_part-name-%s", part.part_name), (None, None)):
> +            if fmt:
> +                var = fmt % id
> +            else:
> +                var = ""
> +            extra_files = get_bitbake_var(cls.image_extra_files_var_name
> + var)
> +            if extra_files is not None:
> +                break
> +
> +        if extra_files is None:
> +            raise WicError('No extra files defined, %s unset for entry
> #%d' % (cls.image_extra_files_var_name, part.lineno))
> +
> +        logger.info('Extra files: %s', extra_files)
> +
> +        # list of tuples (src_name, dst_name)
> +        deploy_files = []
> +        for src_entry in re.findall(r'[\w;\-\./\*]+', extra_files):
> +            if ';' in src_entry:
> +                dst_entry = tuple(src_entry.split(';'))
> +                if not dst_entry[0] or not dst_entry[1]:
> +                    raise WicError('Malformed extra file entry: %s' %
> src_entry)
> +            else:
> +                dst_entry = (src_entry, src_entry)
> +
> +            logger.debug('Destination entry: %r', dst_entry)
> +            deploy_files.append(dst_entry)
> +
> +        cls.install_task = [];
> +        for deploy_entry in deploy_files:
> +            src, dst = deploy_entry
> +            if '*' in src:
> +                # by default install files under their basename
> +                entry_name_fn = os.path.basename
> +                if dst != src:
> +                    # unless a target name was given, then treat name
> +                    # as a directory and append a basename
> +                    entry_name_fn = lambda name: \
> +                                    os.path.join(dst,
> +                                                 os.path.basename(name))
> +
> +                srcs = glob(os.path.join(kernel_dir, src))
> +
> +                logger.debug('Globbed sources: %s', ', '.join(srcs))
> +                for entry in srcs:
> +                    src = os.path.relpath(entry, kernel_dir)
> +                    entry_dst_name = entry_name_fn(entry)
> +                    cls.install_task.append((src, entry_dst_name))
> +            else:
> +                cls.install_task.append((src, dst))
> +
> +
> +    @classmethod
> +    def do_prepare_partition(cls, part, source_params, cr, cr_workdir,
> +                             oe_builddir, bootimg_dir, kernel_dir,
> +                             rootfs_dir, native_sysroot):
> +        """
> +        Called to do the actual content population for a partition i.e. it
> +        'prepares' the partition to be incorporated into the image.
> +        In this case, we copies all files listed in IMAGE_EXTRA_FILES
> variable.
> +        """
> +        extradir = "%s/extra.%d" % (cr_workdir, part.lineno)
> +
> +        if not kernel_dir:
> +            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
> +            if not kernel_dir:
> +                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
> +
> +        for task in cls.install_task:
> +            src_path, dst_path = task
> +            logger.debug('Install %s as %s', src_path, dst_path)
> +            install_cmd = "install -m 0644 -D %s %s" \
> +                          % (os.path.join(kernel_dir, src_path),
> +                             os.path.join(extradir, dst_path))
> +            exec_cmd(install_cmd)
> +
> +        logger.debug('Prepare extra partition using rootfs in %s',
> extradir)
> +        part.prepare_rootfs(cr_workdir, oe_builddir, extradir,
> +                            native_sysroot, False)
> +
> --
> 2.34.1
>
>
diff mbox series

Patch

diff --git a/meta/classes-recipe/image_types_wic.bbclass b/meta/classes-recipe/image_types_wic.bbclass
index 6180874a4c..549a69db60 100644
--- a/meta/classes-recipe/image_types_wic.bbclass
+++ b/meta/classes-recipe/image_types_wic.bbclass
@@ -17,6 +17,7 @@  WICVARS ?= "\
 	IMAGE_BOOT_FILES \
 	IMAGE_CLASSES \
 	IMAGE_EFI_BOOT_FILES \
+	IMAGE_EXTRA_FILES \
 	IMAGE_LINK_NAME \
 	IMAGE_ROOTFS \
 	IMGDEPLOYDIR \
diff --git a/meta/lib/oeqa/selftest/cases/wic.py b/meta/lib/oeqa/selftest/cases/wic.py
index b1c318bd4e..1b59980f1c 100644
--- a/meta/lib/oeqa/selftest/cases/wic.py
+++ b/meta/lib/oeqa/selftest/cases/wic.py
@@ -18,6 +18,7 @@  from glob import glob
 from shutil import rmtree, copy
 from tempfile import NamedTemporaryFile
 from tempfile import TemporaryDirectory
+from textwrap import dedent
 
 from oeqa.selftest.case import OESelftestTestCase
 from oeqa.core.decorator import OETestTag
@@ -1021,7 +1022,7 @@  class Wic2(WicTestCase):
         wicvars = wicvars.difference(('DEPLOY_DIR_IMAGE', 'IMAGE_BOOT_FILES',
                                       'INITRD', 'INITRD_LIVE', 'ISODIR','INITRAMFS_IMAGE',
                                       'INITRAMFS_IMAGE_BUNDLE', 'INITRAMFS_LINK_NAME',
-                                      'APPEND', 'IMAGE_EFI_BOOT_FILES'))
+                                      'APPEND', 'IMAGE_EFI_BOOT_FILES', 'IMAGE_EXTRA_FILES'))
         with open(path) as envfile:
             content = dict(line.split("=", 1) for line in envfile)
             # test if variables used by wic present in the .env file
@@ -1647,6 +1648,46 @@  INITRAMFS_IMAGE = "core-image-initramfs-boot"
             status, output = qemu.run_serial(cmd)
             self.assertEqual(1, status, 'Failed to run command "%s": %s' % (cmd, output))
 
+    def test_extra_partition_plugin(self):
+        """Test extra partition plugin"""
+        config = dedent("""\
+        IMAGE_EXTRA_FILES_label-foo = "bar.conf;foo.conf"
+        IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d = "bar.conf;foobar.conf"
+        IMAGE_EXTRA_FILES = "foo/*"
+        WICVARS:append = "\
+            IMAGE_EXTRA_FILES_label-foo \
+            IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d \
+        "
+        """)
+        self.append_config(config)
+
+        deploy_dir = get_bb_var('DEPLOY_DIR_IMAGE')
+
+        testfile = open(os.path.join(deploy_dir, "bar.conf"), "w")
+        testfile.write("test")
+        testfile.close()
+
+        os.mkdir(os.path.join(deploy_dir, "foo"))
+        testfile = open(os.path.join(deploy_dir, "foo", "bar.conf"), "w")
+        testfile.write("test")
+        testfile.close()
+
+        with NamedTemporaryFile("w", suffix=".wks") as wks:
+            wks.writelines(['part / --source extra_partition --ondisk sda --fstype=ext4 --label foo --align 4 --size 5M\n',
+                            'part / --source extra_partition --ondisk sda --fstype=ext4 --uuid e7d0824e-cda3-4bed-9f54-9ef5312d105d --align 4 --size 5M\n',
+                            'part / --source extra_partition --ondisk sda --fstype=ext4 --label bar --align 4 --size 5M\n'])
+            wks.flush()
+            _, wicimg = self._get_wic(wks.name)
+
+            result = runCmd("wic ls %s | wc -l" % wicimg)
+            self.assertEqual('4', result.output, msg="Expect 3 partitions, not %s" % result.output)
+
+            for part, file in enumerate(["foo.conf", "foobar.conf", "bar.conf"]):
+                result = runCmd("wic ls %s:%d | grep -q \"%s\"" % (wicimg, part + 1, file))
+                self.assertEqual(0, result.status, msg="File '%s' not found in the partition #%d" % (file, part))
+
+        self.remove_config(config)
+
     def test_fs_types(self):
         """Test filesystem types for empty and not empty partitions"""
         img = 'core-image-minimal'
diff --git a/scripts/lib/wic/plugins/source/extra_partition.py b/scripts/lib/wic/plugins/source/extra_partition.py
new file mode 100644
index 0000000000..499bede280
--- /dev/null
+++ b/scripts/lib/wic/plugins/source/extra_partition.py
@@ -0,0 +1,134 @@ 
+import logging
+import os
+import re
+
+from glob import glob
+
+from wic import WicError
+from wic.pluginbase import SourcePlugin
+from wic.misc import exec_cmd, get_bitbake_var
+
+logger = logging.getLogger('wic')
+
+class ExtraPartitionPlugin(SourcePlugin):
+    """
+    Populates an extra partition with files listed in the IMAGE_EXTRA_FILES
+    BitBake variable. Files should be deployed to the DEPLOY_DIR_IMAGE directory.
+
+    The plugin supports:
+    - Glob pattern matching for file selection.
+    - File renaming.
+    - Suffixes to specify the target partition (by label, UUID, or partname),
+      enabling multiple extra partitions to coexist.
+
+    For example:
+
+        IMAGE_EXTRA_FILES_label-foo = "bar.conf;foo.conf"
+        IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d = "bar.conf;foobar.conf"
+        IMAGE_EXTRA_FILES = "foo/*"
+        WICVARS:append = "\
+            IMAGE_EXTRA_FILES_label-foo \
+            IMAGE_EXTRA_FILES_uuid-e7d0824e-cda3-4bed-9f54-9ef5312d105d \
+        "
+
+    """
+
+    name = 'extra_partition'
+    image_extra_files_var_name = 'IMAGE_EXTRA_FILES'
+
+    @classmethod
+    def do_configure_partition(cls, part, source_params, cr, cr_workdir,
+                             oe_builddir, bootimg_dir, kernel_dir,
+                             native_sysroot):
+        """
+        Called before do_prepare_partition(), list the files to copy
+        """
+        extradir = "%s/extra.%d" % (cr_workdir, part.lineno)
+        install_cmd = "install -d %s" % extradir
+        exec_cmd(install_cmd)
+
+        if not kernel_dir:
+            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
+            if not kernel_dir:
+                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
+
+        extra_files = None
+        for (fmt, id) in (("_uuid-%s", part.uuid), ("_label-%s", part.label), ("_part-name-%s", part.part_name), (None, None)):
+            if fmt:
+                var = fmt % id
+            else:
+                var = ""
+            extra_files = get_bitbake_var(cls.image_extra_files_var_name + var)
+            if extra_files is not None:
+                break
+
+        if extra_files is None:
+            raise WicError('No extra files defined, %s unset for entry #%d' % (cls.image_extra_files_var_name, part.lineno))
+
+        logger.info('Extra files: %s', extra_files)
+
+        # list of tuples (src_name, dst_name)
+        deploy_files = []
+        for src_entry in re.findall(r'[\w;\-\./\*]+', extra_files):
+            if ';' in src_entry:
+                dst_entry = tuple(src_entry.split(';'))
+                if not dst_entry[0] or not dst_entry[1]:
+                    raise WicError('Malformed extra file entry: %s' % src_entry)
+            else:
+                dst_entry = (src_entry, src_entry)
+
+            logger.debug('Destination entry: %r', dst_entry)
+            deploy_files.append(dst_entry)
+
+        cls.install_task = [];
+        for deploy_entry in deploy_files:
+            src, dst = deploy_entry
+            if '*' in src:
+                # by default install files under their basename
+                entry_name_fn = os.path.basename
+                if dst != src:
+                    # unless a target name was given, then treat name
+                    # as a directory and append a basename
+                    entry_name_fn = lambda name: \
+                                    os.path.join(dst,
+                                                 os.path.basename(name))
+
+                srcs = glob(os.path.join(kernel_dir, src))
+
+                logger.debug('Globbed sources: %s', ', '.join(srcs))
+                for entry in srcs:
+                    src = os.path.relpath(entry, kernel_dir)
+                    entry_dst_name = entry_name_fn(entry)
+                    cls.install_task.append((src, entry_dst_name))
+            else:
+                cls.install_task.append((src, dst))
+
+
+    @classmethod
+    def do_prepare_partition(cls, part, source_params, cr, cr_workdir,
+                             oe_builddir, bootimg_dir, kernel_dir,
+                             rootfs_dir, native_sysroot):
+        """
+        Called to do the actual content population for a partition i.e. it
+        'prepares' the partition to be incorporated into the image.
+        In this case, we copies all files listed in IMAGE_EXTRA_FILES variable.
+        """
+        extradir = "%s/extra.%d" % (cr_workdir, part.lineno)
+
+        if not kernel_dir:
+            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
+            if not kernel_dir:
+                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
+
+        for task in cls.install_task:
+            src_path, dst_path = task
+            logger.debug('Install %s as %s', src_path, dst_path)
+            install_cmd = "install -m 0644 -D %s %s" \
+                          % (os.path.join(kernel_dir, src_path),
+                             os.path.join(extradir, dst_path))
+            exec_cmd(install_cmd)
+
+        logger.debug('Prepare extra partition using rootfs in %s', extradir)
+        part.prepare_rootfs(cr_workdir, oe_builddir, extradir,
+                            native_sysroot, False)
+