diff mbox series

[2/2] runqemu: Add support for running compressed .zst rootfs images

Message ID 20250725123811.1718097-2-lamine.rehahlia@smile.fr
State Accepted, archived
Commit e069fe2480c871c649b83f6278564a553cc3dd58
Headers show
Series [1/2] runqemu: Enable snapshot mode by default | expand

Commit Message

lamine.rehahlia@smile.fr July 25, 2025, 12:38 p.m. UTC
From: Lamine REHAHLIA <lamine.rehahlia@smile.fr>

Enhance runqemu to detect and decompress .zst-compressed rootfs images
(e.g. ext4.zst, wic.zst) automatically. If a decompressed image already
exists in the original directory, it will be reused to avoid overwriting
build artifacts. Otherwise, the image is decompressed and removed after
the QEMU session ends.

This allows runqemu to be used seamlessly with compressed image formats
generated by the build system or during releases.

IMPORTANT:
This patch assumes that the original directory of the .zst-compressed
image is writable. If, for some reason, the path passed from CI or
another system to the script is read-only, the decompression step will
fail when trying to write the uncompressed image to the same directory.

Signed-off-by: Lamine REHAHLIA <lamine.rehahlia@smile.fr>
---
 scripts/runqemu | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

Comments

Alexander Kanavin July 26, 2025, 9:25 p.m. UTC | #1
No. Filesystem images can be written into, so that any changes to
rootfs are preserved between runqemu sessions, just like on real
hardware with real storage. This would result in throwing away the
writes.

Alex

On Fri, 25 Jul 2025 at 14:38, lamine.rehahlia via
lists.openembedded.org
<lamine.rehahlia=smile.fr@lists.openembedded.org> wrote:
>
> From: Lamine REHAHLIA <lamine.rehahlia@smile.fr>
>
> Enhance runqemu to detect and decompress .zst-compressed rootfs images
> (e.g. ext4.zst, wic.zst) automatically. If a decompressed image already
> exists in the original directory, it will be reused to avoid overwriting
> build artifacts. Otherwise, the image is decompressed and removed after
> the QEMU session ends.
>
> This allows runqemu to be used seamlessly with compressed image formats
> generated by the build system or during releases.
>
> IMPORTANT:
> This patch assumes that the original directory of the .zst-compressed
> image is writable. If, for some reason, the path passed from CI or
> another system to the script is read-only, the decompression step will
> fail when trying to write the uncompressed image to the same directory.
>
> Signed-off-by: Lamine REHAHLIA <lamine.rehahlia@smile.fr>
> ---
>  scripts/runqemu | 37 ++++++++++++++++++++++++++++++++++++-
>  1 file changed, 36 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/runqemu b/scripts/runqemu
> index 6c263d6b2a..b6af619f20 100755
> --- a/scripts/runqemu
> +++ b/scripts/runqemu
> @@ -376,7 +376,42 @@ class BaseConfig(object):
>               re.search('fitImage', p) or re.search('uImage', p):
>              self.kernel =  p
>          elif os.path.isfile(p) and ('-image-' in os.path.basename(p) or '.rootfs.' in os.path.basename(p)):
> -            self.rootfs = p
> +
> +            # Decompress ZST image if needed
> +            if p.endswith('.zst'):
> +                # Get the real path to the image to avoid issues when a symbolic link is passed.
> +                # This ensures we always operate on the actual file.
> +                image_path = os.path.realpath(p)
> +                # Extract target filename by removing .zst
> +                image_dir = os.path.dirname(image_path)
> +                uncompressed_name = os.path.basename(image_path).replace(".zst", "")
> +                uncompressed_path = os.path.join(image_dir, uncompressed_name)
> +
> +                # If the decompressed image already exists (e.g., in the deploy directory),
> +                # we use it directly to avoid overwriting artifacts generated by the build system.
> +                # This prevents redundant decompression and preserves build outputs.
> +                if os.path.exists(uncompressed_path):
> +                    logger.warning(f"Found existing decompressed image: {uncompressed_path}, Using it directly.")
> +                else:
> +                    logger.info(f"Decompressing {p} to {uncompressed_path}")
> +                    # Ensure the 'zstd' tool is installed before attempting to decompress.
> +                    if not shutil.which('zstd'):
> +                        raise RunQemuError(f"'zstd' is required to decompress {p} but was not found in PATH")
> +                    try:
> +                        with open(uncompressed_path, 'wb') as out_file:
> +                            subprocess.check_call(['zstd', '-d', '-c', image_path], stdout=out_file)
> +                    except subprocess.CalledProcessError as e:
> +                        self.cleanup_files.append(uncompressed_path)
> +                        raise RunQemuError(f"Failed to decompress {p}: {e}")
> +
> +                    # Mark for deletion at the end
> +                    self.cleanup_files.append(uncompressed_path)
> +
> +                # Use the decompressed image as the rootfs
> +                self.rootfs = uncompressed_path
> +
> +            else:
> +                self.rootfs = p
>              # Check filename against self.fstypes can handle <file>.cpio.gz,
>              # otherwise, its type would be "gz", which is incorrect.
>              fst = ""
> --
> 2.43.0
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#220909): https://lists.openembedded.org/g/openembedded-core/message/220909
> Mute This Topic: https://lists.openembedded.org/mt/114336729/1686489
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [alex.kanavin@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
diff mbox series

Patch

diff --git a/scripts/runqemu b/scripts/runqemu
index 6c263d6b2a..b6af619f20 100755
--- a/scripts/runqemu
+++ b/scripts/runqemu
@@ -376,7 +376,42 @@  class BaseConfig(object):
              re.search('fitImage', p) or re.search('uImage', p):
             self.kernel =  p
         elif os.path.isfile(p) and ('-image-' in os.path.basename(p) or '.rootfs.' in os.path.basename(p)):
-            self.rootfs = p
+
+            # Decompress ZST image if needed
+            if p.endswith('.zst'):
+                # Get the real path to the image to avoid issues when a symbolic link is passed.
+                # This ensures we always operate on the actual file.
+                image_path = os.path.realpath(p)
+                # Extract target filename by removing .zst
+                image_dir = os.path.dirname(image_path)
+                uncompressed_name = os.path.basename(image_path).replace(".zst", "")
+                uncompressed_path = os.path.join(image_dir, uncompressed_name)
+
+                # If the decompressed image already exists (e.g., in the deploy directory),
+                # we use it directly to avoid overwriting artifacts generated by the build system.
+                # This prevents redundant decompression and preserves build outputs.
+                if os.path.exists(uncompressed_path):
+                    logger.warning(f"Found existing decompressed image: {uncompressed_path}, Using it directly.")
+                else:
+                    logger.info(f"Decompressing {p} to {uncompressed_path}")
+                    # Ensure the 'zstd' tool is installed before attempting to decompress.
+                    if not shutil.which('zstd'):
+                        raise RunQemuError(f"'zstd' is required to decompress {p} but was not found in PATH")
+                    try:
+                        with open(uncompressed_path, 'wb') as out_file:
+                            subprocess.check_call(['zstd', '-d', '-c', image_path], stdout=out_file)
+                    except subprocess.CalledProcessError as e:
+                        self.cleanup_files.append(uncompressed_path)
+                        raise RunQemuError(f"Failed to decompress {p}: {e}")
+
+                    # Mark for deletion at the end
+                    self.cleanup_files.append(uncompressed_path)
+
+                # Use the decompressed image as the rootfs
+                self.rootfs = uncompressed_path
+
+            else:
+                self.rootfs = p
             # Check filename against self.fstypes can handle <file>.cpio.gz,
             # otherwise, its type would be "gz", which is incorrect.
             fst = ""