scripts: sd_fusing: Move sd_fusing.py script under scripts dir 02/315802/1
authorJaehoon Chung <jh80.chung@samsung.com>
Thu, 5 Dec 2024 00:17:50 +0000 (09:17 +0900)
committerJaehoon Chung <jh80.chung@samsung.com>
Thu, 5 Dec 2024 00:17:50 +0000 (09:17 +0900)
Move sd_fusing.py script under scripts dir and add MIT License file.

Change-Id: Ice17e469b2be93438904ae3518979f402a9b760e
Signed-off-by: Jaehoon Chung <jh80.chung@samsung.com>
LICENSE.MIT [new file with mode: 0644]
scripts/sd_fusing.py [new file with mode: 0755]
sd_fusing.py [deleted file]

diff --git a/LICENSE.MIT b/LICENSE.MIT
new file mode 100644 (file)
index 0000000..8036196
--- /dev/null
@@ -0,0 +1,16 @@
+ MIT License
+
+ Copyright (c) 2013-2020 Samsung Electronics Co., Ltd
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
diff --git a/scripts/sd_fusing.py b/scripts/sd_fusing.py
new file mode 100755 (executable)
index 0000000..4d0dc0c
--- /dev/null
@@ -0,0 +1,1486 @@
+#!/usr/bin/env python3
+
+from functools import reduce
+
+import argparse
+import atexit
+import errno
+import logging
+import os
+import re
+import shutil
+import stat
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+__version__ = "1.1.13"
+
+Format = False
+Device = ""
+File = ""
+Yes = False
+SuperDelivered = False
+
+LOGGING_NOTICE = int((logging.INFO + logging.WARNING) / 2)
+
+class DebugFormatter(logging.Formatter):
+    def format(self, record):
+        if record.levelno == logging.DEBUG:
+            record.debuginfo = "[{}:{}] ".format(os.path.basename(record.pathname), record.lineno)
+        else:
+            record.debuginfo = ''
+        return logging.Formatter.format(self, record)
+
+class ColorFormatter(DebugFormatter):
+    _levelToColor = {
+        logging.CRITICAL: "\x1b[35;1m",
+        logging.ERROR: "\x1b[33;1m",
+        logging.WARNING: "\x1b[33;1m",
+        LOGGING_NOTICE: "\x1b[0m",
+        logging.INFO: "\x1b[0m",
+        logging.DEBUG: "\x1b[30;1m",
+        logging.NOTSET: "\x1b[30;1m"
+    }
+    def format(self, record):
+        record.levelcolor = self._levelToColor[record.levelno]
+        record.msg = record.msg
+        return super().format(record)
+
+class ColorStreamHandler(logging.StreamHandler):
+    def __init__(self, stream=None, format=None, datefmt=None, style='%', cformat=None):
+        logging.StreamHandler.__init__(self, stream)
+        if os.isatty(self.stream.fileno()):
+            self.formatter = ColorFormatter(cformat, datefmt, style)
+            self.terminator = "\x1b[0m\n"
+        else:
+            self.formatter = DebugFormatter(format, datefmt, style)
+
+class Partition:
+    def __init__(self, name, size, start=None, ptype=None, fstype="raw", bootable=False, **kwargs):
+        self.name = name
+        self.size = size
+        self.size_sectors = kwargs.get("size_sectors", None)
+        self.start = start
+        self.start_sector = kwargs.get("start_sector", None)
+        self.ptype = ptype
+        self.bootable = bootable
+        if type(self.size_sectors) == int and self.size_sectors >= 0:
+            if type(self.size) == int and self.size >= 0:
+                logging.warning(f"partition:{name} overriding size to the value obtained from size_sectors")
+            # size is used to calculate free space, so adjust it here
+            self.size = (self.size_sectors * 512 - 1) / (1024*1024) + 1
+        if type(self.start_sector) == int and self.start_sector >= 0:
+            if type(self.start) == int and self.start >= 0:
+                logging.warning(f"partition:{name} overriding start to the value obtained from start_sector")
+            self.size = None
+
+    def __str__(self):
+        output = []
+        if self.start_sector:
+            output.append(f"start={self.start_sector}")
+        elif self.start:
+            output.append(f"start={self.start}MiB")
+        if type(self.size_sectors) == int and self.size_sectors >= 0:
+            output.append(f"size={self.size_sectors}")
+        elif type(self.size) == int and self.size >= 0:
+            output.append(f"size={self.size}MiB")
+        if self.name:
+            output.append(f"name={self.name}")
+        output.append(f"type={self.ptype}")
+        if self.bootable:
+                       output.append("bootable")
+        return ", ".join(output) + "\n"
+
+class Label:
+    def __init__(self, part_table, ltype):
+        self.ltype = ltype
+        if ltype == 'gpt':
+            ptype = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
+        elif ltype == 'dos':
+            ptype = '83'
+        self.part_table = []
+        for part in part_table:
+            part["ptype"] = part.get("ptype", ptype)
+            self.part_table.append(Partition(**part))
+    def __str__(self):
+        output = f"label: {self.ltype}\n"
+        if self.ltype == 'gpt':
+            output += f"first-lba: 34\n"
+        for part in self.part_table:
+            output += str(part)
+        return output
+
+class SdFusingTarget:
+    params = (('reboot-param.bin', 'norm'),
+              ('reboot-param.info', 'norm'),
+              ('upgrade-progress-status.info', '0'),
+              ('upgrade-state.info', 'standby'),
+              ('upgrade-type.info', 'offline'))
+
+    def __init__(self, device, ltype):
+        # TODO: make a copy of a sublcass part_table
+        self.with_super = False
+        self.device = device
+        total_size = device_size(device)
+
+        # find user partition and calculate its size
+        n = None
+        for i, p in enumerate(self.part_table):
+            if p['name'] == 'user':
+                n = i;
+                break
+
+        if n is not None:
+            self.user_size = total_size - self.reserved_space - \
+                reduce(lambda x, y: x + (y["size"] or 0), self.part_table, 0)
+            if self.user_size < 100:
+                logging.error(f"Not enough space for user data ({self.user_size}). Use larger storage.")
+                raise OSError(errno.ENOSPC, os.strerror(errno.ENOSPC), device)
+            self.part_table[n]["size"] = self.user_size
+
+        self.label = Label(self.part_table, ltype)
+        self.binaries = self._get_binaries('binaries')
+
+    def apply_partition_sizes(self, partition_sizes):
+        if partition_sizes is None or len(partition_sizes) == 0:
+            return 0
+        resized_total = 0
+        for name, size in partition_sizes.items():
+            resized_count = 0
+            for part in self.part_table:
+                if part['name'] == name:
+                    psize = part['size']
+                    part['size'] = size
+                    logging.debug(f"overriding partition:{name}, old-size:{psize} MiB new-size:{size} MiB")
+                    resized_count = resized_count + 1
+            if resized_count == 0:
+                logging.error(f"partition:{name} not found when attempting to apply_partition_sizes")
+            resized_total = resized_total + resized_count
+        return resized_total
+
+    def _get_binaries(self, key):
+        binaries = {}
+        for i, p in enumerate(self.part_table):
+            b = p.get(key, None)
+            if b is None:
+                continue
+            if isinstance(b, str):
+                binaries[b] = i + 1
+            elif isinstance(b, list):
+                for f in b:
+                    binaries[f] = i + 1
+        return binaries
+
+    def get_partition_index_list(self, binary):
+        if hasattr(self, 'update'):
+            logging.error("You have requested to update the {} partition set. "
+                          "This target does not support A/B partition sets."
+                          .format(self.update.upper()))
+            sys.exit(1)
+        return [self.binaries.get(binary, None)]
+
+    def get_raw_binary_sector(self, binary):
+        if not hasattr(self, "raw_binary_table"):
+            return None
+
+        for entry in self.raw_binary_table:
+            if entry['binaries'] == binary:
+                return entry['start_sector'];
+        return None
+
+    def ensure_parttable(self):
+        logging.notice(f"Verifying that partition table on {Device} matches target specification")
+        for partnum, part in enumerate(self.part_table, 1):
+            d = "/dev/" + get_partition_device(Device, partnum)
+            bo = subprocess.check_output(["blkid", "-o", "export", d]).decode('utf-8')
+            if "PARTLABEL=" in bo and f"PARTLABEL={part['name']}" not in bo:
+                logging.error(f'On-device partition label mismatch with selected target: partlabel={part["name"]}, on-device:\n{bo}')
+                sys.exit(1)
+
+    def initialize_parameters(self):
+        pass
+
+    def write_parameters(self, params = None):
+        pass
+
+    def update_parameters(self):
+        self.write_parameters()
+
+class SdFusingTargetAB(SdFusingTarget):
+    def __init__(self, device, ltype):
+        super().__init__(device, ltype)
+        self.binaries_b = self._get_binaries('binaries_b')
+
+    def get_partition_index_list(self, binary):
+        if self.update == 'b':
+            return [self.binaries_b.get(binary, None)]
+        elif self.update == 'ab':
+            return [self.binaries.get(binary, None), self.binaries_b.get(binary, None)]
+
+        return [self.binaries.get(binary, None)]
+
+    def update_parameters(self):
+        part_ab = 'a' if self.update in [None, '', 'a', 'ab'] else 'b'
+        part_cloned = '1' if self.update == 'ab' else '0'
+        params = [('partition-ab.info', part_ab),
+                  ('partition-ab-cloned.info', part_cloned)]
+        if not self.update in [None, '', 'a', 'ab']:
+            params.append(('partition-a-status.info', 'ok'))
+        if self.update in ['b', 'ab']:
+            params.append(('partition-b-status.info', 'ok'))
+        self.write_parameters(self.params + tuple(params))
+
+class InitParams:
+    def find_inform(self):
+        n = None
+        for i, p in enumerate(self.part_table):
+            if p['name'] == 'inform':
+                n = i + 1;
+                break
+        d = "/dev/" + get_partition_device(self.device, n)
+        return d
+
+    def initialize_parameters(self):
+        logging.debug("Initializing parameters")
+        d = self.find_inform()
+
+        argv = ['tune2fs', '-O', '^metadata_csum', d]
+        logging.debug(" ".join(argv))
+        subprocess.run(argv,
+                       stdin=subprocess.DEVNULL,
+                       stdout=None, stderr=None)
+
+    def write_parameters(self, params = None):
+        d = self.find_inform()
+        logging.debug(f"Writing parameters to {d}")
+        with tempfile.TemporaryDirectory() as mnt:
+            argv = ['mount', '-t', 'ext4', d, mnt]
+            logging.debug(" ".join(argv))
+            proc = subprocess.run(argv,
+                                  stdin=subprocess.DEVNULL,
+                                  stdout=None, stderr=None)
+            if proc.returncode != 0:
+                logging.error(f"Failed to mount {d} in {mnt} (Has the device been initialized with --format?)")
+                return
+            parameters = self.params if params is None else params
+            for param, value in parameters:
+                with open(os.path.join(mnt, param), 'w') as f:
+                    logging.debug(f"Writing parameter {param}={value}")
+                    f.write(value + '\n')
+            argv = ['umount', d]
+            logging.debug(" ".join(argv))
+            subprocess.run(argv,
+                           stdin=subprocess.DEVNULL,
+                           stdout=None, stderr=None)
+
+class Rpi3(InitParams, SdFusingTarget):
+    long_name = "Raspberry Pi 3"
+    part_table = [
+        {"size": 64,   "name": "boot", "start": 4, "ptype": "0xe", "bootable": True,
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "ptype":  "5",    "name": "extended", "start": 4484},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "modules",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal",
+         "binaries":   "hal.img"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 12
+        super().__init__(device, "dos")
+
+class AmlogicMBR(InitParams, SdFusingTarget):
+    part_table = [
+        {"size": 128,   "name": "boot", "start": 4, "ptype": "0xe", "bootable": True,
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "ptype":  "5",    "name": "extended", "start": 4548},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 100,   "name": "modules",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 64,   "name": "hal",
+         "binaries":   "hal.img"},
+        {"size": 128,  "name": "reserved2"},
+    ]
+    raw_binary_table = [
+        {"name":       "bootloader",
+         "start_sector": 1},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 12
+        for entry in self.raw_binary_table:
+            if entry['name'] == 'bootloader':
+                entry['binaries'] = args._bootloader_name
+                break
+        super().__init__(device, "dos")
+
+class OdroidC4(AmlogicMBR):
+    long_name = "Odroid C4"
+
+    def __init__(self, device, args):
+        setattr(args, "_bootloader_name", "u-boot-c4.bin")
+        super().__init__(device, args)
+
+class OdroidN2(AmlogicMBR):
+    long_name = "Odroid N2"
+
+    def __init__(self, device, args):
+        setattr(args, "_bootloader_name", "u-boot-n2.bin")
+        super().__init__(device, args)
+
+class KhadasVim3(AmlogicMBR):
+    long_name = "Khadas VIM3"
+
+    def __init__(self, device, args):
+        setattr(args, "_bootloader_name", "u-boot-vim3.bin")
+        super().__init__(device, args)
+
+class KhadasVim3L(AmlogicMBR):
+    long_name = "Khadas VIM3L"
+
+    def __init__(self, device, args):
+        setattr(args, "_bootloader_name", "u-boot-vim3l.bin")
+        super().__init__(device, args)
+
+class Rpi4Super(InitParams, SdFusingTargetAB):
+    long_name = "Raspberry Pi 4 w/ super partition"
+    part_table = [
+        {"size": 64,   "name": "boot_a","start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 6657, "name": "super",
+         "binaries":   "super.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": 36,   "fstype": "raw",  "name": "none"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 64,   "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"}
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 8
+        self.update = args.update
+        super().__init__(device, "gpt")
+        self.with_super = True
+
+class Rpi4(InitParams, SdFusingTargetAB):
+    long_name = "Raspberry Pi 4"
+    part_table = [
+        {"size": 64,   "name": "boot_a", "start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": 36,   "name": "none"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 64,   "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 3072, "name": "rootfs_b",
+         "binaries_b": "rootfs.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 256,  "name": "hal_b",
+         "binaries_b": "hal.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        super().__init__(device, "gpt")
+
+class Rpi4AoT(InitParams, SdFusingTargetAB):
+    long_name = "Raspberry Pi 4 for AoT"
+    part_table = [
+        {"size": 64,   "name": "boot_a", "start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": 36,   "name": "none"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 64,   "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 3072, "name": "rootfs_b",
+         "binaries_b": "rootfs.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 256,  "name": "hal_b",
+         "binaries_b": "hal.img"},
+        {"size": 1536, "name": "aot-system_a",
+         "binaries":   "system.img"},
+        {"size": 1536, "name": "aot-system_b",
+         "binaries_b": "system.img"},
+        {"size": 256,  "name": "aot-vendor_a",
+         "binaries":   "vendor.img"},
+        {"size": 256,  "name": "aot-vendor_b",
+         "binaries_b": "vendor.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        super().__init__(device, "gpt")
+
+class RV64(InitParams, SdFusingTarget):
+    long_name = "QEMU RISC-V 64-bit"
+    part_table = [
+        {"size": 292,  "name": "boot_a", "start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.apply_partition_sizes(args.partition_sizes)
+        super().__init__(device, 'gpt')
+
+class RV64AB(InitParams, SdFusingTargetAB):
+    long_name = "QEMU RISC-V 64-bit with A/B"
+    part_table = [
+        {"size": 292,  "name": "boot_a", "start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 292,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b":   "boot.img"},
+        {"size": 3072, "name": "rootfs_b",
+         "binaries_b":   "rootfs.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b":   "ramdisk-recovery.img"},
+        {"size": 256,  "name": "hal_b",
+         "binaries_b":   "hal.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        self.apply_partition_sizes(args.partition_sizes)
+        super().__init__(device, 'gpt')
+
+class RV64Super(InitParams, SdFusingTargetAB):
+    long_name = "QEMU RISC-V 64-bit with super"
+    part_table = [
+        {"size": 292,  "name": "boot_a", "start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 6656, "name": "super",
+         "binaries":   "super.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 292,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b":   "boot.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b":   "ramdisk-recovery.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        self.apply_partition_sizes(args.partition_sizes)
+        super().__init__(device, 'gpt')
+        self.with_super = True
+
+class VF2(InitParams, SdFusingTargetAB):
+    long_name = "VisionFive2"
+    part_table = [
+        {"size": 2,    "name": "SPL", "start": 4,
+         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
+         "binaries":  ["u-boot-spl.bin.normal.out"],},
+        {"size": 4,    "name": "u-boot",
+         "ptype":      "5B193300-FC78-40CD-8002-E86C45580B47",
+         "binaries":  ["u-boot.img", "u-boot.itb"],},
+        {"size": 128,  "name": "boot_a",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 128,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 3072, "name": "rootfs_b",
+         "binaries_b": "rootfs.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 256,  "name": "hal_b",
+         "binaries_b": "hal.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        self.apply_partition_sizes(args.partition_sizes)
+        super().__init__(device, 'gpt')
+
+class VF2Super(InitParams, SdFusingTargetAB):
+    long_name = "VisionFive2 w/ super partition"
+    part_table = [
+        {"size": 2,    "name": "SPL", "start": 4,
+         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
+         "binaries":  ["u-boot-spl.bin.normal.out"],},
+        {"size": 4,    "name": "u-boot",
+         "ptype":      "5B193300-FC78-40CD-8002-E86C45580B47",
+         "binaries":  ["u-boot.img", "u-boot.itb"],},
+        {"size": 128,  "name": "boot_a",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 6656, "name": "super",
+         "binaries":   "super.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 128,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        super().__init__(device, 'gpt')
+        self.with_super = True
+
+class LicheePi4A(InitParams, SdFusingTargetAB):
+    long_name = "LicheePi4A"
+    part_table = [
+        {"size": None, "name": "spl+uboot",
+         "start_sector": 34, "size_sectors": 4062,
+         "ptype":      "8DA63339-0007-60C0-C436-083AC8230908",
+         "binaries":  ["u-boot-with-spl.bin"],},
+        {"size": 128,  "name": "boot_a",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 128,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 3072, "name": "rootfs_b",
+         "binaries_b": "rootfs.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 256,  "name": "hal_b",
+         "binaries_b": "hal.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+    raw_binary_table = [
+        {"name":       "bootcode",
+         "start_sector": 0, # part of protective MBR (bootcode)
+         "binaries":   "bootcode.bin", },
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        self.apply_partition_sizes(args.partition_sizes)
+        super().__init__(device, 'gpt')
+
+class LicheePi4ASuper(InitParams, SdFusingTargetAB):
+    long_name = "LicheePi4A w/ super partition"
+    part_table = [
+        {"size": None, "name": "spl+uboot",
+         "start_sector": 34, "size_sectors": 4062,
+         "ptype":      "8DA63339-0007-60C0-C436-083AC8230908",
+         "binaries":  ["u-boot-with-spl.bin"],},
+        {"size": 128,  "name": "boot_a",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 6656, "name": "super",
+         "binaries":   "super.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 128,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"}
+    ]
+    raw_binary_table = [
+        {"name":       "bootcode",
+         "start_sector": 0, # part of protective MBR (bootcode)
+         "binaries":   "bootcode.bin", },
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 8
+        self.update = args.update
+        super().__init__(device, 'gpt')
+        self.with_super = True
+
+
+class X86emu(SdFusingTarget):
+    part_table = [
+        {"size": 512,  "fstype": "vfat",  "name": "EFI", "start": 4,
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "",},
+        {"size": 512,  "name": "boot",
+         "binaries":   "emulator-boot.img",},
+        {"size": 2048, "fstype": "ext4", "name": "rootfs",
+         "binaries":   "emulator-rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "emulator-sysdata.img"},
+        {"size": 1024, "name": "emulator-swap",
+         "ptype":      "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F"},
+    ]
+
+    def __init__(self, device, args):
+        super().__init__(device, 'gpt')
+        for p in self.label.part_table:
+            if p.name == "rootfs":
+                p.ptype = args._rootfs_uuid
+                break
+
+class BpiF3(InitParams, SdFusingTargetAB):
+    long_name = "BananPi BPI-F3"
+    part_table = [
+        {"size": None, "name": "fsbl",
+         "start_sector": 256, "size_sectors": 512,
+         "ptype":      "5B193300-FC78-40CD-8002-E86C45580B47",
+         "binaries":  ["FSBL.bin"],},
+        {"size": None, "name": "opensbi",
+         "start_sector": 2048, "size_sectors": 2048,
+         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
+         "binaries":  ["fw_dynamic.itb"],},
+        {"size": None, "name": "uboot",
+         "start_sector": 4096, "size_sectors": 4096,
+         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
+         "binaries":  ["u-boot.itb"],},
+        {"size": 256,  "name": "boot_a",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries":   "boot.img"},
+        {"size": 3072, "name": "rootfs_a",
+         "binaries":   "rootfs.img"},
+        {"size": 1344, "name": "system-data",
+         "binaries":   "system-data.img"},
+        {"size": None, "name": "user",
+         "binaries":   "user.img"},
+        {"size": 32,   "name": "module_a",
+         "binaries":   "modules.img"},
+        {"size": 32,   "name": "ramdisk_a",
+         "binaries":   "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_a",
+         "binaries":   "ramdisk-recovery.img"},
+        {"size": 8,    "name": "inform", "fstype": "ext4"},
+        {"size": 256,  "name": "hal_a",
+         "binaries":   "hal.img"},
+        {"size": 128,  "name": "boot_b",
+         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+         "binaries_b": "boot.img"},
+        {"size": 3072, "name": "rootfs_b",
+         "binaries_b": "rootfs.img"},
+        {"size": 32,   "name": "module_b",
+         "binaries_b": "modules.img"},
+        {"size": 32,   "name": "ramdisk_b",
+         "binaries_b": "ramdisk.img"},
+        {"size": 32,   "name": "ramdisk-recovery_b",
+         "binaries_b": "ramdisk-recovery.img"},
+        {"size": 256,  "name": "hal_b",
+         "binaries_b": "hal.img"},
+        {"size": 4,    "name": "reserved0"},
+        {"size": 64,   "name": "reserved1"},
+        {"size": 125,  "name": "reserved2"},
+    ]
+    raw_binary_table = [
+        {"name":       "bootinfo_sd",
+         "start_sector": 0, # part of protective MBR (bootcode)
+         "binaries":   "bootinfo_sd.bin", },
+    ]
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.update = args.update
+        self.apply_partition_sizes(args.partition_sizes)
+        super().__init__(device, 'gpt')
+
+
+class X86emu32(X86emu):
+    long_name = "QEMU x86 32-bit"
+
+    def __init__(self, device, args):
+        setattr(args, "_rootfs_uuid", "44479540-F297-41B2-9AF7-D131D5F0458A")
+        super().__init__(device, args)
+
+class X86emu64(X86emu):
+    long_name = "QEMU x86 64-bit"
+
+    def __init__(self, device, args):
+        setattr(args, "_rootfs_uuid", "4f68bce3-e8cd-4db1-96e7-fbcaf984b709")
+        super().__init__(device, args)
+
+TARGETS = {
+    'rpi3': Rpi3,
+    'odroidc4': OdroidC4,
+    'odroidn2': OdroidN2,
+    'kvim3': KhadasVim3,
+    'kvim3l': KhadasVim3L,
+    'rpi4': Rpi4,
+    'rpi4s': Rpi4Super,
+    'rpi4aot': Rpi4AoT,
+    'vf2': VF2,
+    'vf2s': VF2Super,
+    'rv64': RV64,
+    'rv64ab': RV64AB,
+    'rv64s': RV64Super,
+    'lpi4a': LicheePi4A,
+    'lpi4as': LicheePi4ASuper,
+    'bpif3': BpiF3,
+    'x86emu32': X86emu32,
+    'x86emu64': X86emu64,
+}
+
+def device_size(device):
+    argv = ["sfdisk", "-s", device]
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=subprocess.PIPE,
+                          stderr=subprocess.PIPE)
+    stdout = proc.stdout.decode('utf-8')
+    try:
+        size = int(stdout.strip()) >> 10
+    except ValueError:
+        stderr = proc.stderr.decode('utf-8')
+        logging.error(f"Unexpected sfdisk output:\n{stdout}\n{stderr}\n")
+        sys.exit(1)
+
+    logging.debug(f"{device} size {size}MiB")
+    return size
+
+def check_sfdisk():
+    min_major = 2
+    min_minor = 26
+    min_minor_del = 28
+
+    proc = subprocess.run(['sfdisk', '-v'],
+                          stdout=subprocess.PIPE)
+    version = proc.stdout.decode('utf-8').strip()
+    logging.debug(f"Found {version}")
+    version_tokens = [int(x) for x in re.findall('[0-9]+', version)]
+
+    if len(version_tokens) == 3:
+        major, minor, patch = version_tokens[0:3]
+        version_str = f"{major}.{minor}.{patch}"
+    elif len(version_tokens) == 2:
+        major, minor = version_tokens[0:2]
+        version_str = f"{major}.{minor}"
+    else:
+        logging.warning("Did not read version of sfdisk correctly.")
+        return False,False
+
+    support_delete = False
+
+    if major < min_major or major == min_major and minor < min_minor:
+        logging.error(f"Your sfdisk {version_str} is too old. Please switch to at least {min_major}.{min_minor}")
+        return False,False
+    elif major == min_major and minor >= min_minor_del:
+        support_delete = True
+
+    return True, support_delete
+
+def wait_for_udev():
+    # Run `udevadm settle` to ensure that partition change made by `sfdisk` or writing bootcode is completely reflected in userspace.
+    logging.info("Waiting for the udev event queue to empty...")
+    argv = ['udevadm', 'settle']
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=None,
+                          stderr=None)
+    if proc.returncode != 0:
+        logging.warning("udevadm settle exited without clearing the udev event queue.")
+    else:
+        logging.info("The udev event queue is empty.")
+
+def mkpart(args, target):
+    global Device
+    new, support_delete = check_sfdisk()
+
+    if not new:
+        logging.error('sfdisk too old')
+        sys.exit(1)
+
+    with open('/proc/self/mounts') as mounts:
+        device_kname = '/dev/' + get_device_kname(Device)
+        device_re = re.compile(device_kname + '[^ ]*')
+        logging.debug(f"Checking for mounted partitions on {device_kname}")
+        for m in mounts:
+            match = device_re.match(m)
+            if match:
+                logging.warning('Found mounted device: ' + match[0])
+                argv = ['umount', match[0]]
+                logging.debug(" ".join(argv))
+                proc = subprocess.run(argv)
+                if proc.returncode != 0:
+                    logging.error(f"Failed to unmount {match[0]}")
+                    sys.exit(1)
+
+    if support_delete:
+        logging.info("Removing old partitions")
+        argv = ['sfdisk', '--delete', Device]
+        logging.debug(" ".join(argv))
+        proc = subprocess.run(argv)
+        if proc.returncode != 0:
+            logging.error(f"Failed to remove the old partitions from {Device}")
+    else:
+        logging.info("Removing old partition table")
+        argv = ['dd', 'if=/dev/zero', 'of=' + Device,
+                'bs=512', 'count=32', 'conv=notrunc']
+        logging.debug(" ".join(argv))
+        proc = subprocess.run(argv)
+        if proc.returncode != 0:
+            logging.error(f"Failed to clear the old partition table on {Device}")
+            sys.exit(1)
+
+    logging.debug("New partition table:\n" + str(target.label))
+    argv = ['sfdisk', '--wipe-partitions', 'always', Device]
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=None,
+                          stderr=None,
+                          input=str(target.label).encode())
+    if proc.returncode != 0:
+        logging.error(f"Failed to create partition a new table on {Device}")
+        logging.error(f"New partition table:\n" + str(target.label))
+        sys.exit(1)
+
+    wait_for_udev()
+
+    for i, part in enumerate(target.part_table):
+        d = "/dev/" + get_partition_device(target.device, i+1)
+        if not 'fstype' in part:
+            logging.debug(f"Filesystem not defined for {d}, skipping")
+            continue
+        logging.debug(f"Formatting {d} as {part['fstype']}")
+        if part['fstype'] == 'vfat':
+            argv = ['mkfs.vfat', '-F', '16', '-n', part['name'], d]
+            logging.debug(" ".join(argv))
+            proc = subprocess.run(argv,
+                                  stdin=subprocess.DEVNULL,
+                                  stdout=None, stderr=None)
+            if proc.returncode != 0:
+                logging.error(f"Failed to create FAT filesystem on {d}")
+                sys.exit(1)
+        elif part['fstype'] == 'ext4':
+            argv = ['mkfs.ext4', '-F', '-q', '-L', part['name'], d]
+            logging.debug(" ".join(argv))
+            proc = subprocess.run(argv,
+                                  stdin=subprocess.DEVNULL,
+                                  stdout=None, stderr=None)
+            if proc.returncode != 0:
+                logging.error(f"Failed to create ext4 filesystem on {d}")
+                sys.exit(1)
+        elif part['fstype'] == 'swap':
+            argv = ['mkswap', '-L', part['name'], d]
+            logging.debug(" ".join(argv))
+            proc = subprocess.run(argv,
+                                  stdin=subprocess.DEVNULL,
+                                  stdout=None, stderr=None)
+            if proc.returncode != 0:
+                logging.error(f"Failed to format swap partition {d}")
+                sys.exit(1)
+        elif part['fstype'] == 'raw':
+            pass
+    target.initialize_parameters()
+
+def check_args(args):
+    global Format
+    global Yes
+    global SuperDelivered
+
+    logging.info(f"Device: {args.device}")
+
+    if args.binaries and len(args.binaries) > 0:
+        logging.info("Fusing binar{}: {}".format("y" if len(args.binaries) == 1 else "ies",
+                     ", ".join(args.binaries)))
+
+    if args.YES:
+        Yes = True
+
+    if args.create:
+        Format = True
+        Yes = True
+
+    if args.format:
+        if Yes:
+            Format = True
+        else:
+            response = input(f"{args.device} will be formatted. Continue? [y/N] ")
+            if response.lower() in ('y', 'yes'):
+                Format = True
+            else:
+                Format = False
+
+    if args.super_delivered:
+        SuperDelivered = True
+
+def check_device(args):
+    global Format
+    global Device
+    Device = args.device
+
+    if args.create:
+        if os.path.exists(Device):
+            logging.error(f"Failed to create '{Device}', the file alread exists")
+            sys.exit(1)
+        else:
+            argv = ["dd", "if=/dev/zero", f"of={Device}",
+                    "conv=sparse", "bs=1M", f"count={args.size}"]
+            logging.debug(" ".join(argv))
+            rc = subprocess.run(argv)
+            if rc.returncode != 0:
+                logging.error("Failed to create the backing file")
+                sys.exit(1)
+
+    if os.path.isfile(Device):
+        global File
+        File = Device
+
+        argv = ["losetup", "--show", "--partscan", "--find", f"{File}"]
+        logging.debug(" ".join(argv))
+        proc = subprocess.run(argv,
+                              stdout=subprocess.PIPE)
+        Device = proc.stdout.decode('utf-8').strip()
+        if proc.returncode != 0:
+            logging.error(f"Failed to attach {File} to a loopback device")
+            sys.exit(1)
+        logging.debug(f"Loop device found: {Device}")
+        atexit.register(lambda: subprocess.run(["losetup", "-d", Device]))
+
+    try:
+        s = os.stat(Device)
+        if not stat.S_ISBLK(s.st_mode):
+            raise TypeError
+    except FileNotFoundError:
+        logging.error(f"No such device: {Device}")
+        sys.exit(1)
+    except TypeError:
+        logging.error(f"{Device} is not a block device")
+        sys.exit(1)
+
+def check_partition_format(args, target):
+    global Format
+    global Device
+
+    if not Format:
+        logging.info(f"Skip formatting of {Device}".format(Device))
+        target.ensure_parttable()
+        return
+    logging.info(f"Start formatting of {Device}")
+    mkpart(args, target)
+    logging.info(f"{Device} formatted")
+
+def check_ddversion():
+    proc = subprocess.run(["dd", "--version"],
+                            stdout=subprocess.PIPE)
+    version = proc.stdout.decode('utf-8').split('\n')[0].strip()
+    logging.debug(f"Found {version}")
+    major, minor = (int(x) for x in re.findall('[0-9]+', version))
+
+    if major < 8 or major == 8 and minor < 24:
+        return False
+
+    return True
+
+def get_partition_device(device, idx):
+    argv = ['lsblk', device, '-o', 'TYPE,KNAME']
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=subprocess.PIPE)
+    if proc.returncode != 0:
+        logging.error("lsblk has failed")
+        sys.exit(1)
+    part_re = re.compile(f"^part\s+(.*[^0-9]{idx})$")
+    for l in proc.stdout.decode('utf-8').splitlines():
+        match = part_re.match(l)
+        if match:
+            return match[1]
+    logging.error("device entry not found")
+    sys.exit(1)
+
+def get_device_kname(device):
+    argv = ['lsblk', device, '-o', 'TYPE,KNAME']
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=subprocess.PIPE)
+    if proc.returncode != 0:
+        logging.error("lsblk has failed")
+        sys.exit(1)
+    for l in proc.stdout.decode('utf-8').splitlines():
+        match = re.search(f"^(disk|loop)\s+(.*)", l)
+        if match:
+            return match[2]
+    logging.error("kname entry not found")
+    sys.exit(1)
+
+def do_fuse_raw(f, name, target, sector):
+    argv = ['dd', 'bs=512',
+            'oflag=direct',
+            f'seek={sector}',
+            'conv=nocreat',
+            'status=progress',
+            f"of={Device}"]
+    logging.debug(" ".join(argv))
+    proc_dd = subprocess.Popen(argv,
+                               bufsize=(1 << 9),
+                               stdin=subprocess.PIPE,
+                               stdout=None, stderr=None)
+    logging.notice(f"Writing {name} to {Device} at sector {sector}")
+    f.seek(0)
+    buf = f.read(1 << 9)
+    while len(buf) > 0:
+        proc_dd.stdin.write(buf)
+        buf = f.read(1 << 9)
+    proc_dd.communicate()
+    logging.info("Done")
+
+    # direct writing to block device might trigger kernel/udev to re-read partition table
+    wait_for_udev()
+
+    #TODO: verification
+
+def do_fuse_file(f, name, target):
+    sector = target.get_raw_binary_sector(name)
+    if sector is not None:
+        do_fuse_raw(f, name, target, sector)
+        return
+    indexes = target.get_partition_index_list(name)
+    if len(indexes) == 0:
+        logging.info(f"No partition defined for {name}, skipping.")
+        return
+    for idx in indexes:
+        if idx is None:
+            logging.info(f"No partition defined for {name}, skipping.")
+            continue
+        pdevice = "/dev/" + get_partition_device(Device, idx)
+        argv = ['dd', 'bs=4M',
+                'oflag=direct',
+                'iflag=fullblock',
+                'conv=nocreat',
+                'status=progress',
+                f"of={pdevice}"]
+        logging.debug(" ".join(argv))
+        proc_dd = subprocess.Popen(argv,
+                                   bufsize=(4 << 20),
+                                   stdin=subprocess.PIPE,
+                                   stdout=None, stderr=None)
+        logging.notice(f"Writing {name} to {pdevice}")
+        f.seek(0)
+        buf = f.read(4 << 20)
+        while len(buf) > 0:
+            proc_dd.stdin.write(buf)
+            buf = f.read(4 << 20)
+        proc_dd.communicate()
+        logging.info("Done")
+        #TODO: verification
+
+#TODO: functions with the target argument should probably
+#      be part of some class
+
+def do_fuse_image_super(tmpd, target):
+    metadata_slots = 2
+    metadata_size = 65536
+    header_size = 1024 * 1024 # default alignment used in lpmake
+    super_size = 0
+
+    for p in target.label.part_table:
+        if p.name == "super":
+            super_size = p.size * 1024 * 1024 # size of parts is in MiB. Change to B
+            break
+
+    if super_size == 0:
+        logging.error(f"No information found about super partition, cannot create image")
+        sys.exit(1)
+
+    group_size = int((super_size - header_size) / 2);
+    hal_path = os.path.join(tmpd, 'hal.img')
+    rootfs_path = os.path.join(tmpd, 'rootfs.img')
+    super_path = os.path.join(tmpd, 'super.img')
+
+    try:
+        hal_size = os.stat(hal_path).st_size
+        rootfs_size = os.stat(rootfs_path).st_size
+    except FileNotFoundError as e:
+        fn = os.path.split(e.filename)[-1]
+        logging.error(f"{fn} is missing, cannot create super partition image")
+        sys.exit(1)
+
+    if group_size < hal_size + rootfs_size:
+        logging.error(f"rootfs and hal are too big to fit in a slot on a super partition")
+        sys.exit(1)
+
+    argv = ["lpmake", "-F",
+            f"-o={super_path}",
+            f"--device-size={super_size}",
+            f"--metadata-size={metadata_size}",
+            f"--metadata-slots={metadata_slots}",
+            "-g", f"tizen_a:{group_size}",
+            "-p", f"rootfs_a:none:{rootfs_size}:tizen_a",
+            "-p", f"hal_a:none:{hal_size}:tizen_a",
+            "-g", f"tizen_b:{group_size}",
+            "-p", f"rootfs_b:none:{rootfs_size}:tizen_b",
+            "-p", f"hal_b:none:{hal_size}:tizen_b",
+            "-i", f"rootfs_a={rootfs_path}",
+            "-i", f"rootfs_b={rootfs_path}",
+            "-i", f"hal_a={hal_path}",
+            "-i", f"hal_b={hal_path}"]
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdin=subprocess.DEVNULL,
+                          stdout=None, stderr=None)
+
+    if proc.returncode != 0:
+        logging.error("Failed to create super.img")
+        sys.exit(1)
+    do_fuse_image(super_path, target)
+
+def do_fuse_image_tarball(tarball, tmpd, target):
+    with tarfile.open(tarball) as tf:
+        for entry in tf:
+            if target.with_super and not SuperDelivered:
+                if entry.name in('hal.img', 'rootfs.img'):
+                    tf.extract(entry, path=tmpd)
+                    continue
+            f = tf.extractfile(entry)
+            do_fuse_file(f, entry.name, target)
+
+def do_fuse_image(img, target):
+    with open(img, 'rb') as f:
+        do_fuse_file(f, os.path.basename(img), target)
+
+def fuse_image(args, target):
+    global Yes
+
+    if args.binaries is None or len(args.binaries) == 0:
+        return
+
+    if not Yes and not Format:
+        print(f"The following images will be written to {args.device} and the "
+              "existing data will be lost.\n")
+        for b in args.binaries:
+            print("  " + b)
+        response = input("\nContinue? [y/N] ")
+        if not response.lower() in ('y', 'yes'):
+            return
+
+    with tempfile.TemporaryDirectory() as tmpd:
+        for b in args.binaries:
+            if re.search('\.(tar|tar\.gz|tgz)$', b):
+                do_fuse_image_tarball(b, tmpd, target)
+            else:
+                fn = os.path.split(b)[-1]
+                if target.with_super and fn in ('rootfs.img', 'hal.img') and not SuperDelivered:
+                    shutil.copy(b, os.path.join(tmpd, fn))
+                else:
+                    do_fuse_image(b, target)
+
+        if target.with_super and not SuperDelivered:
+            do_fuse_image_super(tmpd, target)
+    target.update_parameters()
+
+def logger_notice(self, msg, *args, **kws):
+    if self.isEnabledFor(LOGGING_NOTICE):
+        self._log(LOGGING_NOTICE, msg, args, **kws)
+logging.Logger.notice = logger_notice
+
+def logging_notice(msg, *args, **kws):
+    if len(logging.root.handlers) == 0:
+        basicConfig()
+    logging.root.notice(msg, *args, **kws)
+logging.notice = logging_notice
+
+def check_python_version():
+    required_min_ver=(3,8)
+    if sys.version_info[:2] < required_min_ver:
+        print("Minimum required Python version is 3.8")
+        sys.exit(1)
+
+if __name__ == '__main__':
+    check_python_version()
+    parser = argparse.ArgumentParser(description="For {}, version {}".format(
+        ", ".join([v.long_name for k,v in TARGETS.items()]),
+        __version__
+    ))
+    parser.add_argument("-b", "--binary", action="extend", dest="binaries",
+                        nargs='+',
+                        help="binary to flash, may be used multiple times")
+    parser.add_argument("--create", action="store_true",
+                        help="create the backing file and format the loopback device")
+    parser.add_argument("--debug", action="store_const", const="debug",
+                        default="notice", dest="log_level",
+                        help="set log level to DEBUG")
+    parser.add_argument("-d", "--device",
+                        help="device node or loopback backing file")
+    parser.add_argument("--format", action="store_true",
+                        help="create new partition table on the target device")
+    parser.add_argument("--log-level", dest="log_level", default="notice",
+                        help="Verbosity, possible values: debug, info, notice, warning, "
+                        "error, critical (default: notice)")
+    parser.add_argument("--partition-size", type=str, action="extend", dest="partition_sizes",
+                        nargs='*',
+                        help="override default partition size (in MiB) (used with --format), "
+                        "may be used multiple times, for example: --partition-size hal_a=256")
+    parser.add_argument("--size", type=int, default=8192,
+                        help="size of the backing file to create (in MiB)")
+    parser.add_argument("-t", "--target", required=True,
+                        help="Target device model. Use `--target list`"
+                        " to show supported devices.")
+    parser.add_argument("--update", choices=['a', 'b', 'ab'], default=None,
+                        help="Choose partition set to update: a or b or ab.")
+    parser.add_argument("--version", action="version",
+                        version=f"%(prog)s {__version__}")
+    parser.add_argument("--YES", action="store_true",
+                        help="agree to destroy data on the DEVICE")
+    parser.add_argument("--super_delivered", action="store_true",
+                        help="indicate that super.img is already in tarball and doesn't have to be created during fusing")
+    args = parser.parse_args()
+
+    if args.target == 'list':
+        print("\nSupported devices:\n")
+        for k,v in TARGETS.items():
+            print(f"  {k:6}  {v.long_name}")
+        sys.exit(0)
+
+    if args.device is None:
+        parser.error('-d/--device argument is required for normal operation')
+
+    if args.partition_sizes is not None:
+        partition_sizes = {}
+        for eqstr in args.partition_sizes:
+            ptstr = eqstr.split('=')
+            if len(ptstr) == 2:
+                name = ptstr[0]
+                size = int(ptstr[1])
+                partition_sizes[name] = size
+            else:
+                parser.error('--partition-size must follow the name=size pattern')
+        args.partition_sizes = partition_sizes
+
+    logging.addLevelName(LOGGING_NOTICE, "NOTICE")
+    conh = ColorStreamHandler(format='%(asctime)s.%(msecs)d %(debuginfo)s%(levelname)-8s %(message)s',
+                              cformat='%(asctime)s.%(msecs)d %(debuginfo)s%(levelcolor)s%(message)s',
+                              datefmt='%Y-%m-%dT%H:%M:%S')
+    log_handlers = [conh]
+    logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
+                        handlers=log_handlers,
+                        level=args.log_level.upper())
+
+    logging.debug(" ".join(sys.argv))
+    check_args(args)
+    check_device(args)
+
+    target = TARGETS[args.target](Device, args)
+
+    check_partition_format(args, target)
+    fuse_image(args, target)
+    subprocess.run(['sync'],
+                   stdin=subprocess.DEVNULL,
+                   stdout=None, stderr=None )
diff --git a/sd_fusing.py b/sd_fusing.py
deleted file mode 100755 (executable)
index 4d0dc0c..0000000
+++ /dev/null
@@ -1,1486 +0,0 @@
-#!/usr/bin/env python3
-
-from functools import reduce
-
-import argparse
-import atexit
-import errno
-import logging
-import os
-import re
-import shutil
-import stat
-import subprocess
-import sys
-import tarfile
-import tempfile
-
-__version__ = "1.1.13"
-
-Format = False
-Device = ""
-File = ""
-Yes = False
-SuperDelivered = False
-
-LOGGING_NOTICE = int((logging.INFO + logging.WARNING) / 2)
-
-class DebugFormatter(logging.Formatter):
-    def format(self, record):
-        if record.levelno == logging.DEBUG:
-            record.debuginfo = "[{}:{}] ".format(os.path.basename(record.pathname), record.lineno)
-        else:
-            record.debuginfo = ''
-        return logging.Formatter.format(self, record)
-
-class ColorFormatter(DebugFormatter):
-    _levelToColor = {
-        logging.CRITICAL: "\x1b[35;1m",
-        logging.ERROR: "\x1b[33;1m",
-        logging.WARNING: "\x1b[33;1m",
-        LOGGING_NOTICE: "\x1b[0m",
-        logging.INFO: "\x1b[0m",
-        logging.DEBUG: "\x1b[30;1m",
-        logging.NOTSET: "\x1b[30;1m"
-    }
-    def format(self, record):
-        record.levelcolor = self._levelToColor[record.levelno]
-        record.msg = record.msg
-        return super().format(record)
-
-class ColorStreamHandler(logging.StreamHandler):
-    def __init__(self, stream=None, format=None, datefmt=None, style='%', cformat=None):
-        logging.StreamHandler.__init__(self, stream)
-        if os.isatty(self.stream.fileno()):
-            self.formatter = ColorFormatter(cformat, datefmt, style)
-            self.terminator = "\x1b[0m\n"
-        else:
-            self.formatter = DebugFormatter(format, datefmt, style)
-
-class Partition:
-    def __init__(self, name, size, start=None, ptype=None, fstype="raw", bootable=False, **kwargs):
-        self.name = name
-        self.size = size
-        self.size_sectors = kwargs.get("size_sectors", None)
-        self.start = start
-        self.start_sector = kwargs.get("start_sector", None)
-        self.ptype = ptype
-        self.bootable = bootable
-        if type(self.size_sectors) == int and self.size_sectors >= 0:
-            if type(self.size) == int and self.size >= 0:
-                logging.warning(f"partition:{name} overriding size to the value obtained from size_sectors")
-            # size is used to calculate free space, so adjust it here
-            self.size = (self.size_sectors * 512 - 1) / (1024*1024) + 1
-        if type(self.start_sector) == int and self.start_sector >= 0:
-            if type(self.start) == int and self.start >= 0:
-                logging.warning(f"partition:{name} overriding start to the value obtained from start_sector")
-            self.size = None
-
-    def __str__(self):
-        output = []
-        if self.start_sector:
-            output.append(f"start={self.start_sector}")
-        elif self.start:
-            output.append(f"start={self.start}MiB")
-        if type(self.size_sectors) == int and self.size_sectors >= 0:
-            output.append(f"size={self.size_sectors}")
-        elif type(self.size) == int and self.size >= 0:
-            output.append(f"size={self.size}MiB")
-        if self.name:
-            output.append(f"name={self.name}")
-        output.append(f"type={self.ptype}")
-        if self.bootable:
-                       output.append("bootable")
-        return ", ".join(output) + "\n"
-
-class Label:
-    def __init__(self, part_table, ltype):
-        self.ltype = ltype
-        if ltype == 'gpt':
-            ptype = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
-        elif ltype == 'dos':
-            ptype = '83'
-        self.part_table = []
-        for part in part_table:
-            part["ptype"] = part.get("ptype", ptype)
-            self.part_table.append(Partition(**part))
-    def __str__(self):
-        output = f"label: {self.ltype}\n"
-        if self.ltype == 'gpt':
-            output += f"first-lba: 34\n"
-        for part in self.part_table:
-            output += str(part)
-        return output
-
-class SdFusingTarget:
-    params = (('reboot-param.bin', 'norm'),
-              ('reboot-param.info', 'norm'),
-              ('upgrade-progress-status.info', '0'),
-              ('upgrade-state.info', 'standby'),
-              ('upgrade-type.info', 'offline'))
-
-    def __init__(self, device, ltype):
-        # TODO: make a copy of a sublcass part_table
-        self.with_super = False
-        self.device = device
-        total_size = device_size(device)
-
-        # find user partition and calculate its size
-        n = None
-        for i, p in enumerate(self.part_table):
-            if p['name'] == 'user':
-                n = i;
-                break
-
-        if n is not None:
-            self.user_size = total_size - self.reserved_space - \
-                reduce(lambda x, y: x + (y["size"] or 0), self.part_table, 0)
-            if self.user_size < 100:
-                logging.error(f"Not enough space for user data ({self.user_size}). Use larger storage.")
-                raise OSError(errno.ENOSPC, os.strerror(errno.ENOSPC), device)
-            self.part_table[n]["size"] = self.user_size
-
-        self.label = Label(self.part_table, ltype)
-        self.binaries = self._get_binaries('binaries')
-
-    def apply_partition_sizes(self, partition_sizes):
-        if partition_sizes is None or len(partition_sizes) == 0:
-            return 0
-        resized_total = 0
-        for name, size in partition_sizes.items():
-            resized_count = 0
-            for part in self.part_table:
-                if part['name'] == name:
-                    psize = part['size']
-                    part['size'] = size
-                    logging.debug(f"overriding partition:{name}, old-size:{psize} MiB new-size:{size} MiB")
-                    resized_count = resized_count + 1
-            if resized_count == 0:
-                logging.error(f"partition:{name} not found when attempting to apply_partition_sizes")
-            resized_total = resized_total + resized_count
-        return resized_total
-
-    def _get_binaries(self, key):
-        binaries = {}
-        for i, p in enumerate(self.part_table):
-            b = p.get(key, None)
-            if b is None:
-                continue
-            if isinstance(b, str):
-                binaries[b] = i + 1
-            elif isinstance(b, list):
-                for f in b:
-                    binaries[f] = i + 1
-        return binaries
-
-    def get_partition_index_list(self, binary):
-        if hasattr(self, 'update'):
-            logging.error("You have requested to update the {} partition set. "
-                          "This target does not support A/B partition sets."
-                          .format(self.update.upper()))
-            sys.exit(1)
-        return [self.binaries.get(binary, None)]
-
-    def get_raw_binary_sector(self, binary):
-        if not hasattr(self, "raw_binary_table"):
-            return None
-
-        for entry in self.raw_binary_table:
-            if entry['binaries'] == binary:
-                return entry['start_sector'];
-        return None
-
-    def ensure_parttable(self):
-        logging.notice(f"Verifying that partition table on {Device} matches target specification")
-        for partnum, part in enumerate(self.part_table, 1):
-            d = "/dev/" + get_partition_device(Device, partnum)
-            bo = subprocess.check_output(["blkid", "-o", "export", d]).decode('utf-8')
-            if "PARTLABEL=" in bo and f"PARTLABEL={part['name']}" not in bo:
-                logging.error(f'On-device partition label mismatch with selected target: partlabel={part["name"]}, on-device:\n{bo}')
-                sys.exit(1)
-
-    def initialize_parameters(self):
-        pass
-
-    def write_parameters(self, params = None):
-        pass
-
-    def update_parameters(self):
-        self.write_parameters()
-
-class SdFusingTargetAB(SdFusingTarget):
-    def __init__(self, device, ltype):
-        super().__init__(device, ltype)
-        self.binaries_b = self._get_binaries('binaries_b')
-
-    def get_partition_index_list(self, binary):
-        if self.update == 'b':
-            return [self.binaries_b.get(binary, None)]
-        elif self.update == 'ab':
-            return [self.binaries.get(binary, None), self.binaries_b.get(binary, None)]
-
-        return [self.binaries.get(binary, None)]
-
-    def update_parameters(self):
-        part_ab = 'a' if self.update in [None, '', 'a', 'ab'] else 'b'
-        part_cloned = '1' if self.update == 'ab' else '0'
-        params = [('partition-ab.info', part_ab),
-                  ('partition-ab-cloned.info', part_cloned)]
-        if not self.update in [None, '', 'a', 'ab']:
-            params.append(('partition-a-status.info', 'ok'))
-        if self.update in ['b', 'ab']:
-            params.append(('partition-b-status.info', 'ok'))
-        self.write_parameters(self.params + tuple(params))
-
-class InitParams:
-    def find_inform(self):
-        n = None
-        for i, p in enumerate(self.part_table):
-            if p['name'] == 'inform':
-                n = i + 1;
-                break
-        d = "/dev/" + get_partition_device(self.device, n)
-        return d
-
-    def initialize_parameters(self):
-        logging.debug("Initializing parameters")
-        d = self.find_inform()
-
-        argv = ['tune2fs', '-O', '^metadata_csum', d]
-        logging.debug(" ".join(argv))
-        subprocess.run(argv,
-                       stdin=subprocess.DEVNULL,
-                       stdout=None, stderr=None)
-
-    def write_parameters(self, params = None):
-        d = self.find_inform()
-        logging.debug(f"Writing parameters to {d}")
-        with tempfile.TemporaryDirectory() as mnt:
-            argv = ['mount', '-t', 'ext4', d, mnt]
-            logging.debug(" ".join(argv))
-            proc = subprocess.run(argv,
-                                  stdin=subprocess.DEVNULL,
-                                  stdout=None, stderr=None)
-            if proc.returncode != 0:
-                logging.error(f"Failed to mount {d} in {mnt} (Has the device been initialized with --format?)")
-                return
-            parameters = self.params if params is None else params
-            for param, value in parameters:
-                with open(os.path.join(mnt, param), 'w') as f:
-                    logging.debug(f"Writing parameter {param}={value}")
-                    f.write(value + '\n')
-            argv = ['umount', d]
-            logging.debug(" ".join(argv))
-            subprocess.run(argv,
-                           stdin=subprocess.DEVNULL,
-                           stdout=None, stderr=None)
-
-class Rpi3(InitParams, SdFusingTarget):
-    long_name = "Raspberry Pi 3"
-    part_table = [
-        {"size": 64,   "name": "boot", "start": 4, "ptype": "0xe", "bootable": True,
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "ptype":  "5",    "name": "extended", "start": 4484},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "modules",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal",
-         "binaries":   "hal.img"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 12
-        super().__init__(device, "dos")
-
-class AmlogicMBR(InitParams, SdFusingTarget):
-    part_table = [
-        {"size": 128,   "name": "boot", "start": 4, "ptype": "0xe", "bootable": True,
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "ptype":  "5",    "name": "extended", "start": 4548},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 100,   "name": "modules",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 64,   "name": "hal",
-         "binaries":   "hal.img"},
-        {"size": 128,  "name": "reserved2"},
-    ]
-    raw_binary_table = [
-        {"name":       "bootloader",
-         "start_sector": 1},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 12
-        for entry in self.raw_binary_table:
-            if entry['name'] == 'bootloader':
-                entry['binaries'] = args._bootloader_name
-                break
-        super().__init__(device, "dos")
-
-class OdroidC4(AmlogicMBR):
-    long_name = "Odroid C4"
-
-    def __init__(self, device, args):
-        setattr(args, "_bootloader_name", "u-boot-c4.bin")
-        super().__init__(device, args)
-
-class OdroidN2(AmlogicMBR):
-    long_name = "Odroid N2"
-
-    def __init__(self, device, args):
-        setattr(args, "_bootloader_name", "u-boot-n2.bin")
-        super().__init__(device, args)
-
-class KhadasVim3(AmlogicMBR):
-    long_name = "Khadas VIM3"
-
-    def __init__(self, device, args):
-        setattr(args, "_bootloader_name", "u-boot-vim3.bin")
-        super().__init__(device, args)
-
-class KhadasVim3L(AmlogicMBR):
-    long_name = "Khadas VIM3L"
-
-    def __init__(self, device, args):
-        setattr(args, "_bootloader_name", "u-boot-vim3l.bin")
-        super().__init__(device, args)
-
-class Rpi4Super(InitParams, SdFusingTargetAB):
-    long_name = "Raspberry Pi 4 w/ super partition"
-    part_table = [
-        {"size": 64,   "name": "boot_a","start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 6657, "name": "super",
-         "binaries":   "super.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": 36,   "fstype": "raw",  "name": "none"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 64,   "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"}
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 8
-        self.update = args.update
-        super().__init__(device, "gpt")
-        self.with_super = True
-
-class Rpi4(InitParams, SdFusingTargetAB):
-    long_name = "Raspberry Pi 4"
-    part_table = [
-        {"size": 64,   "name": "boot_a", "start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": 36,   "name": "none"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 64,   "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 3072, "name": "rootfs_b",
-         "binaries_b": "rootfs.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 256,  "name": "hal_b",
-         "binaries_b": "hal.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        super().__init__(device, "gpt")
-
-class Rpi4AoT(InitParams, SdFusingTargetAB):
-    long_name = "Raspberry Pi 4 for AoT"
-    part_table = [
-        {"size": 64,   "name": "boot_a", "start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": 36,   "name": "none"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 64,   "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 3072, "name": "rootfs_b",
-         "binaries_b": "rootfs.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 256,  "name": "hal_b",
-         "binaries_b": "hal.img"},
-        {"size": 1536, "name": "aot-system_a",
-         "binaries":   "system.img"},
-        {"size": 1536, "name": "aot-system_b",
-         "binaries_b": "system.img"},
-        {"size": 256,  "name": "aot-vendor_a",
-         "binaries":   "vendor.img"},
-        {"size": 256,  "name": "aot-vendor_b",
-         "binaries_b": "vendor.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        super().__init__(device, "gpt")
-
-class RV64(InitParams, SdFusingTarget):
-    long_name = "QEMU RISC-V 64-bit"
-    part_table = [
-        {"size": 292,  "name": "boot_a", "start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.apply_partition_sizes(args.partition_sizes)
-        super().__init__(device, 'gpt')
-
-class RV64AB(InitParams, SdFusingTargetAB):
-    long_name = "QEMU RISC-V 64-bit with A/B"
-    part_table = [
-        {"size": 292,  "name": "boot_a", "start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 292,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b":   "boot.img"},
-        {"size": 3072, "name": "rootfs_b",
-         "binaries_b":   "rootfs.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b":   "ramdisk-recovery.img"},
-        {"size": 256,  "name": "hal_b",
-         "binaries_b":   "hal.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        self.apply_partition_sizes(args.partition_sizes)
-        super().__init__(device, 'gpt')
-
-class RV64Super(InitParams, SdFusingTargetAB):
-    long_name = "QEMU RISC-V 64-bit with super"
-    part_table = [
-        {"size": 292,  "name": "boot_a", "start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 6656, "name": "super",
-         "binaries":   "super.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 292,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b":   "boot.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b":   "ramdisk-recovery.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        self.apply_partition_sizes(args.partition_sizes)
-        super().__init__(device, 'gpt')
-        self.with_super = True
-
-class VF2(InitParams, SdFusingTargetAB):
-    long_name = "VisionFive2"
-    part_table = [
-        {"size": 2,    "name": "SPL", "start": 4,
-         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
-         "binaries":  ["u-boot-spl.bin.normal.out"],},
-        {"size": 4,    "name": "u-boot",
-         "ptype":      "5B193300-FC78-40CD-8002-E86C45580B47",
-         "binaries":  ["u-boot.img", "u-boot.itb"],},
-        {"size": 128,  "name": "boot_a",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 128,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 3072, "name": "rootfs_b",
-         "binaries_b": "rootfs.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 256,  "name": "hal_b",
-         "binaries_b": "hal.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        self.apply_partition_sizes(args.partition_sizes)
-        super().__init__(device, 'gpt')
-
-class VF2Super(InitParams, SdFusingTargetAB):
-    long_name = "VisionFive2 w/ super partition"
-    part_table = [
-        {"size": 2,    "name": "SPL", "start": 4,
-         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
-         "binaries":  ["u-boot-spl.bin.normal.out"],},
-        {"size": 4,    "name": "u-boot",
-         "ptype":      "5B193300-FC78-40CD-8002-E86C45580B47",
-         "binaries":  ["u-boot.img", "u-boot.itb"],},
-        {"size": 128,  "name": "boot_a",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 6656, "name": "super",
-         "binaries":   "super.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 128,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        super().__init__(device, 'gpt')
-        self.with_super = True
-
-class LicheePi4A(InitParams, SdFusingTargetAB):
-    long_name = "LicheePi4A"
-    part_table = [
-        {"size": None, "name": "spl+uboot",
-         "start_sector": 34, "size_sectors": 4062,
-         "ptype":      "8DA63339-0007-60C0-C436-083AC8230908",
-         "binaries":  ["u-boot-with-spl.bin"],},
-        {"size": 128,  "name": "boot_a",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 128,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 3072, "name": "rootfs_b",
-         "binaries_b": "rootfs.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 256,  "name": "hal_b",
-         "binaries_b": "hal.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-    raw_binary_table = [
-        {"name":       "bootcode",
-         "start_sector": 0, # part of protective MBR (bootcode)
-         "binaries":   "bootcode.bin", },
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        self.apply_partition_sizes(args.partition_sizes)
-        super().__init__(device, 'gpt')
-
-class LicheePi4ASuper(InitParams, SdFusingTargetAB):
-    long_name = "LicheePi4A w/ super partition"
-    part_table = [
-        {"size": None, "name": "spl+uboot",
-         "start_sector": 34, "size_sectors": 4062,
-         "ptype":      "8DA63339-0007-60C0-C436-083AC8230908",
-         "binaries":  ["u-boot-with-spl.bin"],},
-        {"size": 128,  "name": "boot_a",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 6656, "name": "super",
-         "binaries":   "super.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 128,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"}
-    ]
-    raw_binary_table = [
-        {"name":       "bootcode",
-         "start_sector": 0, # part of protective MBR (bootcode)
-         "binaries":   "bootcode.bin", },
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 8
-        self.update = args.update
-        super().__init__(device, 'gpt')
-        self.with_super = True
-
-
-class X86emu(SdFusingTarget):
-    part_table = [
-        {"size": 512,  "fstype": "vfat",  "name": "EFI", "start": 4,
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "",},
-        {"size": 512,  "name": "boot",
-         "binaries":   "emulator-boot.img",},
-        {"size": 2048, "fstype": "ext4", "name": "rootfs",
-         "binaries":   "emulator-rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "emulator-sysdata.img"},
-        {"size": 1024, "name": "emulator-swap",
-         "ptype":      "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F"},
-    ]
-
-    def __init__(self, device, args):
-        super().__init__(device, 'gpt')
-        for p in self.label.part_table:
-            if p.name == "rootfs":
-                p.ptype = args._rootfs_uuid
-                break
-
-class BpiF3(InitParams, SdFusingTargetAB):
-    long_name = "BananPi BPI-F3"
-    part_table = [
-        {"size": None, "name": "fsbl",
-         "start_sector": 256, "size_sectors": 512,
-         "ptype":      "5B193300-FC78-40CD-8002-E86C45580B47",
-         "binaries":  ["FSBL.bin"],},
-        {"size": None, "name": "opensbi",
-         "start_sector": 2048, "size_sectors": 2048,
-         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
-         "binaries":  ["fw_dynamic.itb"],},
-        {"size": None, "name": "uboot",
-         "start_sector": 4096, "size_sectors": 4096,
-         "ptype":      "2E54B353-1271-4842-806F-E436D6AF6985",
-         "binaries":  ["u-boot.itb"],},
-        {"size": 256,  "name": "boot_a",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries":   "boot.img"},
-        {"size": 3072, "name": "rootfs_a",
-         "binaries":   "rootfs.img"},
-        {"size": 1344, "name": "system-data",
-         "binaries":   "system-data.img"},
-        {"size": None, "name": "user",
-         "binaries":   "user.img"},
-        {"size": 32,   "name": "module_a",
-         "binaries":   "modules.img"},
-        {"size": 32,   "name": "ramdisk_a",
-         "binaries":   "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_a",
-         "binaries":   "ramdisk-recovery.img"},
-        {"size": 8,    "name": "inform", "fstype": "ext4"},
-        {"size": 256,  "name": "hal_a",
-         "binaries":   "hal.img"},
-        {"size": 128,  "name": "boot_b",
-         "ptype":      "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
-         "binaries_b": "boot.img"},
-        {"size": 3072, "name": "rootfs_b",
-         "binaries_b": "rootfs.img"},
-        {"size": 32,   "name": "module_b",
-         "binaries_b": "modules.img"},
-        {"size": 32,   "name": "ramdisk_b",
-         "binaries_b": "ramdisk.img"},
-        {"size": 32,   "name": "ramdisk-recovery_b",
-         "binaries_b": "ramdisk-recovery.img"},
-        {"size": 256,  "name": "hal_b",
-         "binaries_b": "hal.img"},
-        {"size": 4,    "name": "reserved0"},
-        {"size": 64,   "name": "reserved1"},
-        {"size": 125,  "name": "reserved2"},
-    ]
-    raw_binary_table = [
-        {"name":       "bootinfo_sd",
-         "start_sector": 0, # part of protective MBR (bootcode)
-         "binaries":   "bootinfo_sd.bin", },
-    ]
-
-    def __init__(self, device, args):
-        self.reserved_space = 5
-        self.update = args.update
-        self.apply_partition_sizes(args.partition_sizes)
-        super().__init__(device, 'gpt')
-
-
-class X86emu32(X86emu):
-    long_name = "QEMU x86 32-bit"
-
-    def __init__(self, device, args):
-        setattr(args, "_rootfs_uuid", "44479540-F297-41B2-9AF7-D131D5F0458A")
-        super().__init__(device, args)
-
-class X86emu64(X86emu):
-    long_name = "QEMU x86 64-bit"
-
-    def __init__(self, device, args):
-        setattr(args, "_rootfs_uuid", "4f68bce3-e8cd-4db1-96e7-fbcaf984b709")
-        super().__init__(device, args)
-
-TARGETS = {
-    'rpi3': Rpi3,
-    'odroidc4': OdroidC4,
-    'odroidn2': OdroidN2,
-    'kvim3': KhadasVim3,
-    'kvim3l': KhadasVim3L,
-    'rpi4': Rpi4,
-    'rpi4s': Rpi4Super,
-    'rpi4aot': Rpi4AoT,
-    'vf2': VF2,
-    'vf2s': VF2Super,
-    'rv64': RV64,
-    'rv64ab': RV64AB,
-    'rv64s': RV64Super,
-    'lpi4a': LicheePi4A,
-    'lpi4as': LicheePi4ASuper,
-    'bpif3': BpiF3,
-    'x86emu32': X86emu32,
-    'x86emu64': X86emu64,
-}
-
-def device_size(device):
-    argv = ["sfdisk", "-s", device]
-    logging.debug(" ".join(argv))
-    proc = subprocess.run(argv,
-                          stdout=subprocess.PIPE,
-                          stderr=subprocess.PIPE)
-    stdout = proc.stdout.decode('utf-8')
-    try:
-        size = int(stdout.strip()) >> 10
-    except ValueError:
-        stderr = proc.stderr.decode('utf-8')
-        logging.error(f"Unexpected sfdisk output:\n{stdout}\n{stderr}\n")
-        sys.exit(1)
-
-    logging.debug(f"{device} size {size}MiB")
-    return size
-
-def check_sfdisk():
-    min_major = 2
-    min_minor = 26
-    min_minor_del = 28
-
-    proc = subprocess.run(['sfdisk', '-v'],
-                          stdout=subprocess.PIPE)
-    version = proc.stdout.decode('utf-8').strip()
-    logging.debug(f"Found {version}")
-    version_tokens = [int(x) for x in re.findall('[0-9]+', version)]
-
-    if len(version_tokens) == 3:
-        major, minor, patch = version_tokens[0:3]
-        version_str = f"{major}.{minor}.{patch}"
-    elif len(version_tokens) == 2:
-        major, minor = version_tokens[0:2]
-        version_str = f"{major}.{minor}"
-    else:
-        logging.warning("Did not read version of sfdisk correctly.")
-        return False,False
-
-    support_delete = False
-
-    if major < min_major or major == min_major and minor < min_minor:
-        logging.error(f"Your sfdisk {version_str} is too old. Please switch to at least {min_major}.{min_minor}")
-        return False,False
-    elif major == min_major and minor >= min_minor_del:
-        support_delete = True
-
-    return True, support_delete
-
-def wait_for_udev():
-    # Run `udevadm settle` to ensure that partition change made by `sfdisk` or writing bootcode is completely reflected in userspace.
-    logging.info("Waiting for the udev event queue to empty...")
-    argv = ['udevadm', 'settle']
-    logging.debug(" ".join(argv))
-    proc = subprocess.run(argv,
-                          stdout=None,
-                          stderr=None)
-    if proc.returncode != 0:
-        logging.warning("udevadm settle exited without clearing the udev event queue.")
-    else:
-        logging.info("The udev event queue is empty.")
-
-def mkpart(args, target):
-    global Device
-    new, support_delete = check_sfdisk()
-
-    if not new:
-        logging.error('sfdisk too old')
-        sys.exit(1)
-
-    with open('/proc/self/mounts') as mounts:
-        device_kname = '/dev/' + get_device_kname(Device)
-        device_re = re.compile(device_kname + '[^ ]*')
-        logging.debug(f"Checking for mounted partitions on {device_kname}")
-        for m in mounts:
-            match = device_re.match(m)
-            if match:
-                logging.warning('Found mounted device: ' + match[0])
-                argv = ['umount', match[0]]
-                logging.debug(" ".join(argv))
-                proc = subprocess.run(argv)
-                if proc.returncode != 0:
-                    logging.error(f"Failed to unmount {match[0]}")
-                    sys.exit(1)
-
-    if support_delete:
-        logging.info("Removing old partitions")
-        argv = ['sfdisk', '--delete', Device]
-        logging.debug(" ".join(argv))
-        proc = subprocess.run(argv)
-        if proc.returncode != 0:
-            logging.error(f"Failed to remove the old partitions from {Device}")
-    else:
-        logging.info("Removing old partition table")
-        argv = ['dd', 'if=/dev/zero', 'of=' + Device,
-                'bs=512', 'count=32', 'conv=notrunc']
-        logging.debug(" ".join(argv))
-        proc = subprocess.run(argv)
-        if proc.returncode != 0:
-            logging.error(f"Failed to clear the old partition table on {Device}")
-            sys.exit(1)
-
-    logging.debug("New partition table:\n" + str(target.label))
-    argv = ['sfdisk', '--wipe-partitions', 'always', Device]
-    logging.debug(" ".join(argv))
-    proc = subprocess.run(argv,
-                          stdout=None,
-                          stderr=None,
-                          input=str(target.label).encode())
-    if proc.returncode != 0:
-        logging.error(f"Failed to create partition a new table on {Device}")
-        logging.error(f"New partition table:\n" + str(target.label))
-        sys.exit(1)
-
-    wait_for_udev()
-
-    for i, part in enumerate(target.part_table):
-        d = "/dev/" + get_partition_device(target.device, i+1)
-        if not 'fstype' in part:
-            logging.debug(f"Filesystem not defined for {d}, skipping")
-            continue
-        logging.debug(f"Formatting {d} as {part['fstype']}")
-        if part['fstype'] == 'vfat':
-            argv = ['mkfs.vfat', '-F', '16', '-n', part['name'], d]
-            logging.debug(" ".join(argv))
-            proc = subprocess.run(argv,
-                                  stdin=subprocess.DEVNULL,
-                                  stdout=None, stderr=None)
-            if proc.returncode != 0:
-                logging.error(f"Failed to create FAT filesystem on {d}")
-                sys.exit(1)
-        elif part['fstype'] == 'ext4':
-            argv = ['mkfs.ext4', '-F', '-q', '-L', part['name'], d]
-            logging.debug(" ".join(argv))
-            proc = subprocess.run(argv,
-                                  stdin=subprocess.DEVNULL,
-                                  stdout=None, stderr=None)
-            if proc.returncode != 0:
-                logging.error(f"Failed to create ext4 filesystem on {d}")
-                sys.exit(1)
-        elif part['fstype'] == 'swap':
-            argv = ['mkswap', '-L', part['name'], d]
-            logging.debug(" ".join(argv))
-            proc = subprocess.run(argv,
-                                  stdin=subprocess.DEVNULL,
-                                  stdout=None, stderr=None)
-            if proc.returncode != 0:
-                logging.error(f"Failed to format swap partition {d}")
-                sys.exit(1)
-        elif part['fstype'] == 'raw':
-            pass
-    target.initialize_parameters()
-
-def check_args(args):
-    global Format
-    global Yes
-    global SuperDelivered
-
-    logging.info(f"Device: {args.device}")
-
-    if args.binaries and len(args.binaries) > 0:
-        logging.info("Fusing binar{}: {}".format("y" if len(args.binaries) == 1 else "ies",
-                     ", ".join(args.binaries)))
-
-    if args.YES:
-        Yes = True
-
-    if args.create:
-        Format = True
-        Yes = True
-
-    if args.format:
-        if Yes:
-            Format = True
-        else:
-            response = input(f"{args.device} will be formatted. Continue? [y/N] ")
-            if response.lower() in ('y', 'yes'):
-                Format = True
-            else:
-                Format = False
-
-    if args.super_delivered:
-        SuperDelivered = True
-
-def check_device(args):
-    global Format
-    global Device
-    Device = args.device
-
-    if args.create:
-        if os.path.exists(Device):
-            logging.error(f"Failed to create '{Device}', the file alread exists")
-            sys.exit(1)
-        else:
-            argv = ["dd", "if=/dev/zero", f"of={Device}",
-                    "conv=sparse", "bs=1M", f"count={args.size}"]
-            logging.debug(" ".join(argv))
-            rc = subprocess.run(argv)
-            if rc.returncode != 0:
-                logging.error("Failed to create the backing file")
-                sys.exit(1)
-
-    if os.path.isfile(Device):
-        global File
-        File = Device
-
-        argv = ["losetup", "--show", "--partscan", "--find", f"{File}"]
-        logging.debug(" ".join(argv))
-        proc = subprocess.run(argv,
-                              stdout=subprocess.PIPE)
-        Device = proc.stdout.decode('utf-8').strip()
-        if proc.returncode != 0:
-            logging.error(f"Failed to attach {File} to a loopback device")
-            sys.exit(1)
-        logging.debug(f"Loop device found: {Device}")
-        atexit.register(lambda: subprocess.run(["losetup", "-d", Device]))
-
-    try:
-        s = os.stat(Device)
-        if not stat.S_ISBLK(s.st_mode):
-            raise TypeError
-    except FileNotFoundError:
-        logging.error(f"No such device: {Device}")
-        sys.exit(1)
-    except TypeError:
-        logging.error(f"{Device} is not a block device")
-        sys.exit(1)
-
-def check_partition_format(args, target):
-    global Format
-    global Device
-
-    if not Format:
-        logging.info(f"Skip formatting of {Device}".format(Device))
-        target.ensure_parttable()
-        return
-    logging.info(f"Start formatting of {Device}")
-    mkpart(args, target)
-    logging.info(f"{Device} formatted")
-
-def check_ddversion():
-    proc = subprocess.run(["dd", "--version"],
-                            stdout=subprocess.PIPE)
-    version = proc.stdout.decode('utf-8').split('\n')[0].strip()
-    logging.debug(f"Found {version}")
-    major, minor = (int(x) for x in re.findall('[0-9]+', version))
-
-    if major < 8 or major == 8 and minor < 24:
-        return False
-
-    return True
-
-def get_partition_device(device, idx):
-    argv = ['lsblk', device, '-o', 'TYPE,KNAME']
-    logging.debug(" ".join(argv))
-    proc = subprocess.run(argv,
-                          stdout=subprocess.PIPE)
-    if proc.returncode != 0:
-        logging.error("lsblk has failed")
-        sys.exit(1)
-    part_re = re.compile(f"^part\s+(.*[^0-9]{idx})$")
-    for l in proc.stdout.decode('utf-8').splitlines():
-        match = part_re.match(l)
-        if match:
-            return match[1]
-    logging.error("device entry not found")
-    sys.exit(1)
-
-def get_device_kname(device):
-    argv = ['lsblk', device, '-o', 'TYPE,KNAME']
-    logging.debug(" ".join(argv))
-    proc = subprocess.run(argv,
-                          stdout=subprocess.PIPE)
-    if proc.returncode != 0:
-        logging.error("lsblk has failed")
-        sys.exit(1)
-    for l in proc.stdout.decode('utf-8').splitlines():
-        match = re.search(f"^(disk|loop)\s+(.*)", l)
-        if match:
-            return match[2]
-    logging.error("kname entry not found")
-    sys.exit(1)
-
-def do_fuse_raw(f, name, target, sector):
-    argv = ['dd', 'bs=512',
-            'oflag=direct',
-            f'seek={sector}',
-            'conv=nocreat',
-            'status=progress',
-            f"of={Device}"]
-    logging.debug(" ".join(argv))
-    proc_dd = subprocess.Popen(argv,
-                               bufsize=(1 << 9),
-                               stdin=subprocess.PIPE,
-                               stdout=None, stderr=None)
-    logging.notice(f"Writing {name} to {Device} at sector {sector}")
-    f.seek(0)
-    buf = f.read(1 << 9)
-    while len(buf) > 0:
-        proc_dd.stdin.write(buf)
-        buf = f.read(1 << 9)
-    proc_dd.communicate()
-    logging.info("Done")
-
-    # direct writing to block device might trigger kernel/udev to re-read partition table
-    wait_for_udev()
-
-    #TODO: verification
-
-def do_fuse_file(f, name, target):
-    sector = target.get_raw_binary_sector(name)
-    if sector is not None:
-        do_fuse_raw(f, name, target, sector)
-        return
-    indexes = target.get_partition_index_list(name)
-    if len(indexes) == 0:
-        logging.info(f"No partition defined for {name}, skipping.")
-        return
-    for idx in indexes:
-        if idx is None:
-            logging.info(f"No partition defined for {name}, skipping.")
-            continue
-        pdevice = "/dev/" + get_partition_device(Device, idx)
-        argv = ['dd', 'bs=4M',
-                'oflag=direct',
-                'iflag=fullblock',
-                'conv=nocreat',
-                'status=progress',
-                f"of={pdevice}"]
-        logging.debug(" ".join(argv))
-        proc_dd = subprocess.Popen(argv,
-                                   bufsize=(4 << 20),
-                                   stdin=subprocess.PIPE,
-                                   stdout=None, stderr=None)
-        logging.notice(f"Writing {name} to {pdevice}")
-        f.seek(0)
-        buf = f.read(4 << 20)
-        while len(buf) > 0:
-            proc_dd.stdin.write(buf)
-            buf = f.read(4 << 20)
-        proc_dd.communicate()
-        logging.info("Done")
-        #TODO: verification
-
-#TODO: functions with the target argument should probably
-#      be part of some class
-
-def do_fuse_image_super(tmpd, target):
-    metadata_slots = 2
-    metadata_size = 65536
-    header_size = 1024 * 1024 # default alignment used in lpmake
-    super_size = 0
-
-    for p in target.label.part_table:
-        if p.name == "super":
-            super_size = p.size * 1024 * 1024 # size of parts is in MiB. Change to B
-            break
-
-    if super_size == 0:
-        logging.error(f"No information found about super partition, cannot create image")
-        sys.exit(1)
-
-    group_size = int((super_size - header_size) / 2);
-    hal_path = os.path.join(tmpd, 'hal.img')
-    rootfs_path = os.path.join(tmpd, 'rootfs.img')
-    super_path = os.path.join(tmpd, 'super.img')
-
-    try:
-        hal_size = os.stat(hal_path).st_size
-        rootfs_size = os.stat(rootfs_path).st_size
-    except FileNotFoundError as e:
-        fn = os.path.split(e.filename)[-1]
-        logging.error(f"{fn} is missing, cannot create super partition image")
-        sys.exit(1)
-
-    if group_size < hal_size + rootfs_size:
-        logging.error(f"rootfs and hal are too big to fit in a slot on a super partition")
-        sys.exit(1)
-
-    argv = ["lpmake", "-F",
-            f"-o={super_path}",
-            f"--device-size={super_size}",
-            f"--metadata-size={metadata_size}",
-            f"--metadata-slots={metadata_slots}",
-            "-g", f"tizen_a:{group_size}",
-            "-p", f"rootfs_a:none:{rootfs_size}:tizen_a",
-            "-p", f"hal_a:none:{hal_size}:tizen_a",
-            "-g", f"tizen_b:{group_size}",
-            "-p", f"rootfs_b:none:{rootfs_size}:tizen_b",
-            "-p", f"hal_b:none:{hal_size}:tizen_b",
-            "-i", f"rootfs_a={rootfs_path}",
-            "-i", f"rootfs_b={rootfs_path}",
-            "-i", f"hal_a={hal_path}",
-            "-i", f"hal_b={hal_path}"]
-    logging.debug(" ".join(argv))
-    proc = subprocess.run(argv,
-                          stdin=subprocess.DEVNULL,
-                          stdout=None, stderr=None)
-
-    if proc.returncode != 0:
-        logging.error("Failed to create super.img")
-        sys.exit(1)
-    do_fuse_image(super_path, target)
-
-def do_fuse_image_tarball(tarball, tmpd, target):
-    with tarfile.open(tarball) as tf:
-        for entry in tf:
-            if target.with_super and not SuperDelivered:
-                if entry.name in('hal.img', 'rootfs.img'):
-                    tf.extract(entry, path=tmpd)
-                    continue
-            f = tf.extractfile(entry)
-            do_fuse_file(f, entry.name, target)
-
-def do_fuse_image(img, target):
-    with open(img, 'rb') as f:
-        do_fuse_file(f, os.path.basename(img), target)
-
-def fuse_image(args, target):
-    global Yes
-
-    if args.binaries is None or len(args.binaries) == 0:
-        return
-
-    if not Yes and not Format:
-        print(f"The following images will be written to {args.device} and the "
-              "existing data will be lost.\n")
-        for b in args.binaries:
-            print("  " + b)
-        response = input("\nContinue? [y/N] ")
-        if not response.lower() in ('y', 'yes'):
-            return
-
-    with tempfile.TemporaryDirectory() as tmpd:
-        for b in args.binaries:
-            if re.search('\.(tar|tar\.gz|tgz)$', b):
-                do_fuse_image_tarball(b, tmpd, target)
-            else:
-                fn = os.path.split(b)[-1]
-                if target.with_super and fn in ('rootfs.img', 'hal.img') and not SuperDelivered:
-                    shutil.copy(b, os.path.join(tmpd, fn))
-                else:
-                    do_fuse_image(b, target)
-
-        if target.with_super and not SuperDelivered:
-            do_fuse_image_super(tmpd, target)
-    target.update_parameters()
-
-def logger_notice(self, msg, *args, **kws):
-    if self.isEnabledFor(LOGGING_NOTICE):
-        self._log(LOGGING_NOTICE, msg, args, **kws)
-logging.Logger.notice = logger_notice
-
-def logging_notice(msg, *args, **kws):
-    if len(logging.root.handlers) == 0:
-        basicConfig()
-    logging.root.notice(msg, *args, **kws)
-logging.notice = logging_notice
-
-def check_python_version():
-    required_min_ver=(3,8)
-    if sys.version_info[:2] < required_min_ver:
-        print("Minimum required Python version is 3.8")
-        sys.exit(1)
-
-if __name__ == '__main__':
-    check_python_version()
-    parser = argparse.ArgumentParser(description="For {}, version {}".format(
-        ", ".join([v.long_name for k,v in TARGETS.items()]),
-        __version__
-    ))
-    parser.add_argument("-b", "--binary", action="extend", dest="binaries",
-                        nargs='+',
-                        help="binary to flash, may be used multiple times")
-    parser.add_argument("--create", action="store_true",
-                        help="create the backing file and format the loopback device")
-    parser.add_argument("--debug", action="store_const", const="debug",
-                        default="notice", dest="log_level",
-                        help="set log level to DEBUG")
-    parser.add_argument("-d", "--device",
-                        help="device node or loopback backing file")
-    parser.add_argument("--format", action="store_true",
-                        help="create new partition table on the target device")
-    parser.add_argument("--log-level", dest="log_level", default="notice",
-                        help="Verbosity, possible values: debug, info, notice, warning, "
-                        "error, critical (default: notice)")
-    parser.add_argument("--partition-size", type=str, action="extend", dest="partition_sizes",
-                        nargs='*',
-                        help="override default partition size (in MiB) (used with --format), "
-                        "may be used multiple times, for example: --partition-size hal_a=256")
-    parser.add_argument("--size", type=int, default=8192,
-                        help="size of the backing file to create (in MiB)")
-    parser.add_argument("-t", "--target", required=True,
-                        help="Target device model. Use `--target list`"
-                        " to show supported devices.")
-    parser.add_argument("--update", choices=['a', 'b', 'ab'], default=None,
-                        help="Choose partition set to update: a or b or ab.")
-    parser.add_argument("--version", action="version",
-                        version=f"%(prog)s {__version__}")
-    parser.add_argument("--YES", action="store_true",
-                        help="agree to destroy data on the DEVICE")
-    parser.add_argument("--super_delivered", action="store_true",
-                        help="indicate that super.img is already in tarball and doesn't have to be created during fusing")
-    args = parser.parse_args()
-
-    if args.target == 'list':
-        print("\nSupported devices:\n")
-        for k,v in TARGETS.items():
-            print(f"  {k:6}  {v.long_name}")
-        sys.exit(0)
-
-    if args.device is None:
-        parser.error('-d/--device argument is required for normal operation')
-
-    if args.partition_sizes is not None:
-        partition_sizes = {}
-        for eqstr in args.partition_sizes:
-            ptstr = eqstr.split('=')
-            if len(ptstr) == 2:
-                name = ptstr[0]
-                size = int(ptstr[1])
-                partition_sizes[name] = size
-            else:
-                parser.error('--partition-size must follow the name=size pattern')
-        args.partition_sizes = partition_sizes
-
-    logging.addLevelName(LOGGING_NOTICE, "NOTICE")
-    conh = ColorStreamHandler(format='%(asctime)s.%(msecs)d %(debuginfo)s%(levelname)-8s %(message)s',
-                              cformat='%(asctime)s.%(msecs)d %(debuginfo)s%(levelcolor)s%(message)s',
-                              datefmt='%Y-%m-%dT%H:%M:%S')
-    log_handlers = [conh]
-    logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
-                        handlers=log_handlers,
-                        level=args.log_level.upper())
-
-    logging.debug(" ".join(sys.argv))
-    check_args(args)
-    check_device(args)
-
-    target = TARGETS[args.target](Device, args)
-
-    check_partition_format(args, target)
-    fuse_image(args, target)
-    subprocess.run(['sync'],
-                   stdin=subprocess.DEVNULL,
-                   stdout=None, stderr=None )