3 from functools import reduce
25 class DebugFormatter(logging.Formatter):
26 def format(self, record):
27 if record.levelno == logging.DEBUG:
28 record.debuginfo = "[{}:{}] ".format(os.path.basename(record.pathname), record.lineno)
31 return logging.Formatter.format(self, record)
33 class ColorFormatter(DebugFormatter):
35 logging.CRITICAL: "\x1b[35;1m",
36 logging.ERROR: "\x1b[33;1m",
37 logging.WARNING: "\x1b[33;1m",
38 logging.INFO: "\x1b[0m",
39 logging.DEBUG: "\x1b[30;1m",
40 logging.NOTSET: "\x1b[30;1m"
42 def format(self, record):
43 record.levelcolor = self._levelToColor[record.levelno]
44 record.msg = record.msg
45 return super().format(record)
47 class ColorStreamHandler(logging.StreamHandler):
48 def __init__(self, stream=None, format=None, datefmt=None, style='%', cformat=None):
49 logging.StreamHandler.__init__(self, stream)
50 if os.isatty(self.stream.fileno()):
51 self.formatter = ColorFormatter(cformat, datefmt, style)
52 self.terminator = "\x1b[0m\n"
54 self.formatter = DebugFormatter(format, datefmt, style)
57 def __init__(self, name, size, start=None, ptype=None, fstype="raw", bootable=False, **kwargs):
62 self.bootable = bootable
66 output.append(f"start={self.start}MiB")
67 if type(self.size) == int and self.size >= 0:
68 output.append(f"size={self.size}MiB")
70 output.append(f"name={self.name}")
71 output.append(f"type={self.ptype}")
73 output.append("bootable")
74 return ", ".join(output) + "\n"
77 def __init__(self, part_table, ltype):
80 ptype = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
84 for part in part_table:
85 part["ptype"] = part.get("ptype", ptype)
86 self.part_table.append(Partition(**part))
88 output = f"label: {self.ltype}\n"
89 for part in self.part_table:
94 def __init__(self, device, ltype):
95 # TODO: make a copy of a sublcass part_table
96 self.with_super = False
98 total_size = device_size(device)
99 self.user_size = total_size - self.reserved_space - \
100 reduce(lambda x, y: x + (y["size"] or 0), self.part_table, 0)
101 if self.user_size < 100:
102 logging.error(f"Not enough space for user data ({self.user_size}). Use larger storage.")
103 raise OSError(errno.ENOSPC, os.strerror(errno.ENOSPC), device)
104 # self.user_partition counts from 0
105 self.part_table[self.user_partition]["size"] = self.user_size
106 self.label = Label(self.part_table, ltype)
107 self.binaries = self._get_binaries('binaries')
109 def _get_binaries(self, key):
111 for i, p in enumerate(self.part_table):
115 if isinstance(b, str):
117 elif isinstance(b, list):
122 def get_partition_index(self, binary):
123 return self.binaries.get(binary, None)
126 def initialize_parameters(self):
129 class SdFusingTargetAB(SdFusingTarget):
130 def __init__(self, device, ltype):
131 super().__init__(device, ltype)
132 self.binaries_b = self._get_binaries('binaries_b')
134 def get_partition_index(self, binary):
135 if self.update == 'b':
136 return self.binaries_b.get(binary, None)
137 return self.binaries.get(binary, None)
140 def initialize_parameters(self):
141 logging.debug("Initializing parameterss")
143 for i, p in enumerate(self.part_table):
144 if p['name'] == 'inform':
147 d = "/dev/" + get_partition_device(self.device, n)
149 argv = ['tune2fs', '-O', '^metadata_csum', d]
150 logging.debug(" ".join(argv))
152 stdin=subprocess.DEVNULL,
153 stdout=None, stderr=None)
155 with tempfile.TemporaryDirectory() as mnt:
156 argv = ['mount', '-t', 'ext4', d, mnt]
157 logging.debug(" ".join(argv))
158 proc = subprocess.run(argv,
159 stdin=subprocess.DEVNULL,
160 stdout=None, stderr=None)
161 if proc.returncode != 0:
162 logging.error("Failed to mount {d} in {mnt}")
164 for param, value in self.params:
165 with open(os.path.join(mnt, param), 'w') as f:
166 f.write(value + '\n')
168 logging.debug(" ".join(argv))
170 stdin=subprocess.DEVNULL,
171 stdout=None, stderr=None)
173 class Rpi3(RpiInitParams, SdFusingTarget):
174 long_name = "Raspberry Pi 3"
176 {"size": 64, "fstype": "vfat", "name": "boot", "start": 4, "ptype": "0xe", "bootable": True,
177 "binaries": "boot.img"},
178 {"size": 3072, "fstype": "ext4", "name": "rootfs",
179 "binaries": "rootfs.img"},
180 {"size": 1344, "fstype": "ext4", "name": "system-data",
181 "binaries": "system-data.img"},
182 {"size": None, "ptype": "5", "name": "extended", "start": 4484},
183 {"size": None, "fstype": "ext4", "name": "user",
184 "binaries": "user.img"},
185 {"size": 32, "fstype": "ext4", "name": "modules",
186 "binaries": "modules.img"},
187 {"size": 32, "fstype": "ext4", "name": "ramdisk",
188 "binaries": "ramdisk.img"},
189 {"size": 32, "fstype": "ext4", "name": "ramdisk-recovery",
190 "binaries": "ramdisk-recovery.img"},
191 {"size": 8, "fstype": "ext4", "name": "inform"},
192 {"size": 256, "fstype": "ext4", "name": "hal",
193 "binaries": "hal.img"},
194 {"size": 125, "fstype": "ext4", "name": "reserved2"},
196 params = (('reboot-param.bin', ''),)
198 def __init__(self, device, args):
199 self.reserved_space = 12
200 self.user_partition = 4
201 super().__init__(device, "dos")
203 class Rpi4Super(RpiInitParams, SdFusingTargetAB):
204 long_name = "Raspberry Pi 4 w/ super partition"
206 {"size": 64, "fstype": "vfat", "name": "boot_a","start": 4,
207 "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
208 "binaries": "boot.img"},
209 {"size": 6656, "fstype": "ext4", "name": "super",
210 "binaries": "super.img"},
211 {"size": 1344, "fstype": "ext4", "name": "system-data",
212 "binaries": "system-data.img"},
213 {"size": 36, "fstype": "raw", "name": "none"},
214 {"size": None, "fstype": "ext4", "name": "user",
215 "binaries": "user.img"},
216 {"size": 32, "fstype": "ext4", "name": "module_a",
217 "binaries": "modules.img"},
218 {"size": 32, "fstype": "ext4", "name": "ramdisk_a",
219 "binaries": "ramdisk.img"},
220 {"size": 32, "fstype": "ext4", "name": "ramdisk-recovery_a",
221 "binaries": "ramdisk-recovery.img"},
222 {"size": 8, "fstype": "ext4", "name": "inform"},
223 {"size": 64, "fstype": "vfat", "name": "boot_b",
224 "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
225 "binaries_b": "boot.img"},
226 {"size": 32, "fstype": "ext4", "name": "module_b",
227 "binaries_b": "modules.img"},
228 {"size": 32, "fstype": "ext4", "name": "ramdisk_b",
229 "binaries_b": "ramdisk.img"},
230 {"size": 32, "fstype": "ext4", "name": "ramdisk-recovery_b",
231 "binaries_b": "ramdisk-recovery.img"},
232 {"size": 4, "fstype": "ext4", "name": "reserved0"},
233 {"size": 64, "fstype": "ext4", "name": "reserved1"},
234 {"size": 125, "fstype": "ext4", "name": "reserved2"}
236 params = (('reboot-param.bin', 'norm'),
237 ('reboot-param.info', 'norm'),
238 ('partition-ab.info', 'a'),
239 ('partition-ab-cloned.info', '1'),
240 ('upgrade-status.info', '0'),
241 ('partition-a-status.info', 'ok'),
242 ('partition-b-status.info', 'ok'))
244 def __init__(self, device, args):
245 self.reserved_space = 8
246 self.user_partition = 4
247 self.update = args.update
248 super().__init__(device, "gpt")
249 self.with_super = True
250 self.super_alignment = 1048576
252 class Rpi4(RpiInitParams, SdFusingTargetAB):
253 long_name = "Raspberry Pi 4"
255 {"size": 64, "fstype": "vfat", "name": "boot_a", "start": 4,
256 "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
257 "binaries": "boot.img"},
258 {"size": 3072, "fstype": "ext4", "name": "rootfs_a",
259 "binaries": "rootfs.img"},
260 {"size": 1344, "fstype": "ext4", "name": "system-data",
261 "binaries": "system-data.img"},
262 {"size": 36, "fstype": "raw", "name": "none"},
263 {"size": None, "fstype": "ext4", "name": "user",
264 "binaries": "user.img"},
265 {"size": 32, "fstype": "ext4", "name": "module_a",
266 "binaries": "modules.img"},
267 {"size": 32, "fstype": "ext4", "name": "ramdisk_a",
268 "binaries": "ramdisk.img"},
269 {"size": 32, "fstype": "ext4", "name": "ramdisk-recovery_a",
270 "binaries": "ramdisk-recovery.img"},
271 {"size": 8, "fstype": "ext4", "name": "inform"},
272 {"size": 256, "fstype": "ext4", "name": "hal_a",
273 "binaries": "hal.img"},
274 {"size": 64, "fstype": "vfat", "name": "boot_b",
275 "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
276 "binaries_b": "boot.img"},
277 {"size": 3072, "fstype": "ext4", "name": "rootfs_b",
278 "binaries_b": "rootfs.img"},
279 {"size": 32, "fstype": "ext4", "name": "module_b",
280 "binaries_b": "modules.img"},
281 {"size": 32, "fstype": "ext4", "name": "ramdisk_b",
282 "binaries_b": "ramdisk.img"},
283 {"size": 32, "fstype": "ext4", "name": "ramdisk-recovery_b",
284 "binaries_b": "ramdisk-recovery.img"},
285 {"size": 256, "fstype": "ext4", "name": "hal_b",
286 "binaries_b": "hal.img"},
287 {"size": 4, "fstype": "ext4", "name": "param"},
288 {"size": 64, "fstype": "ext4", "name": "reserved1"},
289 {"size": 125, "fstype": "ext4", "name": "reserved2"},
291 params = (('reboot-param.bin', 'norm'),
292 ('reboot-param.info', 'norm'),
293 ('partition-ab.info', 'a'),
294 ('partition-ab-cloned.info', '1'),
295 ('upgrade-status.info', '0'),
296 ('partition-a-status.info', 'ok'),
297 ('partition-b-status.info', 'ok'))
299 def __init__(self, device, args):
300 self.reserved_space = 5
301 self.user_partition = 4
302 self.update = args.update
303 super().__init__(device, "gpt")
305 class RV64(SdFusingTarget):
306 long_name = "QEMU RISC-V 64-bit"
308 {"size": 2, "fstype": "raw", "name": "SPL", "start": 4,
309 "ptype": "2E54B353-1271-4842-806F-E436D6AF6985",
311 {"size": 4, "fstype": "raw", "name": "u-boot",
312 "ptype": "5B193300-FC78-40CD-8002-E86C45580B47",
313 "binaries": ["u-boot.img", "u-boot.itb"],},
314 {"size": 292, "fstype": "vfat", "name": "boot_a",
315 "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
316 "binaries": "boot.img"},
317 {"size": 36, "fstype": "raw", "name": "none"},
318 {"size": 3072, "fstype": "ext4", "name": "rootfs_a",
319 "binaries": "rootfs.img"},
320 {"size": 1344, "fstype": "ext4", "name": "system-data",
321 "binaries": "system-data.img"},
322 {"size": None, "fstype": "ext4", "name": "user",
323 "binaries": "user.img"},
324 {"size": 32, "fstype": "ext4", "name": "module_a",
325 "binaries": "modules.img"},
326 {"size": 32, "fstype": "ext4", "name": "ramdisk_a",
327 "binaries": "ramdisk.img"},
328 {"size": 32, "fstype": "ext4", "name": "ramdisk-recovery_a",
329 "binaries": "ramdisk-recovery.img"},
330 {"size": 8, "fstype": "raw", "name": "inform"},
331 {"size": 256, "fstype": "ext4", "name": "hal_a",
332 "binaries": "hal.img"},
333 {"size": 4, "fstype": "raw", "name": "reserved0"},
334 {"size": 64, "fstype": "raw", "name": "reserved1"},
335 {"size": 125, "fstype": "raw", "name": "reserved2"},
338 def __init__(self, device, args):
339 self.user_partition = 6
340 self.reserved_space = 5
341 super().__init__(device, 'gpt')
344 long_name = "VisionFive2"
354 def device_size(device):
355 argv = ["sfdisk", "-s", device]
356 logging.debug(" ".join(argv))
357 proc = subprocess.run(argv,
358 stdout=subprocess.PIPE)
359 size = int(proc.stdout.decode('utf-8').strip()) >> 10
360 logging.debug(f"{device} size {size}MiB")
364 proc = subprocess.run(['sfdisk', '-v'],
365 stdout=subprocess.PIPE)
366 version = proc.stdout.decode('utf-8').strip()
367 logging.debug(f"Found {version}")
368 major, minor = [int(x) for x in re.findall('[0-9]+', version)][0:2]
369 support_delete = False
371 if major < 2 or major == 2 and minor < 26:
372 log.error(f"Your sfdisk {major}.{minor}.{patch} is too old.")
374 elif major == 2 and minor >= 28:
375 support_delete = True
377 return True, support_delete
379 def mkpart(args, target):
381 new, support_delete = check_sfdisk()
384 logging.error('sfdisk too old')
387 with open('/proc/self/mounts') as mounts:
388 device_kname = '/dev/' + get_device_kname(Device)
389 device_re = re.compile(device_kname + '[^ ]*')
390 logging.debug(f"Checking for mounted partitions on {device_kname}")
392 match = device_re.match(m)
394 logging.warning('Found mounted device: ' + match[0])
395 argv = ['umount', match[0]]
396 logging.debug(" ".join(argv))
397 proc = subprocess.run(argv)
398 if proc.returncode != 0:
399 logging.error(f"Failed to unmount {match[0]}")
403 logging.info("Removing old partitions")
404 argv = ['sfdisk', '--delete', Device]
405 logging.debug(" ".join(argv))
406 proc = subprocess.run(argv)
407 if proc.returncode != 0:
408 logging.error(f"Failed to remove the old partitions from {Device}")
410 logging.info("Removing old partition table")
411 argv = ['dd', 'if=/dev/zero', 'of=' + Device,
412 'bs=512', 'count=32', 'conv=notrunc']
413 logging.debug(" ".join(argv))
414 proc = subprocess.run(argv)
415 if proc.returncode != 0:
416 logging.error(f"Failed to clear the old partition table on {Device}")
419 logging.debug("New partition table:\n" + str(target.label))
420 argv = ['sfdisk', '--wipe-partitions', 'always', Device]
421 logging.debug(" ".join(argv))
422 proc = subprocess.run(argv,
425 input=str(target.label).encode())
426 if proc.returncode != 0:
427 logging.error(f"Failed to create partition a new table on {Device}")
428 logging.error(f"New partition table:\n" + str(target.label))
431 for i, part in enumerate(target.part_table):
432 d = "/dev/" + get_partition_device(target.device, i+1)
433 if not 'fstype' in part:
434 logging.debug(f"Filesystem not defined for {d}, skipping")
436 logging.debug(f"Formatting {d} as {part['fstype']}")
437 if part['fstype'] == 'vfat':
438 argv = ['mkfs.vfat', '-F', '16', '-n', part['name'], d]
439 logging.debug(" ".join(argv))
440 proc = subprocess.run(argv,
441 stdin=subprocess.DEVNULL,
442 stdout=None, stderr=None)
443 if proc.returncode != 0:
444 log.error(f"Failed to create FAT filesystem on {d}")
446 elif part['fstype'] == 'ext4':
447 argv = ['mkfs.ext4', '-q', '-L', part['name'], d]
448 logging.debug(" ".join(argv))
449 proc = subprocess.run(argv,
450 stdin=subprocess.DEVNULL,
451 stdout=None, stderr=None)
452 if proc.returncode != 0:
453 log.error(f"Failed to create ext4 filesystem on {d}")
455 elif part['fstype'] == 'raw':
457 target.initialize_parameters()
459 def check_args(args):
463 logging.info(f"Device: {args.device}")
465 if args.binaries and len(args.binaries) > 0:
466 logging.info("Fusing binar{}: {}".format("y" if len(args.binaries) == 1 else "ies",
467 ", ".join(args.binaries)))
480 response = input(f"{args.device} will be formatted. Continue? [y/N] ")
481 if response.lower() in ('y', 'yes'):
486 def check_device(args):
492 if os.path.exists(Device):
493 logging.error(f"Failed to create '{Device}', the file alread exists")
496 argv = ["dd", "if=/dev/zero", f"of={Device}",
497 "conv=sparse", "bs=1M", f"count={args.size}"]
498 logging.debug(" ".join(argv))
499 rc = subprocess.run(argv)
500 if rc.returncode != 0:
501 logging.error("Failed to create the backing file")
504 if os.path.isfile(Device):
508 argv = ["losetup", "--show", "--partscan", "--find", f"{File}"]
509 logging.debug(" ".join(argv))
510 proc = subprocess.run(argv,
511 stdout=subprocess.PIPE)
512 Device = proc.stdout.decode('utf-8').strip()
513 if proc.returncode != 0:
514 logging.error(f"Failed to attach {File} to a loopback device")
516 logging.debug(f"Loop device found: {Device}")
517 atexit.register(lambda: subprocess.run(["losetup", "-d", Device]))
521 if not stat.S_ISBLK(s.st_mode):
523 except FileNotFoundError:
524 logging.error(f"No such device: {Device}")
527 logging.error(f"{Device} is not a block device")
530 def check_partition_format(args, target):
535 logging.info(f"Skip formatting of {Device}".format(Device))
537 logging.info(f"Start formatting of {Device}")
539 logging.info(f"{Device} formatted")
541 def check_ddversion():
542 proc = subprocess.run(["dd", "--version"],
543 stdout=subprocess.PIPE)
544 version = proc.stdout.decode('utf-8').split('\n')[0].strip()
545 logging.debug(f"Found {version}")
546 major, minor = (int(x) for x in re.findall('[0-9]+', version))
548 if major < 8 or major == 8 and minor < 24:
553 def get_partition_device(device, idx):
554 argv = ['lsblk', device, '-o', 'TYPE,KNAME']
555 logging.debug(" ".join(argv))
556 proc = subprocess.run(argv,
557 stdout=subprocess.PIPE)
558 if proc.returncode != 0:
559 logging.error("lsblk has failed")
561 part_re = re.compile(f"^part\s+(.*[^0-9]{idx})$")
562 for l in proc.stdout.decode('utf-8').splitlines():
563 match = part_re.match(l)
568 def get_device_kname(device):
569 argv = ['lsblk', device, '-o', 'TYPE,KNAME']
570 logging.debug(" ".join(argv))
571 proc = subprocess.run(argv,
572 stdout=subprocess.PIPE)
573 for l in proc.stdout.decode('utf-8').splitlines():
574 match = re.search(f"^(disk|loop)\s+(.*)", l)
579 def do_fuse_file(f, name, target):
580 idx = target.get_partition_index(name)
582 logging.info(f"No partition defined for {name}, skipping.")
584 pdevice = "/dev/" + get_partition_device(Device, idx)
585 argv = ['dd', 'bs=4M',
590 logging.debug(" ".join(argv))
591 proc_dd = subprocess.Popen(argv,
593 stdin=subprocess.PIPE,
594 stdout=None, stderr=None)
595 logging.info(f"Writing {name} to {pdevice}")
596 buf = f.read(4 << 20)
599 proc_dd.stdin.write(buf)
600 buf = f.read(4 << 20)
601 proc_dd.communicate()
605 #TODO: functions with the target argument should probably
606 # be part of some class
608 def get_aligned_size(size, target):
609 return target.super_alignment*int(1+(size-1)/target.super_alignment)
611 def do_fuse_image_super(tmpd, target):
613 metadata_size = 65536
614 metadata_aligned_size = get_aligned_size(metadata_size, target)
616 hal_path = os.path.join(tmpd, 'hal.img')
617 rootfs_path = os.path.join(tmpd, 'rootfs.img')
618 super_path = os.path.join(tmpd, 'super.img')
621 hal_size = os.stat(hal_path).st_size
622 rootfs_size = os.stat(rootfs_path).st_size
623 except FileNotFoundError as e:
624 fn = os.path.split(e.filename)[-1]
625 logging.warning(f"{fn} is missing, skipping super partition image")
628 hal_aligned_size = get_aligned_size(hal_size, target)
629 rootfs_aligned_size = get_aligned_size(rootfs_size, target)
630 group_size = hal_aligned_size + rootfs_aligned_size
631 super_size = metadata_aligned_size + 2 * group_size
633 argv = ["lpmake", "-F",
635 f"--device-size={super_size}",
636 f"--metadata-size={metadata_size}",
637 f"--metadata-slots={metadata_slots}",
638 "-g", f"tizen_a:{group_size}",
639 "-p", f"rootfs_a:none:{rootfs_aligned_size}:tizen_a",
640 "-p", f"hal_a:none:{hal_aligned_size}:tizen_a",
641 "-g", f"tizen_b:{group_size}",
642 "-p", f"rootfs_b:none:{rootfs_aligned_size}:tizen_b",
643 "-p", f"hal_b:none:{hal_aligned_size}:tizen_b",
644 "-i", f"rootfs_a={rootfs_path}",
645 "-i", f"rootfs_b={rootfs_path}",
646 "-i", f"hal_a={hal_path}",
647 "-i", f"hal_b={hal_path}"]
648 logging.debug(" ".join(argv))
649 proc = subprocess.run(argv,
650 stdin=subprocess.DEVNULL,
651 stdout=None, stderr=None)
653 if proc.returncode != 0:
654 logging.error("Failed to create super.img")
655 do_fuse_image(super_path, target)
657 def do_fuse_image_tarball(tarball, tmpd, target):
658 with tarfile.open(tarball) as tf:
660 if target.with_super:
661 if entry.name in('hal.img', 'rootfs.img'):
662 tf.extract(entry, path=tmpd)
664 f = tf.extractfile(entry)
665 do_fuse_file(f, entry.name, target)
667 def do_fuse_image(img, target):
668 with open(img, 'rb') as f:
669 do_fuse_file(f, os.path.basename(img), target)
671 def fuse_image(args, target):
674 if args.binaries is None or len(args.binaries) == 0:
677 if not Yes and not Format:
678 print(f"The following images will be written to {args.device} and the "
679 "existing data will be lost.\n")
680 for b in args.binaries:
682 response = input("\nContinue? [y/N] ")
683 if not response.lower() in ('y', 'yes'):
686 with tempfile.TemporaryDirectory() as tmpd:
687 for b in args.binaries:
688 if re.search('\.(tar|tar\.gz|tgz)$', b):
689 do_fuse_image_tarball(b, tmpd, target)
691 fn = os.path.split(b)[-1]
692 if target.with_super and fn in ('rootfs.img', 'hal.img'):
693 shutil.copy(b, os.path.join(tmpd, fn))
695 do_fuse_image(b, target)
697 if target.with_super:
698 do_fuse_image_super(tmpd, target)
700 if __name__ == '__main__':
701 parser = argparse.ArgumentParser(description="For {}, version {}".format(
702 ", ".join([v.long_name for k,v in TARGETS.items()]),
705 parser.add_argument("-b", "--binary", action="extend", dest="binaries",
707 help="binary to flash, may be used multiple times")
708 parser.add_argument("--create", action="store_true",
709 help="create the backing file and format the loopback device")
710 parser.add_argument("--debug", action="store_const", const="debug",
711 default="warning", dest="log_level",
712 help="set log level to DEBUG")
713 parser.add_argument("-d", "--device",
714 help="device node or loopback backing file")
715 parser.add_argument("--format", action="store_true",
716 help="create new partition table on the target device")
717 parser.add_argument("--log-level", dest="log_level", default="warning",
718 help="Verbosity, possible values: debug, info, warning, "
719 "error, critical (default: warning)")
720 parser.add_argument("--size", type=int, default=8192,
721 help="size of the backing file to create (in MiB)")
722 parser.add_argument("-t", "--target", required=True,
723 help="Target device model. Use `--target list`"
724 " to show supported devices.")
725 parser.add_argument("--update", choices=['a', 'b'], default=None,
726 help="Choose partition set to update: a or b.")
727 parser.add_argument("--version", action="version",
728 version=f"%(prog)s {__version__}")
729 parser.add_argument("--YES", action="store_true",
730 help="agree to destroy data on the DEVICE")
731 args = parser.parse_args()
733 if args.target == 'list':
734 print("\nSupported devices:\n")
735 for k,v in TARGETS.items():
736 print(f" {k:6} {v.long_name}")
739 if args.device is None:
740 parser.error('-d/--device argument is required for normal operation')
742 conh = ColorStreamHandler(format='%(asctime)s.%(msecs)d %(debuginfo)s%(levelname)-8s %(message)s',
743 cformat='%(asctime)s.%(msecs)d %(debuginfo)s%(levelcolor)s%(message)s',
744 datefmt='%Y-%m-%dT%H:%M:%S')
745 log_handlers = [conh]
746 logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
747 handlers=log_handlers,
748 level=args.log_level.upper())
750 logging.debug(" ".join(sys.argv))
754 target = TARGETS[args.target](Device, args)
756 check_partition_format(args, target)
757 fuse_image(args, target)
758 subprocess.run(['sync'],
759 stdin=subprocess.DEVNULL,
760 stdout=None, stderr=None )