scripts: rewrite fusing scirpts in python 37/299537/3
authorŁukasz Stelmach <l.stelmach@samsung.com>
Mon, 4 Sep 2023 10:23:12 +0000 (12:23 +0200)
committerŁukasz Stelmach <l.stelmach@samsung.com>
Fri, 6 Oct 2023 11:13:46 +0000 (13:13 +0200)
Replace target specific shell fusing scirpts with a single extensible
Python script. To add a new target add a new class inheriting from
SdFusingTarget or SdFusingTargetAB and provide necessary information
in the part_table class variable.

Change-Id: I98cc732d78a8e71b423bc7ec42c1a6a890ffb41b
Signed-off-by: Łukasz Stelmach <l.stelmach@samsung.com>
scripts/tizen/sd_fusing.py [new file with mode: 0755]

diff --git a/scripts/tizen/sd_fusing.py b/scripts/tizen/sd_fusing.py
new file mode 100755 (executable)
index 0000000..162a1cb
--- /dev/null
@@ -0,0 +1,754 @@
+#!/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.0.0"
+
+Format = False
+Device = ""
+File = ""
+Yes = False
+
+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.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):
+        self.name = name
+        self.size = size
+        self.start = start
+        self.ptype = ptype
+        self.bootable = bootable
+    def __str__(self):
+        output = []
+        if self.start:
+            output.append(f"start={self.start}MiB")
+        if 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"
+        for part in self.part_table:
+            output += str(part)
+        return output
+
+class SdFusingTarget:
+    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)
+        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[self.user_partition]["size"] = self.user_size
+        self.label = Label(self.part_table, ltype)
+
+    def get_partition_index(self, binary):
+        return self.binaries.get(binary, None)
+
+    params = ()
+    def initialize_parameters(self):
+        pass
+
+class SdFusingTargetAB(SdFusingTarget):
+    def get_partition_index(self, binary):
+        if self.update == 'b':
+            return self.binaries_b.get(binary, None)
+        return self.binaries.get(binary, None)
+
+class RpiInitParams:
+    def initialize_parameters(self):
+        logging.debug("Initializing parameterss")
+        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)
+
+        argv = ['tune2fs', '-O', '^metadata_csum', d]
+        logging.debug(" ".join(argv))
+        subprocess.run(argv,
+                       stdin=subprocess.DEVNULL,
+                       stdout=None, stderr=None)
+
+        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("Failed to mount {d} in {mnt}")
+                return
+            for param, value in self.params:
+                with open(os.path.join(mnt, param), 'w') as f:
+                    f.write(value + '\n')
+            argv = ['umount', d]
+            logging.debug(" ".join(argv))
+            subprocess.run(argv,
+                           stdin=subprocess.DEVNULL,
+                           stdout=None, stderr=None)
+
+class Rpi3(RpiInitParams, SdFusingTarget):
+    long_name = "Raspberry Pi 3"
+    part_table = [
+        {"size": 64,   "fstype": "vfat", "name": "boot", "start": 4, "ptype": "0xe", "bootable": True},
+        {"size": 3072, "fstype": "ext4", "name": "rootfs"},
+        {"size": 1344, "fstype": "ext4", "name": "system-data"},
+        {"size": None, "ptype":  "5",    "name": "extended", "start": 4484},
+        {"size": None, "fstype": "ext4", "name": "user"},
+        {"size": 32,   "fstype": "ext4", "name": "modules"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery"},
+        {"size": 8,    "fstype": "ext4", "name": "inform"},
+        {"size": 256,  "fstype": "ext4", "name": "hal"},
+        {"size": 125,  "fstype": "ext4", "name": "reserved2"},
+    ]
+    binaries = {
+        "boot.img":             1,
+        "rootfs.img":           2,
+        "system-data.img":      3,
+        "user.img":             5,
+        "modules.img":          6,
+        "ramdisk.img":          7,
+        "ramdisk-recovery.img": 8,
+        "hal.img":              10,
+    }
+    params = (('reboot-param.bin', ''),)
+
+    def __init__(self, device, args):
+        self.reserved_space = 12
+        self.user_partition = 4
+        super().__init__(device, "dos")
+
+class Rpi4Super(RpiInitParams, SdFusingTargetAB):
+    long_name = "Raspberry Pi 4 w/ super partition"
+    part_table = [
+        {"size": 64,   "fstype": "vfat", "name": "boot_a","start": 4,
+         "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"},
+        {"size": 6656, "fstype": "ext4", "name": "super"},
+        {"size": 1344, "fstype": "ext4", "name": "system-data"},
+        {"size": 36,   "fstype": "raw",  "name": "none"},
+        {"size": None, "fstype": "ext4", "name": "user"},
+        {"size": 32,   "fstype": "ext4", "name": "module_a"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk_a"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a"},
+        {"size": 8,    "fstype": "ext4", "name": "inform"},
+        {"size": 64,   "fstype": "vfat", "name": "boot_b",
+         "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"},
+        {"size": 32,   "fstype": "ext4", "name": "module_b"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk_b"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_b"},
+        {"size": 4,    "fstype": "ext4", "name": "reserved0"},
+        {"size": 64,   "fstype": "ext4", "name": "reserved1"},
+        {"size": 125,  "fstype": "ext4", "name": "reserved2"}
+    ]
+    binaries = {
+        "boot.img":              1,
+        "super.img":             2,
+        "system-data.img":       3,
+        "user.img":              5,
+        "modules.img":           6,
+        "ramdisk.img":           7,
+        "ramdisk-recovery.img":  8,
+    }
+    binaries_b = {
+        "boot.img":             10,
+        "modules.img":          11,
+        "ramdisk.img":          12,
+        "ramdisk-recovery.img": 13,
+    }
+    params = (('reboot-param.bin', 'norm'),
+              ('reboot-param.info', 'norm'),
+              ('partition-ab.info', 'a'),
+              ('partition-ab-cloned.info', '1'),
+              ('upgrade-status.info', '0'),
+              ('partition-a-status.info', 'ok'),
+              ('partition-b-status.info', 'ok'))
+
+    def __init__(self, device, args):
+        self.reserved_space = 8
+        self.user_partition = 4
+        self.update = args.update
+        super().__init__(device, "gpt")
+        self.with_super = True
+        self.super_alignment = 1048576
+
+class Rpi4(RpiInitParams, SdFusingTargetAB):
+    long_name = "Raspberry Pi 4"
+    part_table = [
+        {"size": 64,   "fstype": "vfat", "name": "boot_a", "start": 4,
+         "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"},
+        {"size": 3072, "fstype": "ext4", "name": "rootfs_a"},
+        {"size": 1344, "fstype": "ext4", "name": "system-data"},
+        {"size": 36,   "fstype": "raw",  "name": "none"},
+        {"size": None, "fstype": "ext4", "name": "user"},
+        {"size": 32,   "fstype": "ext4", "name": "module_a"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk_a"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a"},
+        {"size": 8,    "fstype": "ext4", "name": "inform"},
+        {"size": 256,  "fstype": "ext4", "name": "hal_a"},
+        {"size": 64,   "fstype": "vfat", "name": "boot_b",
+         "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"},
+        {"size": 3072, "fstype": "ext4", "name": "rootfs_b"},
+        {"size": 32,   "fstype": "ext4", "name": "module_b"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk_b"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_b"},
+        {"size": 256,  "fstype": "ext4", "name": "hal_b"},
+        {"size": 4,    "fstype": "ext4", "name": "param"},
+        {"size": 64,  "fstype": "ext4", "name": "reserved1"},
+        {"size": 125,  "fstype": "ext4", "name": "reserved2"},
+    ]
+    binaries = {
+        "boot.img":              1,
+        "rootfs.img":            2,
+        "system-data.img":       3,
+        "user.img":              5,
+        "modules.img":           6,
+        "ramdisk.img":           7,
+        "ramdisk-recovery.img":  8,
+        "hal.img":              10,
+    }
+    binaries_b = {
+        "boot.img":             11,
+        "rootfs.img":           12,
+        "modules.img":          13,
+        "ramdisk.img":          14,
+        "ramdisk-recovery.img": 15,
+        "hal.img":              16,
+    }
+    params = (('reboot-param.bin', 'norm'),
+              ('reboot-param.info', 'norm'),
+              ('partition-ab.info', 'a'),
+              ('partition-ab-cloned.info', '1'),
+              ('upgrade-status.info', '0'),
+              ('partition-a-status.info', 'ok'),
+              ('partition-b-status.info', 'ok'))
+
+    def __init__(self, device, args):
+        self.reserved_space = 5
+        self.user_partition = 4
+        self.update = args.update
+        super().__init__(device, "gpt")
+
+class RV64(SdFusingTarget):
+    long_name = "QEMU RISC-V 64-bit"
+    part_table = [
+        {"size": 2,    "fstype": "raw",  "name": "SPL", "start": 4,
+         "ptype": "2E54B353-1271-4842-806F-E436D6AF6985"},
+        {"size": 4,    "fstype": "raw",  "name": "u-boot",
+         "ptype": "5B193300-FC78-40CD-8002-E86C45580B47"},
+        {"size": 292,  "fstype": "vfat", "name": "boot_a",
+         "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"},
+        {"size": 36,   "fstype": "raw",  "name": "none"},
+        {"size": 3072, "fstype": "ext4", "name": "rootfs_a"},
+        {"size": 1344, "fstype": "ext4", "name": "system-data"},
+        {"size": None, "fstype": "ext4", "name": "user"},
+        {"size": 32,   "fstype": "ext4", "name": "module_a"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk_a"},
+        {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a"},
+        {"size": 8,    "fstype": "raw",  "name": "inform"},
+        {"size": 256,  "fstype": "ext4", "name": "hal_a"},
+        {"size": 4,    "fstype": "raw",  "name": "reserved0"},
+        {"size": 64,   "fstype": "raw",  "name": "reserved1"},
+        {"size": 125,  "fstype": "raw",  "name": "reserved2"},
+    ]
+    binaries = {
+        "u-boot-spl.bin.normal.out":     1,
+        "u-boot.img":                    2,
+        "u-boot.itb":                    2,
+        "boot.img":                      3,
+        "rootfs.img":                    5,
+        "system-data.img":               6,
+        "user.img":                      7,
+        "modules.img":                   8,
+        "ramdisk.img":                   9,
+        "ramdisk-recovery.img":          10,
+        "hal.img":                       12,
+    }
+
+    def __init__(self, device, args):
+        self.user_partition = 6
+        self.reserved_space = 5
+        super().__init__(device, 'gpt')
+
+class VF2(RV64):
+    long_name = "VisionFive2"
+
+TARGETS = {
+    'rpi3': Rpi3,
+    'rpi4': Rpi4,
+    'rpi4s': Rpi4Super,
+    'vf2': VF2,
+    'rv64': RV64
+}
+
+def device_size(device):
+    argv = ["sfdisk", "-s", device]
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=subprocess.PIPE)
+    size = int(proc.stdout.decode('utf-8').strip()) >> 10
+    logging.debug(f"{device} size {size}MiB")
+    return size
+
+def check_sfdisk():
+    proc = subprocess.run(['sfdisk', '-v'],
+                          stdout=subprocess.PIPE)
+    version = proc.stdout.decode('utf-8').strip()
+    logging.debug(f"Found {version}")
+    major, minor = [int(x) for x in re.findall('[0-9]+', version)][0:2]
+    support_delete = False
+
+    if major < 2 or major == 2 and minor < 26:
+        log.error(f"Your sfdisk {major}.{minor}.{patch} is too old.")
+        return False,False
+    elif major == 2 and minor >= 28:
+        support_delete = True
+
+    return True, support_delete
+
+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)
+
+    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:
+                log.error(f"Failed to create FAT filesystem on {d}")
+                sys.exit(1)
+        elif part['fstype'] == 'ext4':
+            argv = ['mkfs.ext4', '-q', '-L', part['name'], d]
+            logging.debug(" ".join(argv))
+            proc = subprocess.run(argv,
+                                  stdin=subprocess.DEVNULL,
+                                  stdout=None, stderr=None)
+            if proc.returncode != 0:
+                log.error(f"Failed to create ext4 filesystem on {d}")
+                sys.exit(1)
+        elif part['fstype'] == 'raw':
+            pass
+    target.initialize_parameters()
+
+def check_args(args):
+    global Format
+    global Yes
+
+    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
+
+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))
+        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")
+        return None
+    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]
+    return None
+
+def get_device_kname(device):
+    argv = ['lsblk', device, '-o', 'TYPE,KNAME']
+    logging.debug(" ".join(argv))
+    proc = subprocess.run(argv,
+                          stdout=subprocess.PIPE)
+    for l in proc.stdout.decode('utf-8').splitlines():
+        match = re.search(f"^(disk|loop)\s+(.*)", l)
+        if match:
+            return match[2]
+    return None
+
+def do_fuse_file(f, name, target):
+    idx = target.get_partition_index(name)
+    if idx is None:
+        logging.info(f"No partition defined for {name}, skipping.")
+        return
+    pdevice = "/dev/" + get_partition_device(Device, idx)
+    argv = ['dd', 'bs=4M',
+            'oflag=direct',
+            'iflag=fullblock',
+            'conv=nocreat',
+            f"of={pdevice}"]
+    logging.debug(" ".join(argv))
+    proc_dd = subprocess.Popen(argv,
+                               bufsize=(4 << 20),
+                               stdin=subprocess.PIPE,
+                               stdout=None, stderr=None)
+    logging.info(f"Writing {name} to {pdevice}")
+    buf = f.read(4 << 20)
+    while len(buf) > 0:
+        #TODO: progress
+        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 get_aligned_size(size, target):
+    return target.super_alignment*int(1+(size-1)/target.super_alignment)
+
+def do_fuse_image_super(tmpd, target):
+    metadata_slots = 2
+    metadata_size = 65536
+    metadata_aligned_size = get_aligned_size(metadata_size, target)
+
+    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.warning(f"{fn} is missing, skipping super partition image")
+        return
+
+    hal_aligned_size = get_aligned_size(hal_size, target)
+    rootfs_aligned_size = get_aligned_size(rootfs_size, target)
+    group_size = hal_aligned_size + rootfs_aligned_size
+    super_size = metadata_aligned_size + 2 * group_size
+
+    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_aligned_size}:tizen_a",
+            "-p", f"hal_a:none:{hal_aligned_size}:tizen_a",
+            "-g", f"tizen_b:{group_size}",
+            "-p", f"rootfs_b:none:{rootfs_aligned_size}:tizen_b",
+            "-p", f"hal_b:none:{hal_aligned_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")
+    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:
+                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'):
+                    shutil.copy(b, os.path.join(tmpd, fn))
+                else:
+                    do_fuse_image(b, target)
+
+        if target.with_super:
+            do_fuse_image_super(tmpd, target)
+
+if __name__ == '__main__':
+    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="warning", 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="warning",
+                        help="Verbosity, possible values: debug, info, warning, "
+                        "error, critical (default: warning)")
+    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'], default=None,
+                        help="Choose partition set to update: a or b.")
+    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")
+    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')
+
+    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 )