diff --git a/build-tests/x86/rawhide/test-image-erofs/appliance.kiwi b/build-tests/x86/rawhide/test-image-erofs/appliance.kiwi new file mode 100644 index 00000000000..28945ab0bb9 --- /dev/null +++ b/build-tests/x86/rawhide/test-image-erofs/appliance.kiwi @@ -0,0 +1,40 @@ + + + + + Marcus Schaefer + marcus.schaefer@suse.com + Fedora Appliance, Testing erofs filesystem image + + + 2.0.0 + dnf5 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-tests/x86/rawhide/test-image-live-disk/appliance.kiwi b/build-tests/x86/rawhide/test-image-live-disk/appliance.kiwi index e9039c726bb..397668030cc 100644 --- a/build-tests/x86/rawhide/test-image-live-disk/appliance.kiwi +++ b/build-tests/x86/rawhide/test-image-live-disk/appliance.kiwi @@ -11,6 +11,7 @@ + @@ -24,6 +25,11 @@ UTC false + + + + + @@ -71,7 +77,7 @@ - + diff --git a/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi b/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi index f713e7c57d4..c41a35f068d 100644 --- a/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi +++ b/build-tests/x86/tumbleweed/test-image-live/appliance.kiwi @@ -14,6 +14,7 @@ + 1.42.3 @@ -31,6 +32,11 @@ + + + + + @@ -50,7 +56,7 @@ - + @@ -58,6 +64,7 @@ + diff --git a/build-tests/x86/tumbleweed/test-image-live/config.sh b/build-tests/x86/tumbleweed/test-image-live/config.sh index 1bd6f33a453..607183c193e 100644 --- a/build-tests/x86/tumbleweed/test-image-live/config.sh +++ b/build-tests/x86/tumbleweed/test-image-live/config.sh @@ -1,39 +1,26 @@ #!/bin/bash -#================ -# FILE : config.sh -#---------------- -# PROJECT : OpenSuSE KIWI Image System -# COPYRIGHT : (c) 2006 SUSE LINUX Products GmbH. All rights reserved -# : -# AUTHOR : Marcus Schaefer -# : -# BELONGS TO : Operating System images -# : -# DESCRIPTION : configuration script for SUSE based -# : operating systems -#---------------- -#====================================== -# Functions... -#-------------------------------------- -test -f /.kconfig && . /.kconfig -test -f /.profile && . /.profile + +set -ex + +declare kiwi_profiles=${kiwi_profiles} +declare kiwi_iname=${kiwi_iname} #====================================== # Greeting... #-------------------------------------- echo "Configure image: [$kiwi_iname]..." -#====================================== -# Setup baseproduct link -#-------------------------------------- -suseSetupProduct - #====================================== # Activate services #-------------------------------------- -suseInsertService sshd +systemctl enable sshd #====================================== -# Setup default target, multi-user +# Include erofs module #-------------------------------------- -baseSetRunlevel 3 +for profile in ${kiwi_profiles//,/ }; do + if [ "${profile}" = "EroFS" ]; then + # remove from blacklist + rm -f /usr/lib/modprobe.d/60-blacklist_fs-erofs.conf + fi +done diff --git a/kiwi/builder/filesystem.py b/kiwi/builder/filesystem.py index 1bc53a67479..34b6b42d0f4 100644 --- a/kiwi/builder/filesystem.py +++ b/kiwi/builder/filesystem.py @@ -89,7 +89,7 @@ def __init__( self.blocksize = xml_state.build_type.get_target_blocksize() self.filesystem_setup = FileSystemSetup(xml_state, root_dir) self.filesystems_no_device_node = [ - 'squashfs' + 'squashfs', 'erofs' ] self.luks = xml_state.get_luks_credentials() self.result = Result(xml_state) diff --git a/kiwi/builder/live.py b/kiwi/builder/live.py index 9be04d0a14d..c4dc7e988c0 100644 --- a/kiwi/builder/live.py +++ b/kiwi/builder/live.py @@ -246,7 +246,7 @@ def create(self) -> Result: filesystem_setup = FileSystemSetup( self.xml_state, self.root_dir ) - if root_filesystem != 'squashfs': + if root_filesystem not in ['squashfs', 'erofs']: # Create a filesystem image of the specified type # and put it into a SquashFS container root_image = Temporary().new_file() @@ -302,12 +302,15 @@ def create(self) -> Result: else: # Put the root filesystem into SquashFS directly with FileSystem.new( - name='squashfs', + name=root_filesystem, device_provider=DeviceProvider(), root_dir=self.root_dir + os.sep, custom_args={ 'compression': self.xml_state.build_type.get_squashfscompression() + } if root_filesystem == 'squashfs' else { + 'compression': + self.xml_state.build_type.get_erofscompression() } ) as live_container_image: container_image = Temporary().new_file() @@ -316,6 +319,12 @@ def create(self) -> Result: ) Path.create(self.media_dir.name + '/LiveOS') os.chmod(container_image.name, 0o644) + # Note: we keep the filename of the read-only image as it is + # even if another read-only filesystem not matching this + # filename is used. This is because the following filename + # is also used in the initrd code for the kiwi-live and + # dmsquash dracut modules. The name can be overwritten + # with the rd.live.squashimg boot option though. shutil.copy( container_image.name, self.media_dir.name + '/LiveOS/squashfs.img' diff --git a/kiwi/defaults.py b/kiwi/defaults.py index 0deb1707c5f..7539f5c91a9 100644 --- a/kiwi/defaults.py +++ b/kiwi/defaults.py @@ -1523,7 +1523,7 @@ def get_filesystem_image_types(): """ return [ 'ext2', 'ext3', 'ext4', 'btrfs', 'squashfs', - 'xfs', 'fat16', 'fat32' + 'xfs', 'fat16', 'fat32', 'erofs' ] @staticmethod diff --git a/kiwi/filesystem/__init__.py b/kiwi/filesystem/__init__.py index 067048cc32f..3adfbc3d977 100644 --- a/kiwi/filesystem/__init__.py +++ b/kiwi/filesystem/__init__.py @@ -54,7 +54,8 @@ def new( 'fat16': 'Fat16', 'fat32': 'Fat32', 'squashfs': 'SquashFs', - 'swap': 'Swap' + 'swap': 'Swap', + 'erofs': 'EroFs' } try: filesystem = importlib.import_module( diff --git a/kiwi/filesystem/erofs.py b/kiwi/filesystem/erofs.py new file mode 100644 index 00000000000..1858d1b4064 --- /dev/null +++ b/kiwi/filesystem/erofs.py @@ -0,0 +1,60 @@ +# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. All rights reserved. +# +# This file is part of kiwi. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kiwi. If not, see +# +from typing import List + +# project +from kiwi.filesystem.base import FileSystemBase +from kiwi.command import Command + + +class FileSystemEroFs(FileSystemBase): + """ + **Implements creation of erofs filesystem** + """ + def create_on_file( + self, filename, label: str = None, exclude: List[str] = None + ): + """ + Create erofs filesystem from data tree + + :param string filename: result file path name + :param string label: volume label + :param list exclude: list of exclude dirs/files + """ + self.filename = filename + exclude_options = [] + compression = self.custom_args.get('compression') + if compression: + self.custom_args['create_options'].append('-z') + self.custom_args['create_options'].append(compression) + + if exclude: + for item in exclude: + exclude_options.append(f'--exclude-regex={item}') + + if label: + self.custom_args['create_options'].append('-L') + self.custom_args['create_options'].append(label) + + Command.run( + [ + 'mkfs.erofs' + ] + self.custom_args['create_options'] + exclude_options + [ + self.filename, self.root_dir + ] + ) diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc index cb39f22eaf8..7aa763fde03 100644 --- a/kiwi/schema/kiwi.rnc +++ b/kiwi/schema/kiwi.rnc @@ -1671,7 +1671,7 @@ div { k.type.filesystem.attribute = ## Specifies the root filesystem type attribute filesystem { - "btrfs" | "ext2" | "ext3" | "ext4" | "squashfs" | "xfs" + "btrfs" | "ext2" | "ext3" | "ext4" | "squashfs" | "erofs" | "xfs" } >> sch:pattern [ id = "filesystem" is-a = "image_type" sch:param [ name = "attr" value = "filesystem" ] @@ -1682,6 +1682,13 @@ div { sch:param [ name = "attr" value = "filesystem" ] sch:param [ name = "types" value = "oem" ] ] + k.type.erofscompression.attribute = + ## Specifies the compression type for erofs + attribute erofscompression { text } + >> sch:pattern [ id = "erofscompression" is-a = "image_type" + sch:param [ name = "attr" value = "erofscompression" ] + sch:param [ name = "types" value = "oem pxe kis iso erofs" ] + ] k.type.squashfscompression.attribute = ## Specifies the compression type for mksquashfs attribute squashfscompression { @@ -1958,7 +1965,7 @@ div { ## Specifies the image type attribute image { "btrfs" | "cpio" | "docker" | "ext2" | "ext3" | - "ext4" | "iso" | "oem" | "pxe" | "kis" | "squashfs" | "tbz" | + "ext4" | "iso" | "oem" | "pxe" | "kis" | "squashfs" | "erofs" | "tbz" | "xfs" | "oci" | "appx" | "enclave" } >> sch:pattern [ @@ -2284,6 +2291,7 @@ div { k.type.fsmountoptions.attribute? & k.type.fscreateoptions.attribute? & k.type.squashfscompression.attribute? & + k.type.erofscompression.attribute? & k.type.gcelicense.attribute? & k.type.hybridpersistent.attribute? & k.type.hybridpersistent_filesystem.attribute? & diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng index 9988bac5f4c..8857a1fc595 100644 --- a/kiwi/schema/kiwi.rng +++ b/kiwi/schema/kiwi.rng @@ -2435,6 +2435,7 @@ structure ext3 ext4 squashfs + erofs xfs @@ -2447,6 +2448,15 @@ structure + + + Specifies the compression type for erofs + + + + + + Specifies the compression type for mksquashfs @@ -2822,6 +2832,7 @@ initrd architecture. pxe kis squashfs + erofs tbz xfs oci @@ -3310,6 +3321,9 @@ kiwi-ng result bundle ... + + + diff --git a/kiwi/xml_parse.py b/kiwi/xml_parse.py index 7a7bae03205..83c97e8daa6 100644 --- a/kiwi/xml_parse.py +++ b/kiwi/xml_parse.py @@ -3094,7 +3094,7 @@ class type_(GeneratedsSuper): """The Image Type of the Logical Extend""" subclass = None superclass = None - def __init__(self, boot=None, bootfilesystem=None, firmware=None, bootkernel=None, bootpartition=None, bootpartsize=None, efipartsize=None, efifatimagesize=None, eficsm=None, efiparttable=None, dosparttable_extended_layout=None, bootprofile=None, btrfs_quota_groups=None, btrfs_root_is_snapshot=None, btrfs_root_is_subvolume=None, btrfs_set_default_volume=None, btrfs_root_is_readonly_snapshot=None, compressed=None, devicepersistency=None, editbootconfig=None, editbootinstall=None, filesystem=None, flags=None, enclave_format=None, format=None, formatoptions=None, fsmountoptions=None, fscreateoptions=None, squashfscompression=None, gcelicense=None, hybridpersistent=None, hybridpersistent_filesystem=None, gpt_hybrid_mbr=None, force_mbr=None, initrd_system=None, image=None, metadata_path=None, installboot=None, install_continue_on_timeout=None, installprovidefailsafe=None, installiso=None, installstick=None, installpxe=None, mediacheck=None, kernelcmdline=None, luks=None, luks_version=None, luksOS=None, luks_randomize=None, luks_pbkdf=None, mdraid=None, overlayroot=None, overlayroot_write_partition=None, overlayroot_readonly_partsize=None, verity_blocks=None, embed_verity_metadata=None, standalone_integrity=None, embed_integrity_metadata=None, integrity_legacy_hmac=None, integrity_metadata_key_description=None, integrity_keyfile=None, primary=None, ramonly=None, rootfs_label=None, spare_part=None, spare_part_mountpoint=None, spare_part_fs=None, spare_part_fs_attributes=None, spare_part_is_last=None, target_blocksize=None, target_removable=None, selinux_policy=None, vga=None, vhdfixedtag=None, volid=None, application_id=None, wwid_wait_timeout=None, derived_from=None, delta_root=None, ensure_empty_tmpdirs=None, xen_server=None, publisher=None, disk_start_sector=None, root_clone=None, boot_clone=None, bundle_format=None, bootloader=None, containerconfig=None, machine=None, oemconfig=None, size=None, systemdisk=None, partitions=None, vagrantconfig=None, installmedia=None, luksformat=None): + def __init__(self, boot=None, bootfilesystem=None, firmware=None, bootkernel=None, bootpartition=None, bootpartsize=None, efipartsize=None, efifatimagesize=None, eficsm=None, efiparttable=None, dosparttable_extended_layout=None, bootprofile=None, btrfs_quota_groups=None, btrfs_root_is_snapshot=None, btrfs_root_is_subvolume=None, btrfs_set_default_volume=None, btrfs_root_is_readonly_snapshot=None, compressed=None, devicepersistency=None, editbootconfig=None, editbootinstall=None, filesystem=None, flags=None, enclave_format=None, format=None, formatoptions=None, fsmountoptions=None, fscreateoptions=None, squashfscompression=None, erofscompression=None, gcelicense=None, hybridpersistent=None, hybridpersistent_filesystem=None, gpt_hybrid_mbr=None, force_mbr=None, initrd_system=None, image=None, metadata_path=None, installboot=None, install_continue_on_timeout=None, installprovidefailsafe=None, installiso=None, installstick=None, installpxe=None, mediacheck=None, kernelcmdline=None, luks=None, luks_version=None, luksOS=None, luks_randomize=None, luks_pbkdf=None, mdraid=None, overlayroot=None, overlayroot_write_partition=None, overlayroot_readonly_partsize=None, verity_blocks=None, embed_verity_metadata=None, standalone_integrity=None, embed_integrity_metadata=None, integrity_legacy_hmac=None, integrity_metadata_key_description=None, integrity_keyfile=None, primary=None, ramonly=None, rootfs_label=None, spare_part=None, spare_part_mountpoint=None, spare_part_fs=None, spare_part_fs_attributes=None, spare_part_is_last=None, target_blocksize=None, target_removable=None, selinux_policy=None, vga=None, vhdfixedtag=None, volid=None, application_id=None, wwid_wait_timeout=None, derived_from=None, delta_root=None, ensure_empty_tmpdirs=None, xen_server=None, publisher=None, disk_start_sector=None, root_clone=None, boot_clone=None, bundle_format=None, bootloader=None, containerconfig=None, machine=None, oemconfig=None, size=None, systemdisk=None, partitions=None, vagrantconfig=None, installmedia=None, luksformat=None): self.original_tagname_ = None self.boot = _cast(None, boot) self.bootfilesystem = _cast(None, bootfilesystem) @@ -3125,6 +3125,7 @@ def __init__(self, boot=None, bootfilesystem=None, firmware=None, bootkernel=Non self.fsmountoptions = _cast(None, fsmountoptions) self.fscreateoptions = _cast(None, fscreateoptions) self.squashfscompression = _cast(None, squashfscompression) + self.erofscompression = _cast(None, erofscompression) self.gcelicense = _cast(None, gcelicense) self.hybridpersistent = _cast(bool, hybridpersistent) self.hybridpersistent_filesystem = _cast(None, hybridpersistent_filesystem) @@ -3341,6 +3342,8 @@ def get_fscreateoptions(self): return self.fscreateoptions def set_fscreateoptions(self, fscreateoptions): self.fscreateoptions = fscreateoptions def get_squashfscompression(self): return self.squashfscompression def set_squashfscompression(self, squashfscompression): self.squashfscompression = squashfscompression + def get_erofscompression(self): return self.erofscompression + def set_erofscompression(self, erofscompression): self.erofscompression = erofscompression def get_gcelicense(self): return self.gcelicense def set_gcelicense(self, gcelicense): self.gcelicense = gcelicense def get_hybridpersistent(self): return self.hybridpersistent @@ -3629,6 +3632,9 @@ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_=' if self.squashfscompression is not None and 'squashfscompression' not in already_processed: already_processed.add('squashfscompression') outfile.write(' squashfscompression=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.squashfscompression), input_name='squashfscompression')), )) + if self.erofscompression is not None and 'erofscompression' not in already_processed: + already_processed.add('erofscompression') + outfile.write(' erofscompression=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.erofscompression), input_name='erofscompression')), )) if self.gcelicense is not None and 'gcelicense' not in already_processed: already_processed.add('gcelicense') outfile.write(' gcelicense=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.gcelicense), input_name='gcelicense')), )) @@ -4018,6 +4024,10 @@ def buildAttributes(self, node, attrs, already_processed): already_processed.add('squashfscompression') self.squashfscompression = value self.squashfscompression = ' '.join(self.squashfscompression.split()) + value = find_attr_value_('erofscompression', node) + if value is not None and 'erofscompression' not in already_processed: + already_processed.add('erofscompression') + self.erofscompression = value value = find_attr_value_('gcelicense', node) if value is not None and 'gcelicense' not in already_processed: already_processed.add('gcelicense') diff --git a/package/python-kiwi-spec-template b/package/python-kiwi-spec-template index 40f0425c728..e4561ecd974 100644 --- a/package/python-kiwi-spec-template +++ b/package/python-kiwi-spec-template @@ -291,10 +291,17 @@ Provides: kiwi-filesystem:ext3 Provides: kiwi-filesystem:ext4 Provides: kiwi-filesystem:squashfs Provides: kiwi-filesystem:xfs +%if ! (0%{?suse_version} && 0%{?suse_version} < 1600) +Provides: kiwi-filesystem:erofs +Provides: kiwi-image:erofs +%endif %endif Requires: dosfstools Requires: e2fsprogs Requires: xfsprogs +%if ! (0%{?suse_version} && 0%{?suse_version} < 1600) +Requires: erofs-utils +%endif %if 0%{?suse_version} Requires: btrfsprogs %else diff --git a/test/unit/filesystem/erofs_test.py b/test/unit/filesystem/erofs_test.py new file mode 100644 index 00000000000..bc91f232051 --- /dev/null +++ b/test/unit/filesystem/erofs_test.py @@ -0,0 +1,43 @@ +from unittest.mock import patch + +import unittest.mock as mock + +from kiwi.defaults import Defaults +from kiwi.filesystem.erofs import FileSystemEroFs + + +class TestFileSystemEroFs: + @patch('os.path.exists') + def setup(self, mock_exists): + mock_exists.return_value = True + self.erofs = FileSystemEroFs( + mock.Mock(), 'root_dir', + custom_args={'compression': 'zstd,level=21'} + ) + + @patch('os.path.exists') + def setup_method(self, cls, mock_exists): + self.setup() + + @patch('kiwi.filesystem.erofs.Command.run') + def test_create_on_file(self, mock_command): + Defaults.set_platform_name('x86_64') + self.erofs.create_on_file('myimage', 'label') + mock_command.assert_called_once_with( + [ + 'mkfs.erofs', '-z', 'zstd,level=21', + '-L', 'label', 'myimage', 'root_dir' + ] + ) + + @patch('kiwi.filesystem.erofs.Command.run') + def test_create_on_file_exclude_data(self, mock_command): + Defaults.set_platform_name('x86_64') + self.erofs.create_on_file('myimage', 'label', ['foo']) + mock_command.assert_called_once_with( + [ + 'mkfs.erofs', '-z', 'zstd,level=21', + '-L', 'label', '--exclude-regex=foo', + 'myimage', 'root_dir' + ] + )