--- /dev/null
+#!/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 )