From 05b75c042fe93741c0c17702efb9672ebaab72f1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Jun 2026 14:14:25 +0000
Subject: [PATCH 1/6] Replace sgdisk with sfdisk
---
.../59kiwi-lib/kiwi-partitions-lib.sh | 74 ++++++-
dracut/modules.d/59kiwi-lib/module-setup.sh | 2 +-
kiwi/bootloader/config/systemd_boot.py | 13 +-
kiwi/config/strip.xml | 1 -
kiwi/partitioner/gpt.py | 186 +++++++++++++-----
kiwi/storage/disk.py | 13 +-
.../bootloader/config/systemd_boot_test.py | 2 +-
test/unit/partitioner/gpt_test.py | 132 +++++++++----
test/unit/storage/disk_test.py | 6 +-
test/unit/tempfile | 1 +
10 files changed, 326 insertions(+), 104 deletions(-)
create mode 100644 test/unit/tempfile
diff --git a/dracut/modules.d/59kiwi-lib/kiwi-partitions-lib.sh b/dracut/modules.d/59kiwi-lib/kiwi-partitions-lib.sh
index f8cb9d5f743..e608721f057 100644
--- a/dracut/modules.d/59kiwi-lib/kiwi-partitions-lib.sh
+++ b/dracut/modules.d/59kiwi-lib/kiwi-partitions-lib.sh
@@ -90,7 +90,7 @@ function create_msdos_partitions {
function create_gpt_partitions {
# """
- # create partitions using sgdisk (gpt table)
+ # create partitions using sfdisk (gpt table)
# """
local disk_device=$1
local partition_setup=$2
@@ -103,7 +103,7 @@ function create_gpt_partitions {
# put partition setup in a command list(cmd_list)
for cmd in ${partition_setup};do
- # default values in sgdisk are used if 0 is provided
+ # default values in sfdisk are used if 0 is provided
cmd=$(echo "${cmd}" | tr . 0)
cmd_list[$index]=${cmd}
index=$((index + 1))
@@ -117,7 +117,7 @@ function create_gpt_partitions {
# delete a partition...
partid=${cmd_list[$index + 1]}
set_device_lock "${disk_device}" \
- sgdisk --delete "${partid}" "${disk_device}"
+ sfdisk --force --delete "${disk_device}" "${partid}"
;;
"n")
# create a partition...
@@ -125,17 +125,27 @@ function create_gpt_partitions {
partid=${cmd_list[$index + 2]}
part_size_start=${cmd_list[$index + 3]}
part_size_end=${cmd_list[$index + 4]}
+ if [ "${part_size_end}" = "0" ];then
+ part_size=+
+ else
+ part_size=$((part_size_end - part_size_start + 1))
+ fi
+ if [ "${part_size_start}" = "0" ];then
+ printf 'size=%s, name="%s"\n' \
+ "${part_size}" "${part_name}" > /tmp/sfdisk.in
+ else
+ printf 'start=%s, size=%s, name="%s"\n' \
+ "${part_size_start}" "${part_size}" "${part_name}" > /tmp/sfdisk.in
+ fi
set_device_lock "${disk_device}" \
- sgdisk --new "${partid}:${part_size_start}:${part_size_end}" "${disk_device}"
- set_device_lock "${disk_device}" \
- sgdisk --change-name "${partid}:${part_name}" "${disk_device}"
+ sfdisk --force -N "${partid}" "${disk_device}" < /tmp/sfdisk.in
;;
"t")
# change a partition type...
part_type=${cmd_list[$index + 2]}
partid=${cmd_list[$index + 1]}
set_device_lock "${disk_device}" \
- sgdisk --typecode "${partid}:$(_to_guid "${part_type}")" "${disk_device}"
+ sfdisk --part-type "${disk_device}" "${partid}" "$(_to_guid "${part_type}")"
;;
esac
index=$((index + 1))
@@ -417,15 +427,29 @@ function activate_boot_partition {
function create_hybrid_gpt {
local disk_device=$1
local partition_count
+ local partition_id
+ local partition_start
+ local partition_size
+ local partition_type
udev_pending
partition_count=$(lsblk -r -o NAME,TYPE "${disk_device}" | grep -c part)
if [ "${partition_count}" -gt 3 ]; then
# The max number of partitions to embed is 3
- # see man sgdisk for details
+ # see man sfdisk for details
partition_count=3
fi
+ : > /tmp/sfdisk.in
+ for partition_id in $(seq 1 "${partition_count}");do
+ partition_start=$(_get_gpt_partition_start_sector "${disk_device}" "${partition_id}")
+ partition_size=$(_get_gpt_partition_size "${disk_device}" "${partition_id}")
+ partition_type=$(_get_gpt_partition_mbr_type "${disk_device}" "${partition_id}")
+ printf '%s : start=%s, size=%s, type=%s\n' \
+ "$(get_partition_node_name "${disk_device}" "${partition_id}")" \
+ "${partition_start}" "${partition_size}" "${partition_type}" \
+ >> /tmp/sfdisk.in
+ done
if ! set_device_lock "${disk_device}" \
- sgdisk -h "$(seq -s : 1 "${partition_count}")" "${disk_device}"
+ sfdisk --label-nested=mbr "${disk_device}" < /tmp/sfdisk.in
then
die "Failed to create hybrid GPT/MBR !"
fi
@@ -515,6 +539,38 @@ function _get_msdos_partition_start_sector {
grep "${partid} :" | cut -f1 -d, | cut -f2 -d= | tr -d " "
}
+function _get_gpt_partition_start_sector {
+ local disk_device=$1
+ local partid=$2
+ sfdisk --dump "${disk_device}" |\
+ grep "${partid} :" | cut -f1 -d, | cut -f2 -d= | tr -d " "
+}
+
+function _get_gpt_partition_size {
+ local disk_device=$1
+ local partid=$2
+ sfdisk --dump "${disk_device}" |\
+ grep "${partid} :" | cut -f2 -d, | cut -f2 -d= | tr -d " "
+}
+
+function _get_gpt_partition_mbr_type {
+ local disk_device=$1
+ local partid=$2
+ local part_type
+ part_type=$(sfdisk --part-type "${disk_device}" "${partid}")
+ if [ "${part_type}" = "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F" ];then
+ echo 82
+ elif [ "${part_type}" = "E6D6D379-F507-44C2-A23C-238F2A3DF928" ];then
+ echo 8e
+ elif [ "${part_type}" = "A19D880F-05FC-4D3B-A006-743F0F84911E" ];then
+ echo fd
+ elif [ "${part_type}" = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" ];then
+ echo 41
+ else
+ echo 83
+ fi
+}
+
function _to_guid {
# """
# convert two digit partition id to guid id
diff --git a/dracut/modules.d/59kiwi-lib/module-setup.sh b/dracut/modules.d/59kiwi-lib/module-setup.sh
index 8a3b890cf81..880c7ee46b3 100755
--- a/dracut/modules.d/59kiwi-lib/module-setup.sh
+++ b/dracut/modules.d/59kiwi-lib/module-setup.sh
@@ -17,7 +17,7 @@ install() {
inst_multiple \
blkid blockdev dd mkdir rmdir env \
grep cut tail head tr bc true false mountpoint \
- basename sfdisk sgdisk mkswap readlink lsblk \
+ basename sfdisk mkswap readlink lsblk \
btrfs xfs_growfs resize2fs \
e2fsck btrfsck xfs_repair \
vgs vgchange lvextend lvcreate lvresize pvresize \
diff --git a/kiwi/bootloader/config/systemd_boot.py b/kiwi/bootloader/config/systemd_boot.py
index d0d6df83619..507c275e33c 100644
--- a/kiwi/bootloader/config/systemd_boot.py
+++ b/kiwi/bootloader/config/systemd_boot.py
@@ -18,6 +18,7 @@
import os
import glob
import logging
+import shlex
from string import Template
from contextlib import ExitStack
from typing import Dict
@@ -137,7 +138,17 @@ def _create_embedded_fat_efi_image(self, path: str):
['qemu-img', 'create', path, f'{fat_image_mbsize}M']
)
Command.run(
- ['sgdisk', '-n', ':1.0', '-t', '1:EF00', path]
+ [
+ 'bash', '-c', ' '.join(
+ [
+ 'printf',
+ shlex.quote('label: gpt\nsize=+, type=U\n'),
+ '|',
+ 'sfdisk',
+ shlex.quote(path)
+ ]
+ )
+ ]
)
with LoopDevice(path) as loop_provider:
loop_provider.create(overwrite=False)
diff --git a/kiwi/config/strip.xml b/kiwi/config/strip.xml
index f00320adfc2..7ca3cf2ec5d 100644
--- a/kiwi/config/strip.xml
+++ b/kiwi/config/strip.xml
@@ -215,7 +215,6 @@
-
diff --git a/kiwi/partitioner/gpt.py b/kiwi/partitioner/gpt.py
index 8590f95da18..0426c019265 100644
--- a/kiwi/partitioner/gpt.py
+++ b/kiwi/partitioner/gpt.py
@@ -15,7 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with kiwi. If not, see
#
+import json
import logging
+import re
+import shlex
from typing import (
List, Optional
)
@@ -23,6 +26,7 @@
# project
from kiwi.command import Command
from kiwi.partitioner.base import PartitionerBase
+from kiwi.utils.temporary import Temporary
from kiwi.exceptions import (
KiwiPartitionerGptFlagError
@@ -39,7 +43,7 @@ def post_init(self) -> None:
"""
Post initialization method
- Setup gdisk partition type/flag map
+ Setup sfdisk GPT partition type/flag map
"""
self.flag_map = {
'f.active': None,
@@ -52,6 +56,15 @@ def post_init(self) -> None:
't.prep': '4100'
}
self.partition_map: dict[int, int] = {}
+ self.type_guid_map = {
+ 'EF02': '21686148-6449-6E6F-744E-656564454649',
+ '8300': '0FC63DAF-8483-4772-8E79-3D69D8477DE4',
+ '8200': '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F',
+ '8E00': 'E6D6D379-F507-44C2-A23C-238F2A3DF928',
+ 'FD00': 'A19D880F-05FC-4D3B-A006-743F0F84911E',
+ 'EF00': 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B',
+ '4100': '9E1A2D38-C612-4316-AA26-8B49521E5A8B'
+ }
def create(
self, name: str, mbsize: int, type_name: str, flags: List[str] = None,
@@ -73,24 +86,19 @@ def create(
self.partition_count += 1
self.partition_map[self.partition_count] = self.partition_id
if mbsize == 'all_free':
- partition_end = '0'
+ partition_size = '+'
else:
- partition_end = '+' + format(mbsize) + 'M'
+ partition_size = format(mbsize) + 'MiB'
if self.partition_count > 1 or not self.start_sector:
- # A start sector value of 0 specifies the default value
- # defined in sgdisk
+ # A start sector value of 0 specifies the default value
+ # defined in sfdisk
self.start_sector = 0
- Command.run(
- [
- 'sgdisk', '-n', ':'.join(
- [
- format(self.partition_id),
- format(self.start_sector),
- partition_end
- ]
- ), '-c', ':'.join([format(self.partition_id), name]),
- self.disk_device
- ]
+ partition_fields = [f'size={partition_size}', f'name={json.dumps(name)}']
+ if self.start_sector:
+ partition_fields.insert(0, f'start={self.start_sector}')
+ self._call_sfdisk(
+ [', '.join(partition_fields)],
+ ['--force', '-N', format(self.partition_id)]
)
self.set_flag(self.partition_id, type_name)
if flags:
@@ -111,14 +119,9 @@ def set_flag(self, partition_id: int, flag_name: str) -> None:
if self.flag_map[flag_name]:
Command.run(
[
- 'sgdisk', '-t',
- ':'.join(
- [
- format(partition_id),
- format(self.flag_map[flag_name])
- ]
- ),
- self.disk_device
+ 'sfdisk', '--part-type', self.disk_device,
+ format(partition_id),
+ self._to_guid(self.flag_map[flag_name])
]
)
else:
@@ -133,9 +136,8 @@ def set_uuid(self, partition_id: int, uuid: str) -> None:
"""
Command.run(
[
- 'sgdisk',
- '--typecode', f'{partition_id}:{uuid}',
- self.disk_device
+ 'sfdisk', '--part-type', self.disk_device,
+ format(partition_id), uuid
]
)
@@ -147,7 +149,7 @@ def set_hybrid_mbr(self) -> None:
partition_number_to_embed = self.partition_count
if partition_number_to_embed > 3:
# the max number of partitions to embed is 3
- # for details see man sgdisk
+ # for details see man sfdisk
log.debug(
'maximum number of GPT hybrid MBR partitions is 3, got %d',
partition_number_to_embed
@@ -159,34 +161,44 @@ def set_hybrid_mbr(self) -> None:
)
for number in range(1, partition_number_to_embed + 1):
if self.partition_map.get(number):
- partition_ids.append(format(self.partition_map[number]))
- Command.run(
- ['sgdisk', '-h', ':'.join(partition_ids), self.disk_device]
- )
+ partition_id = self.partition_map[number]
+ start, size = self._get_partition_geometry(partition_id)
+ partition_type = self._get_partition_type(partition_id)
+ partition_ids.append(
+ '{device}{suffix}{number} : start={start}, '
+ 'size={size}, type={type}'.format(
+ device=self.disk_device,
+ suffix='p' if self.disk_device[-1].isdigit() else '',
+ number=number,
+ start=start,
+ size=size,
+ type=self._to_mbr_type(partition_type)
+ )
+ )
+ self._call_sfdisk(partition_ids, ['--label-nested=mbr'])
def set_mbr(self) -> None:
"""
Turn partition table into MBR (msdos table)
"""
partition_ids = []
- efi_partition_number = None
for number in range(1, self.partition_id + 1):
if self.partition_map.get(number):
- partition_info = Command.run(
- [
- 'sgdisk', '-i={0}'.format(self.partition_map[number]),
- self.disk_device
- ]
+ partition_id = self.partition_map[number]
+ start, size = self._get_partition_geometry(partition_id)
+ partition_type = self._get_partition_type(partition_id)
+ partition_ids.append(
+ '{device}{suffix}{number} : start={start}, '
+ 'size={size}, type={type}'.format(
+ device=self.disk_device,
+ suffix='p' if self.disk_device[-1].isdigit() else '',
+ number=number,
+ start=start,
+ size=size,
+ type=self._to_mbr_type(partition_type)
+ )
)
- if '(EFI System)' in partition_info.output:
- efi_partition_number = self.partition_map[number]
- partition_ids.append(format(self.partition_map[number]))
- if efi_partition_number:
- # turn former EFI partition into standard linux partition
- self.set_flag(efi_partition_number, 't.linux')
- Command.run(
- ['sgdisk', '-m', ':'.join(partition_ids), self.disk_device]
- )
+ self._call_sfdisk(['label: dos'] + partition_ids)
def resize_table(self, entries: int = 128) -> None:
"""
@@ -194,9 +206,83 @@ def resize_table(self, entries: int = 128) -> None:
:param int entries: number of default entries
"""
- Command.run(
+ partition_table = Command.run(
+ ['sfdisk', '--dump', self.disk_device]
+ ).output.splitlines()
+ table_length = f'table-length: {entries}'
+ updated_table = []
+ insert_index = 0
+ for line in partition_table:
+ if line.startswith('table-length:'):
+ updated_table.append(table_length)
+ insert_index = -1
+ continue
+ updated_table.append(line)
+ if line.startswith('last-lba:'):
+ insert_index = len(updated_table)
+ if insert_index >= 0:
+ updated_table.insert(insert_index, table_length)
+ self._call_sfdisk(updated_table)
+
+ def _call_sfdisk(
+ self, partition_setup: List[str], options: List[str] = None
+ ) -> None:
+ sfdisk_input = Temporary().new_file()
+ with open(sfdisk_input.name, 'w') as partition:
+ partition.write('\n'.join(partition_setup) + '\n')
+ sfdisk_command = ['sfdisk']
+ sfdisk_command.extend(
+ [shlex.quote(option) for option in (options or [])]
+ )
+ sfdisk_command.extend(
+ [shlex.quote(self.disk_device), '<', shlex.quote(sfdisk_input.name)]
+ )
+ command = ' '.join(
+ sfdisk_command
+ )
+ Command.run(['bash', '-c', command])
+
+ def _get_partition_geometry(self, partition_id: int) -> tuple[str, str]:
+ partition_info = Command.run(
+ ['sfdisk', '--dump', self.disk_device]
+ ).output
+ partition_node = self._get_partition_node(partition_id)
+ partition_match = re.search(
+ r'^{partition_node}\s+:\s+start=\s*(\d+), size=\s*(\d+)'.format(
+ partition_node=re.escape(partition_node)
+ ),
+ partition_info,
+ re.MULTILINE
+ )
+ if not partition_match:
+ raise RuntimeError(f'Failed to locate partition {partition_id}')
+ return partition_match.group(1), partition_match.group(2)
+
+ def _get_partition_type(self, partition_id: int) -> str:
+ return Command.run(
+ ['sfdisk', '--part-type', self.disk_device, format(partition_id)]
+ ).output.strip()
+
+ def _to_guid(self, partition_type: str) -> str:
+ return self.type_guid_map.get(partition_type, partition_type)
+
+ def _to_mbr_type(self, partition_type: str) -> str:
+ partition_type = partition_type.upper()
+ if partition_type == self.type_guid_map['8200']:
+ return '82'
+ if partition_type == self.type_guid_map['8E00']:
+ return '8e'
+ if partition_type == self.type_guid_map['FD00']:
+ return 'fd'
+ if partition_type == self.type_guid_map['4100']:
+ return '41'
+ return '83'
+
+ def _get_partition_node(self, partition_id: int) -> str:
+ return ''.join(
[
- 'sgdisk', '--resize-table', '{0}'.format(entries),
- self.disk_device
+ self.disk_device,
+ 'p' if self.disk_device[-1].isdigit() else '',
+ format(partition_id)
]
)
diff --git a/kiwi/storage/disk.py b/kiwi/storage/disk.py
index b6ddb4cac63..68b1fb83c2e 100644
--- a/kiwi/storage/disk.py
+++ b/kiwi/storage/disk.py
@@ -17,6 +17,7 @@
#
import os
import logging
+import shlex
from collections import OrderedDict
from typing import (
Dict, NamedTuple, Tuple, Optional
@@ -554,9 +555,19 @@ def wipe(self):
log.debug('potential fdasd errors were ignored')
else:
log.debug('Initialize %s disk', self.table_type)
+ sfdisk_input = Temporary().new_file()
+ label_type = 'dos' if self.table_type == 'msdos' else self.table_type
+ with open(sfdisk_input.name, 'w') as partition:
+ partition.write(f'label: {label_type}\n')
Command.run(
[
- 'sgdisk', '--zap-all', self.storage_provider.get_device()
+ 'bash', '-c', ' '.join(
+ [
+ 'sfdisk', '--wipe', 'always',
+ shlex.quote(self.storage_provider.get_device()),
+ '<', shlex.quote(sfdisk_input.name)
+ ]
+ )
]
)
diff --git a/test/unit/bootloader/config/systemd_boot_test.py b/test/unit/bootloader/config/systemd_boot_test.py
index bed3813bf65..81b0ad566ad 100644
--- a/test/unit/bootloader/config/systemd_boot_test.py
+++ b/test/unit/bootloader/config/systemd_boot_test.py
@@ -237,7 +237,7 @@ def test_create_embedded_fat_efi_image(
]
assert mock_Command_run.call_args_list == [
call(['qemu-img', 'create', 'ESP', '20M']),
- call(['sgdisk', '-n', ':1.0', '-t', '1:EF00', 'ESP']),
+ call(['bash', '-c', "printf 'label: gpt\nsize=+, type=U\n' | sfdisk ESP"]),
call(
['mkdosfs', '-n', 'BOOT', 'efi_device']
),
diff --git a/test/unit/partitioner/gpt_test.py b/test/unit/partitioner/gpt_test.py
index a32a380a166..093fc2ab30a 100644
--- a/test/unit/partitioner/gpt_test.py
+++ b/test/unit/partitioner/gpt_test.py
@@ -26,12 +26,12 @@ def setup(self):
def setup_method(self, cls):
self.setup()
- @patch('kiwi.partitioner.gpt.Command.run')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
@patch('kiwi.partitioner.gpt.PartitionerGpt.set_flag')
- def test_create(self, mock_flag, mock_command):
+ def test_create(self, mock_flag, mock_call_sfdisk):
self.partitioner.create('name', 100, 't.linux', ['t.csm'])
- mock_command.assert_called_once_with(
- ['sgdisk', '-n', '1:0:+100M', '-c', '1:name', '/dev/loop0']
+ mock_call_sfdisk.assert_called_once_with(
+ ['size=100MiB, name="name"'], ['--force', '-N', '1']
)
call = mock_flag.call_args_list[0]
assert mock_flag.call_args_list[0] == \
@@ -40,9 +40,9 @@ def test_create(self, mock_flag, mock_command):
assert mock_flag.call_args_list[1] == \
call(1, 't.csm')
- @patch('kiwi.partitioner.gpt.Command.run')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
@patch('kiwi.partitioner.gpt.PartitionerGpt.set_flag')
- def test_create_custom_start_sector(self, mock_flag, mock_command):
+ def test_create_custom_start_sector(self, mock_flag, mock_call_sfdisk):
disk_provider = Mock()
disk_provider.get_device = Mock(
return_value='/dev/loop0'
@@ -50,25 +50,27 @@ def test_create_custom_start_sector(self, mock_flag, mock_command):
partitioner = PartitionerGpt(disk_provider, 4096)
partitioner.create('name', 100, 't.linux', ['t.csm'])
partitioner.create('name', 100, 't.linux', ['t.csm'])
- mock_command.assert_has_calls([
- call([
- 'sgdisk', '-n', '1:4096:+100M', '-c', '1:name', '/dev/loop0'
- ]),
- call([
- 'sgdisk', '-n', '2:0:+100M', '-c', '2:name', '/dev/loop0'
- ])
+ mock_call_sfdisk.assert_has_calls([
+ call(
+ ['start=4096, size=100MiB, name="name"'],
+ ['--force', '-N', '1']
+ ),
+ call(
+ ['size=100MiB, name="name"'],
+ ['--force', '-N', '2']
+ )
])
assert mock_flag.call_args_list[0] == \
call(1, 't.linux')
assert mock_flag.call_args_list[1] == \
call(1, 't.csm')
- @patch('kiwi.partitioner.gpt.Command.run')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
@patch('kiwi.partitioner.gpt.PartitionerGpt.set_flag')
- def test_create_all_free(self, mock_flag, mock_command):
+ def test_create_all_free(self, mock_flag, mock_call_sfdisk):
self.partitioner.create('name', 'all_free', 't.linux')
- mock_command.assert_called_once_with(
- ['sgdisk', '-n', '1:0:0', '-c', '1:name', '/dev/loop0']
+ mock_call_sfdisk.assert_called_once_with(
+ ['size=+, name="name"'], ['--force', '-N', '1']
)
def test_set_flag_invalid(self):
@@ -79,15 +81,22 @@ def test_set_flag_invalid(self):
def test_set_flag(self, mock_command):
self.partitioner.set_flag(1, 't.csm')
mock_command.assert_called_once_with(
- ['sgdisk', '-t', '1:EF02', '/dev/loop0']
+ [
+ 'sfdisk', '--part-type', '/dev/loop0', '1',
+ '21686148-6449-6E6F-744E-656564454649'
+ ]
)
def test_set_flag_ignored(self):
with self._caplog.at_level(logging.WARNING):
self.partitioner.set_flag(1, 'f.active')
- @patch('kiwi.partitioner.gpt.Command.run')
- def test_set_hybrid_mbr(self, mock_command):
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_type')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_geometry')
+ def test_set_hybrid_mbr(
+ self, mock_geometry, mock_type, mock_call_sfdisk
+ ):
self.partitioner.partition_id = 5
self.partitioner.partition_count = 5
self.partitioner.partition_map = {
@@ -97,15 +106,22 @@ def test_set_hybrid_mbr(self, mock_command):
4: 4,
5: 5
}
+ mock_geometry.side_effect = [('2048', '4096')] * 3
+ mock_type.return_value = '0FC63DAF-8483-4772-8E79-3D69D8477DE4'
self.partitioner.set_hybrid_mbr()
- mock_command.assert_called_once_with(
- ['sgdisk', '-h', '1:2:3', '/dev/loop0']
+ mock_call_sfdisk.assert_called_once_with(
+ [
+ '/dev/loop0p1 : start=2048, size=4096, type=83',
+ '/dev/loop0p2 : start=2048, size=4096, type=83',
+ '/dev/loop0p3 : start=2048, size=4096, type=83'
+ ],
+ ['--label-nested=mbr']
)
- @patch('kiwi.partitioner.gpt.Command.run')
- def test_set_mbr(self, mock_command):
- command_output = Mock()
- command_output.output = '...(EFI System)'
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_type')
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_geometry')
+ def test_set_mbr(self, mock_geometry, mock_type, mock_call_sfdisk):
self.partitioner.partition_id = 4
self.partitioner.partition_count = 4
self.partitioner.partition_map = {
@@ -114,27 +130,67 @@ def test_set_mbr(self, mock_command):
3: 3,
4: 4
}
- mock_command.return_value = command_output
- self.partitioner.set_mbr()
- assert mock_command.call_args_list == [
- call(['sgdisk', '-i=1', '/dev/loop0']),
- call(['sgdisk', '-i=2', '/dev/loop0']),
- call(['sgdisk', '-i=3', '/dev/loop0']),
- call(['sgdisk', '-i=4', '/dev/loop0']),
- call(['sgdisk', '-t', '4:8300', '/dev/loop0']),
- call(['sgdisk', '-m', '1:2:3:4', '/dev/loop0'])
+ mock_geometry.side_effect = [
+ ('2048', '4096'),
+ ('6144', '4096'),
+ ('10240', '4096'),
+ ('14336', '4096')
+ ]
+ mock_type.side_effect = [
+ '0FC63DAF-8483-4772-8E79-3D69D8477DE4',
+ '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F',
+ 'E6D6D379-F507-44C2-A23C-238F2A3DF928',
+ 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B'
]
+ self.partitioner.set_mbr()
+ mock_call_sfdisk.assert_called_once_with(
+ [
+ 'label: dos',
+ '/dev/loop0p1 : start=2048, size=4096, type=83',
+ '/dev/loop0p2 : start=6144, size=4096, type=82',
+ '/dev/loop0p3 : start=10240, size=4096, type=8e',
+ '/dev/loop0p4 : start=14336, size=4096, type=83'
+ ]
+ )
+ @patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
@patch('kiwi.partitioner.gpt.Command.run')
- def test_resize_table(self, mock_command):
+ def test_resize_table(self, mock_command, mock_call_sfdisk):
+ command_output = Mock()
+ command_output.output = '\n'.join([
+ 'label: gpt',
+ 'label-id: ID',
+ 'device: /dev/loop0',
+ 'unit: sectors',
+ 'first-lba: 2048',
+ 'last-lba: 4096',
+ 'sector-size: 512',
+ '',
+ '/dev/loop0p1 : start=2048, size=1024, type=TYPE'
+ ])
+ mock_command.return_value = command_output
self.partitioner.resize_table(42)
mock_command.assert_called_once_with(
- ['sgdisk', '--resize-table', '42', '/dev/loop0']
+ ['sfdisk', '--dump', '/dev/loop0']
+ )
+ mock_call_sfdisk.assert_called_once_with(
+ [
+ 'label: gpt',
+ 'label-id: ID',
+ 'device: /dev/loop0',
+ 'unit: sectors',
+ 'first-lba: 2048',
+ 'last-lba: 4096',
+ 'table-length: 42',
+ 'sector-size: 512',
+ '',
+ '/dev/loop0p1 : start=2048, size=1024, type=TYPE'
+ ]
)
@patch('kiwi.partitioner.gpt.Command.run')
def test_set_uuid(self, mock_Command_run):
self.partitioner.set_uuid(42, 'ID')
mock_Command_run.assert_called_once_with(
- ['sgdisk', '--typecode', '42:ID', '/dev/loop0']
+ ['sfdisk', '--part-type', '/dev/loop0', '42', 'ID']
)
diff --git a/test/unit/storage/disk_test.py b/test/unit/storage/disk_test.py
index 863e0928825..a5e5523750c 100644
--- a/test/unit/storage/disk_test.py
+++ b/test/unit/storage/disk_test.py
@@ -374,11 +374,13 @@ def test_activate_boot_partition_is_prep_partition(self, mock_command):
self.disk.activate_boot_partition()
self.partitioner.set_flag(1, 'f.active')
+ @patch('kiwi.storage.disk.Temporary.new_file')
@patch('kiwi.storage.disk.Command.run')
- def test_wipe_gpt(self, mock_command):
+ def test_wipe_gpt(self, mock_command, mock_temp):
+ mock_temp.return_value = self.tempfile
self.disk.wipe()
mock_command.assert_called_once_with(
- ['sgdisk', '--zap-all', '/dev/loop0']
+ ['bash', '-c', 'sfdisk --wipe always /dev/loop0 < tempfile']
)
@patch('kiwi.storage.disk.Command.run')
diff --git a/test/unit/tempfile b/test/unit/tempfile
new file mode 100644
index 00000000000..e0b00807c5b
--- /dev/null
+++ b/test/unit/tempfile
@@ -0,0 +1 @@
+label: gpt
From 534120b9b022ebfa4f3a4b25a1857461fd2fb545 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?=
Date: Wed, 17 Jun 2026 16:48:53 +0200
Subject: [PATCH 2/6] Fixed static type checking
---
kiwi/partitioner/gpt.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kiwi/partitioner/gpt.py b/kiwi/partitioner/gpt.py
index 0426c019265..119b21126c0 100644
--- a/kiwi/partitioner/gpt.py
+++ b/kiwi/partitioner/gpt.py
@@ -121,7 +121,7 @@ def set_flag(self, partition_id: int, flag_name: str) -> None:
[
'sfdisk', '--part-type', self.disk_device,
format(partition_id),
- self._to_guid(self.flag_map[flag_name])
+ self._to_guid(format(self.flag_map[flag_name]))
]
)
else:
From d35467384dc953b1609281da146f90a1ba982770 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?=
Date: Wed, 17 Jun 2026 16:52:28 +0200
Subject: [PATCH 3/6] Update package spec
drop gdisk/gptfdisk packages from requirements
This Fixes #2804
---
kiwi/config/strip.xml | 1 +
package/python-kiwi-spec-template | 8 --------
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/kiwi/config/strip.xml b/kiwi/config/strip.xml
index 7ca3cf2ec5d..f00320adfc2 100644
--- a/kiwi/config/strip.xml
+++ b/kiwi/config/strip.xml
@@ -215,6 +215,7 @@
+
diff --git a/package/python-kiwi-spec-template b/package/python-kiwi-spec-template
index fe065299e9f..f5807f8b2af 100644
--- a/package/python-kiwi-spec-template
+++ b/package/python-kiwi-spec-template
@@ -359,11 +359,6 @@ Requires: kiwi-systemdeps-iso-media = %{version}-%{release}
Requires: binutils
Requires: glibc-gconv-modules-extra
%endif
-%if 0%{?suse_version}
-Requires: gptfdisk
-%else
-Requires: gdisk
-%endif
Requires: lvm2
Requires: kpartx
Requires: cryptsetup
@@ -512,15 +507,12 @@ Requires: btrfs-progs
%else
Recommends: btrfs-progs
%endif
-Requires: gdisk
Requires: dracut-network
%else
%if 0%{?debian} || 0%{?ubuntu}
Recommends: btrfs-tools
-Requires: gdisk
%else
Requires: btrfsprogs
-Requires: gptfdisk
%endif
%endif
Requires: coreutils
From d698efd4ae34f11cf815df0ea557ca2b9d459e25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?=
Date: Wed, 17 Jun 2026 18:42:31 +0200
Subject: [PATCH 4/6] Fixes by human review
Fix resize_table, invalid and unneeded lba header
Fix exception handling, use KIWIGeometryError instead of RuntimeError
Fix tests, all code must be covered by tests
---
kiwi/partitioner/gpt.py | 29 ++++-----
test/unit/partitioner/gpt_test.py | 97 ++++++++++++++++++++++++++-----
test/unit/storage/disk_test.py | 4 +-
test/unit/tempfile | 1 -
4 files changed, 99 insertions(+), 32 deletions(-)
delete mode 100644 test/unit/tempfile
diff --git a/kiwi/partitioner/gpt.py b/kiwi/partitioner/gpt.py
index 119b21126c0..3f030a1a94b 100644
--- a/kiwi/partitioner/gpt.py
+++ b/kiwi/partitioner/gpt.py
@@ -29,7 +29,8 @@
from kiwi.utils.temporary import Temporary
from kiwi.exceptions import (
- KiwiPartitionerGptFlagError
+ KiwiPartitionerGptFlagError,
+ KiwiDiskGeometryError
)
log = logging.getLogger('kiwi')
@@ -204,25 +205,17 @@ def resize_table(self, entries: int = 128) -> None:
"""
Resize partition table
- :param int entries: number of default entries
+ This is done by a dump/reload which automatically corrects
+ geometry differences in the table when sfdisk is used
+
+ :param int entries: Specify the maximal number of GPT partitions
"""
partition_table = Command.run(
['sfdisk', '--dump', self.disk_device]
).output.splitlines()
- table_length = f'table-length: {entries}'
- updated_table = []
- insert_index = 0
- for line in partition_table:
- if line.startswith('table-length:'):
- updated_table.append(table_length)
- insert_index = -1
- continue
- updated_table.append(line)
- if line.startswith('last-lba:'):
- insert_index = len(updated_table)
- if insert_index >= 0:
- updated_table.insert(insert_index, table_length)
- self._call_sfdisk(updated_table)
+ if entries != 128:
+ partition_table.insert(0, f'table-length: {entries}')
+ self._call_sfdisk(partition_table)
def _call_sfdisk(
self, partition_setup: List[str], options: List[str] = None
@@ -255,7 +248,9 @@ def _get_partition_geometry(self, partition_id: int) -> tuple[str, str]:
re.MULTILINE
)
if not partition_match:
- raise RuntimeError(f'Failed to locate partition {partition_id}')
+ raise KiwiDiskGeometryError(
+ f'Failed to locate partition {partition_id}'
+ )
return partition_match.group(1), partition_match.group(2)
def _get_partition_type(self, partition_id: int) -> str:
diff --git a/test/unit/partitioner/gpt_test.py b/test/unit/partitioner/gpt_test.py
index 093fc2ab30a..a71053af154 100644
--- a/test/unit/partitioner/gpt_test.py
+++ b/test/unit/partitioner/gpt_test.py
@@ -1,14 +1,18 @@
+import io
import logging
from unittest.mock import (
- patch, call, Mock
+ patch, call, Mock, MagicMock
)
from pytest import (
raises, fixture
)
from kiwi.partitioner.gpt import PartitionerGpt
-
-from kiwi.exceptions import KiwiPartitionerGptFlagError
+from kiwi.command import CommandT
+from kiwi.exceptions import (
+ KiwiPartitionerGptFlagError,
+ KiwiDiskGeometryError
+)
class TestPartitionerGpt:
@@ -94,7 +98,7 @@ def test_set_flag_ignored(self):
@patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
@patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_type')
@patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_geometry')
- def test_set_hybrid_mbr(
+ def test_set_hybrid_mbr_max_partitions_exceeded(
self, mock_geometry, mock_type, mock_call_sfdisk
):
self.partitioner.partition_id = 5
@@ -106,17 +110,84 @@ def test_set_hybrid_mbr(
4: 4,
5: 5
}
+
+ def type_call(args):
+ if args == 1:
+ return 'A19D880F-05FC-4D3B-A006-743F0F84911E'
+ else:
+ return '9E1A2D38-C612-4316-AA26-8B49521E5A8B'
+
+ mock_type.side_effect = type_call
+
mock_geometry.side_effect = [('2048', '4096')] * 3
- mock_type.return_value = '0FC63DAF-8483-4772-8E79-3D69D8477DE4'
self.partitioner.set_hybrid_mbr()
- mock_call_sfdisk.assert_called_once_with(
- [
- '/dev/loop0p1 : start=2048, size=4096, type=83',
- '/dev/loop0p2 : start=2048, size=4096, type=83',
- '/dev/loop0p3 : start=2048, size=4096, type=83'
- ],
- ['--label-nested=mbr']
+ assert len(mock_type.call_args_list) == 3
+
+ @patch('kiwi.partitioner.gpt.Command.run')
+ def test_set_hybrid_mbr_geometry_error(self, mock_Command_run):
+ self.partitioner.partition_id = 1
+ self.partitioner.partition_count = 1
+ self.partitioner.partition_map = {
+ 1: 1,
+ }
+ mock_Command_run.return_value = CommandT(
+ output='bogus',
+ error='',
+ returncode=0
)
+ with raises(KiwiDiskGeometryError):
+ self.partitioner.set_hybrid_mbr()
+
+ @patch('kiwi.partitioner.gpt.Command.run')
+ def test_set_hybrid_mbr(self, mock_Command_run):
+ self.partitioner.partition_id = 1
+ self.partitioner.partition_count = 1
+ self.partitioner.partition_map = {
+ 1: 1,
+ }
+
+ def command_call(args):
+ print(args)
+ if '--dump' in args:
+ return CommandT(
+ output='\n'.join(
+ [
+ 'label: gpt',
+ 'label-id: ID',
+ 'device: /dev/loop0',
+ 'unit: sectors',
+ 'first-lba: 2048',
+ 'last-lba: 4096',
+ 'sector-size: 512',
+ '',
+ '/dev/loop0p1 : start=2048, size=1024, type=TYPE'
+ ]
+ ),
+ error='',
+ returncode=0
+ )
+ elif '--part-type' in args:
+ return CommandT(
+ output='0FC63DAF-8483-4772-8E79-3D69D8477DE4\n',
+ error='',
+ returncode=0
+ )
+ else:
+ return CommandT(
+ output='',
+ error='',
+ returncode=0
+ )
+
+ mock_Command_run.side_effect = command_call
+
+ with patch('builtins.open', create=True) as mock_open:
+ mock_open.return_value = MagicMock(spec=io.IOBase)
+ file_handle = mock_open.return_value.__enter__.return_value
+ self.partitioner.set_hybrid_mbr()
+ file_handle.write.assert_called_once_with(
+ '/dev/loop0p1 : start=2048, size=1024, type=83\n'
+ )
@patch('kiwi.partitioner.gpt.PartitionerGpt._call_sfdisk')
@patch('kiwi.partitioner.gpt.PartitionerGpt._get_partition_type')
@@ -175,13 +246,13 @@ def test_resize_table(self, mock_command, mock_call_sfdisk):
)
mock_call_sfdisk.assert_called_once_with(
[
+ 'table-length: 42',
'label: gpt',
'label-id: ID',
'device: /dev/loop0',
'unit: sectors',
'first-lba: 2048',
'last-lba: 4096',
- 'table-length: 42',
'sector-size: 512',
'',
'/dev/loop0p1 : start=2048, size=1024, type=TYPE'
diff --git a/test/unit/storage/disk_test.py b/test/unit/storage/disk_test.py
index a5e5523750c..827eb5818a7 100644
--- a/test/unit/storage/disk_test.py
+++ b/test/unit/storage/disk_test.py
@@ -378,7 +378,9 @@ def test_activate_boot_partition_is_prep_partition(self, mock_command):
@patch('kiwi.storage.disk.Command.run')
def test_wipe_gpt(self, mock_command, mock_temp):
mock_temp.return_value = self.tempfile
- self.disk.wipe()
+ m_open = mock_open()
+ with patch('builtins.open', m_open, create=True):
+ self.disk.wipe()
mock_command.assert_called_once_with(
['bash', '-c', 'sfdisk --wipe always /dev/loop0 < tempfile']
)
diff --git a/test/unit/tempfile b/test/unit/tempfile
deleted file mode 100644
index e0b00807c5b..00000000000
--- a/test/unit/tempfile
+++ /dev/null
@@ -1 +0,0 @@
-label: gpt
From 4acfd8791ec754a2a09a2bce2db3a38f4cd899e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?=
Date: Wed, 17 Jun 2026 22:30:33 +0200
Subject: [PATCH 5/6] Fixed set_uuid
Wrong implementation by Copilot
---
kiwi/partitioner/gpt.py | 5 +++--
test/unit/partitioner/gpt_test.py | 7 +++++--
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/kiwi/partitioner/gpt.py b/kiwi/partitioner/gpt.py
index 3f030a1a94b..2b684048655 100644
--- a/kiwi/partitioner/gpt.py
+++ b/kiwi/partitioner/gpt.py
@@ -19,6 +19,7 @@
import logging
import re
import shlex
+from uuid import UUID
from typing import (
List, Optional
)
@@ -137,8 +138,8 @@ def set_uuid(self, partition_id: int, uuid: str) -> None:
"""
Command.run(
[
- 'sfdisk', '--part-type', self.disk_device,
- format(partition_id), uuid
+ 'sfdisk', '--part-uuid', self.disk_device,
+ format(partition_id), format(UUID(uuid))
]
)
diff --git a/test/unit/partitioner/gpt_test.py b/test/unit/partitioner/gpt_test.py
index a71053af154..4dd796b5f56 100644
--- a/test/unit/partitioner/gpt_test.py
+++ b/test/unit/partitioner/gpt_test.py
@@ -261,7 +261,10 @@ def test_resize_table(self, mock_command, mock_call_sfdisk):
@patch('kiwi.partitioner.gpt.Command.run')
def test_set_uuid(self, mock_Command_run):
- self.partitioner.set_uuid(42, 'ID')
+ self.partitioner.set_uuid(42, 'c12a7328f81f11d2ba4b00a0c93ec93b')
mock_Command_run.assert_called_once_with(
- ['sfdisk', '--part-type', '/dev/loop0', '42', 'ID']
+ [
+ 'sfdisk', '--part-uuid', '/dev/loop0',
+ '42', 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b'
+ ]
)
From aaf357dfd88b48c6a3e68135e64991c531d97373 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?=
Date: Thu, 18 Jun 2026 09:28:03 +0200
Subject: [PATCH 6/6] Print sfdisk input file to log
---
kiwi/partitioner/gpt.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/kiwi/partitioner/gpt.py b/kiwi/partitioner/gpt.py
index 2b684048655..f09adc41f3a 100644
--- a/kiwi/partitioner/gpt.py
+++ b/kiwi/partitioner/gpt.py
@@ -234,6 +234,7 @@ def _call_sfdisk(
command = ' '.join(
sfdisk_command
)
+ log.debug(f'Creating partition layout: {partition_setup}')
Command.run(['bash', '-c', command])
def _get_partition_geometry(self, partition_id: int) -> tuple[str, str]: