scripts: tizen: sd_fusing.py: Display an information during flashing
[platform/kernel/u-boot.git] / scripts / tizen / sd_fusing.py
1 #!/usr/bin/env python3
2
3 from functools import reduce
4
5 import argparse
6 import atexit
7 import errno
8 import logging
9 import os
10 import re
11 import shutil
12 import stat
13 import subprocess
14 import sys
15 import tarfile
16 import tempfile
17
18 __version__ = "1.0.0"
19
20 Format = False
21 Device = ""
22 File = ""
23 Yes = False
24
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)
29         else:
30             record.debuginfo = ''
31         return logging.Formatter.format(self, record)
32
33 class ColorFormatter(DebugFormatter):
34     _levelToColor = {
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"
41     }
42     def format(self, record):
43         record.levelcolor = self._levelToColor[record.levelno]
44         record.msg = record.msg
45         return super().format(record)
46
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"
53         else:
54             self.formatter = DebugFormatter(format, datefmt, style)
55
56 class Partition:
57     def __init__(self, name, size, start=None, ptype=None, fstype="raw", bootable=False, **kwargs):
58         self.name = name
59         self.size = size
60         self.start = start
61         self.ptype = ptype
62         self.bootable = bootable
63     def __str__(self):
64         output = []
65         if self.start:
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")
69         if self.name:
70             output.append(f"name={self.name}")
71         output.append(f"type={self.ptype}")
72         if self.bootable:
73                        output.append("bootable")
74         return ", ".join(output) + "\n"
75
76 class Label:
77     def __init__(self, part_table, ltype):
78         self.ltype = ltype
79         if ltype == 'gpt':
80             ptype = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
81         elif ltype == 'dos':
82             ptype = '83'
83         self.part_table = []
84         for part in part_table:
85             part["ptype"] = part.get("ptype", ptype)
86             self.part_table.append(Partition(**part))
87     def __str__(self):
88         output = f"label: {self.ltype}\n"
89         for part in self.part_table:
90             output += str(part)
91         return output
92
93 class SdFusingTarget:
94     def __init__(self, device, ltype):
95         # TODO: make a copy of a sublcass part_table
96         self.with_super = False
97         self.device = device
98         total_size = device_size(device)
99
100         if hasattr(self, 'user_partition'):
101             self.user_size = total_size - self.reserved_space - \
102                 reduce(lambda x, y: x + (y["size"] or 0), self.part_table, 0)
103             if self.user_size < 100:
104                 logging.error(f"Not enough space for user data ({self.user_size}). Use larger storage.")
105                 raise OSError(errno.ENOSPC, os.strerror(errno.ENOSPC), device)
106             # self.user_partition counts from 0
107             self.part_table[self.user_partition]["size"] = self.user_size
108
109         self.label = Label(self.part_table, ltype)
110         self.binaries = self._get_binaries('binaries')
111
112     def apply_partition_sizes(self, partition_sizes):
113         if partition_sizes is None or len(partition_sizes) == 0:
114             return 0
115         resized_total = 0
116         for name, size in partition_sizes.items():
117             resized_count = 0
118             for part in self.part_table:
119                 if part['name'] == name:
120                     psize = part['size']
121                     part['size'] = size
122                     logging.debug(f"overriding partition:{name}, old-size:{psize} MiB new-size:{size} MiB")
123                     resized_count = resized_count + 1
124             if resized_count == 0:
125                 logging.error(f"partition:{name} not found when attempting to apply_partition_sizes")
126             resized_total = resized_total + resized_count
127         return resized_total
128
129     def _get_binaries(self, key):
130         binaries = {}
131         for i, p in enumerate(self.part_table):
132             b = p.get(key, None)
133             if b is None:
134                 continue
135             if isinstance(b, str):
136                 binaries[b] = i + 1
137             elif isinstance(b, list):
138                 for f in b:
139                     binaries[f] = i + 1
140         return binaries
141
142     def get_partition_index(self, binary):
143         return self.binaries.get(binary, None)
144
145     params = ()
146     def initialize_parameters(self):
147         pass
148
149 class SdFusingTargetAB(SdFusingTarget):
150     def __init__(self, device, ltype):
151         super().__init__(device, ltype)
152         self.binaries_b = self._get_binaries('binaries_b')
153
154     def get_partition_index(self, binary):
155         if self.update == 'b':
156             return self.binaries_b.get(binary, None)
157         return self.binaries.get(binary, None)
158
159 class InitParams:
160     def initialize_parameters(self):
161         logging.debug("Initializing parameterss")
162         n = None
163         for i, p in enumerate(self.part_table):
164             if p['name'] == 'inform':
165                 n = i + 1;
166                 break
167         d = "/dev/" + get_partition_device(self.device, n)
168
169         argv = ['tune2fs', '-O', '^metadata_csum', d]
170         logging.debug(" ".join(argv))
171         subprocess.run(argv,
172                        stdin=subprocess.DEVNULL,
173                        stdout=None, stderr=None)
174
175         with tempfile.TemporaryDirectory() as mnt:
176             argv = ['mount', '-t', 'ext4', d, mnt]
177             logging.debug(" ".join(argv))
178             proc = subprocess.run(argv,
179                                   stdin=subprocess.DEVNULL,
180                                   stdout=None, stderr=None)
181             if proc.returncode != 0:
182                 logging.error("Failed to mount {d} in {mnt}")
183                 return
184             for param, value in self.params:
185                 with open(os.path.join(mnt, param), 'w') as f:
186                     f.write(value + '\n')
187             argv = ['umount', d]
188             logging.debug(" ".join(argv))
189             subprocess.run(argv,
190                            stdin=subprocess.DEVNULL,
191                            stdout=None, stderr=None)
192
193 class Rpi3(InitParams, SdFusingTarget):
194     long_name = "Raspberry Pi 3"
195     part_table = [
196         {"size": 64,   "fstype": "vfat", "name": "boot", "start": 4, "ptype": "0xe", "bootable": True,
197          "binaries": "boot.img"},
198         {"size": 3072, "fstype": "ext4", "name": "rootfs",
199          "binaries": "rootfs.img"},
200         {"size": 1344, "fstype": "ext4", "name": "system-data",
201          "binaries": "system-data.img"},
202         {"size": None, "ptype":  "5",    "name": "extended", "start": 4484},
203         {"size": None, "fstype": "ext4", "name": "user",
204          "binaries": "user.img"},
205         {"size": 32,   "fstype": "ext4", "name": "modules",
206          "binaries": "modules.img"},
207         {"size": 32,   "fstype": "ext4", "name": "ramdisk",
208          "binaries": "ramdisk.img"},
209         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery",
210          "binaries": "ramdisk-recovery.img"},
211         {"size": 8,    "fstype": "ext4", "name": "inform"},
212         {"size": 256,  "fstype": "ext4", "name": "hal",
213          "binaries": "hal.img"},
214         {"size": 125,  "fstype": "ext4", "name": "reserved2"},
215     ]
216     params = (('reboot-param.bin', ''),)
217
218     def __init__(self, device, args):
219         self.reserved_space = 12
220         self.user_partition = 4
221         super().__init__(device, "dos")
222
223 class Rpi4Super(InitParams, SdFusingTargetAB):
224     long_name = "Raspberry Pi 4 w/ super partition"
225     part_table = [
226         {"size": 64,   "fstype": "vfat", "name": "boot_a","start": 4,
227          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
228          "binaries": "boot.img"},
229         {"size": 6656, "fstype": "ext4", "name": "super",
230          "binaries": "super.img"},
231         {"size": 1344, "fstype": "ext4", "name": "system-data",
232          "binaries": "system-data.img"},
233         {"size": 36,   "fstype": "raw",  "name": "none"},
234         {"size": None, "fstype": "ext4", "name": "user",
235          "binaries": "user.img"},
236         {"size": 32,   "fstype": "ext4", "name": "module_a",
237          "binaries": "modules.img"},
238         {"size": 32,   "fstype": "ext4", "name": "ramdisk_a",
239          "binaries": "ramdisk.img"},
240         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a",
241          "binaries": "ramdisk-recovery.img"},
242         {"size": 8,    "fstype": "ext4", "name": "inform"},
243         {"size": 64,   "fstype": "vfat", "name": "boot_b",
244          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
245          "binaries_b": "boot.img"},
246         {"size": 32,   "fstype": "ext4", "name": "module_b",
247          "binaries_b": "modules.img"},
248         {"size": 32,   "fstype": "ext4", "name": "ramdisk_b",
249          "binaries_b": "ramdisk.img"},
250         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_b",
251          "binaries_b": "ramdisk-recovery.img"},
252         {"size": 4,    "fstype": "ext4", "name": "reserved0"},
253         {"size": 64,   "fstype": "ext4", "name": "reserved1"},
254         {"size": 125,  "fstype": "ext4", "name": "reserved2"}
255     ]
256     params = (('reboot-param.bin', 'norm'),
257               ('reboot-param.info', 'norm'),
258               ('partition-ab.info', 'a'),
259               ('partition-ab-cloned.info', '1'),
260               ('upgrade-status.info', '0'),
261               ('partition-a-status.info', 'ok'),
262               ('partition-b-status.info', 'ok'))
263
264     def __init__(self, device, args):
265         self.reserved_space = 8
266         self.user_partition = 4
267         self.update = args.update
268         super().__init__(device, "gpt")
269         self.with_super = True
270         self.super_alignment = 1048576
271
272 class Rpi4(InitParams, SdFusingTargetAB):
273     long_name = "Raspberry Pi 4"
274     part_table = [
275         {"size": 64,   "fstype": "vfat", "name": "boot_a", "start": 4,
276          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
277          "binaries": "boot.img"},
278         {"size": 3072, "fstype": "ext4", "name": "rootfs_a",
279          "binaries": "rootfs.img"},
280         {"size": 1344, "fstype": "ext4", "name": "system-data",
281          "binaries": "system-data.img"},
282         {"size": 36,   "fstype": "raw",  "name": "none"},
283         {"size": None, "fstype": "ext4", "name": "user",
284          "binaries": "user.img"},
285         {"size": 32,   "fstype": "ext4", "name": "module_a",
286          "binaries": "modules.img"},
287         {"size": 32,   "fstype": "ext4", "name": "ramdisk_a",
288          "binaries": "ramdisk.img"},
289         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a",
290          "binaries": "ramdisk-recovery.img"},
291         {"size": 8,    "fstype": "ext4", "name": "inform"},
292         {"size": 256,  "fstype": "ext4", "name": "hal_a",
293          "binaries": "hal.img"},
294         {"size": 64,   "fstype": "vfat", "name": "boot_b",
295          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
296          "binaries_b": "boot.img"},
297         {"size": 3072, "fstype": "ext4", "name": "rootfs_b",
298          "binaries_b": "rootfs.img"},
299         {"size": 32,   "fstype": "ext4", "name": "module_b",
300          "binaries_b": "modules.img"},
301         {"size": 32,   "fstype": "ext4", "name": "ramdisk_b",
302          "binaries_b": "ramdisk.img"},
303         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_b",
304          "binaries_b": "ramdisk-recovery.img"},
305         {"size": 256,  "fstype": "ext4", "name": "hal_b",
306          "binaries_b": "hal.img"},
307         {"size": 4,    "fstype": "ext4", "name": "param"},
308         {"size": 64,  "fstype": "ext4", "name": "reserved1"},
309         {"size": 125,  "fstype": "ext4", "name": "reserved2"},
310     ]
311     params = (('reboot-param.bin', 'norm'),
312               ('reboot-param.info', 'norm'),
313               ('partition-ab.info', 'a'),
314               ('partition-ab-cloned.info', '1'),
315               ('upgrade-status.info', '0'),
316               ('partition-a-status.info', 'ok'),
317               ('partition-b-status.info', 'ok'))
318
319     def __init__(self, device, args):
320         self.reserved_space = 5
321         self.user_partition = 4
322         self.update = args.update
323         super().__init__(device, "gpt")
324
325 class Rpi4AoT(InitParams, SdFusingTargetAB):
326     long_name = "Raspberry Pi 4 for AoT"
327     part_table = [
328         {"size": 64,   "fstype": "vfat", "name": "boot_a", "start": 4,
329          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
330          "binaries": "boot.img"},
331         {"size": 3072, "fstype": "ext4", "name": "rootfs_a",
332          "binaries": "rootfs.img"},
333         {"size": 1344, "fstype": "ext4", "name": "system-data",
334          "binaries": "system-data.img"},
335         {"size": 36,   "fstype": "raw",  "name": "none"},
336         {"size": None, "fstype": "ext4", "name": "user",
337          "binaries": "user.img"},
338         {"size": 32,   "fstype": "ext4", "name": "module_a",
339          "binaries": "modules.img"},
340         {"size": 32,   "fstype": "ext4", "name": "ramdisk_a",
341          "binaries": "ramdisk.img"},
342         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a",
343          "binaries": "ramdisk-recovery.img"},
344         {"size": 8,    "fstype": "ext4", "name": "inform"},
345         {"size": 256,  "fstype": "ext4", "name": "hal_a",
346          "binaries": "hal.img"},
347         {"size": 64,   "fstype": "vfat", "name": "boot_b",
348          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
349          "binaries_b": "boot.img"},
350         {"size": 3072, "fstype": "ext4", "name": "rootfs_b",
351          "binaries_b": "rootfs.img"},
352         {"size": 32,   "fstype": "ext4", "name": "module_b",
353          "binaries_b": "modules.img"},
354         {"size": 32,   "fstype": "ext4", "name": "ramdisk_b",
355          "binaries_b": "ramdisk.img"},
356         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_b",
357          "binaries_b": "ramdisk-recovery.img"},
358         {"size": 256,  "fstype": "ext4", "name": "hal_b",
359          "binaries_b": "hal.img"},
360         {"size": 1536,  "fstype": "ext4", "name": "aot-system_a",
361          "binaries": "system.img"},
362         {"size": 1536,  "fstype": "ext4", "name": "aot-system_b",
363          "binaries_b": "system.img"},
364         {"size": 256,  "fstype": "ext4", "name": "aot-vendor_a",
365          "binaries": "vendor.img"},
366         {"size": 256,  "fstype": "ext4", "name": "aot-vendor_b",
367          "binaries_b": "vendor.img"},
368         {"size": 4,    "fstype": "ext4", "name": "param"},
369         {"size": 64,  "fstype": "ext4", "name": "reserved1"},
370         {"size": 125,  "fstype": "ext4", "name": "reserved2"},
371     ]
372     params = (('reboot-param.bin', 'norm'),
373               ('reboot-param.info', 'norm'),
374               ('partition-ab.info', 'a'),
375               ('partition-ab-cloned.info', '1'),
376               ('upgrade-status.info', '0'),
377               ('partition-a-status.info', 'ok'),
378               ('partition-b-status.info', 'ok'))
379
380     def __init__(self, device, args):
381         self.reserved_space = 5
382         self.user_partition = 4
383         self.update = args.update
384         super().__init__(device, "gpt")
385
386 class RV64(InitParams, SdFusingTarget):
387     long_name = "QEMU RISC-V 64-bit"
388     part_table = [
389         {"size": 2,    "fstype": "raw",  "name": "SPL", "start": 4,
390          "ptype": "2E54B353-1271-4842-806F-E436D6AF6985",
391          "binaries": ""},
392         {"size": 4,    "fstype": "raw",  "name": "u-boot",
393          "ptype": "5B193300-FC78-40CD-8002-E86C45580B47",
394          "binaries": ["u-boot.img", "u-boot.itb"],},
395         {"size": 292,  "fstype": "vfat", "name": "boot_a",
396          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
397          "binaries": "boot.img"},
398         {"size": 36,   "fstype": "raw",  "name": "none"},
399         {"size": 3072, "fstype": "ext4", "name": "rootfs_a",
400          "binaries": "rootfs.img"},
401         {"size": 1344, "fstype": "ext4", "name": "system-data",
402          "binaries": "system-data.img"},
403         {"size": None, "fstype": "ext4", "name": "user",
404          "binaries": "user.img"},
405         {"size": 32,   "fstype": "ext4", "name": "module_a",
406          "binaries": "modules.img"},
407         {"size": 32,   "fstype": "ext4", "name": "ramdisk_a",
408          "binaries": "ramdisk.img"},
409         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a",
410          "binaries": "ramdisk-recovery.img"},
411         {"size": 8,    "fstype": "ext4", "name": "inform"},
412         {"size": 256,  "fstype": "ext4", "name": "hal_a",
413          "binaries": "hal.img"},
414         {"size": 4,    "fstype": "raw",  "name": "reserved0"},
415         {"size": 64,   "fstype": "raw",  "name": "reserved1"},
416         {"size": 125,  "fstype": "raw",  "name": "reserved2"},
417     ]
418     params = (('reboot-param.bin', 'norm'),
419               ('reboot-param.info', 'norm'))
420
421     def __init__(self, device, args):
422         self.user_partition = 6
423         self.reserved_space = 5
424         self.apply_partition_sizes(args.partition_sizes)
425         super().__init__(device, 'gpt')
426
427 class VF2(InitParams, SdFusingTarget):
428     long_name = "VisionFive2"
429     part_table = [
430         {"size": 2,    "fstype": "raw",  "name": "SPL", "start": 4,
431          "ptype": "2E54B353-1271-4842-806F-E436D6AF6985",
432          "binaries": ["u-boot-spl.bin.normal.out"],},
433         {"size": 4,    "fstype": "raw",  "name": "u-boot",
434          "ptype": "5B193300-FC78-40CD-8002-E86C45580B47",
435          "binaries": ["u-boot.img", "u-boot.itb"],},
436         {"size": 292,  "fstype": "vfat", "name": "boot_a",
437          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
438          "binaries": "boot.img"},
439         {"size": 36,   "fstype": "raw",  "name": "none"},
440         {"size": 3072, "fstype": "ext4", "name": "rootfs_a",
441          "binaries": "rootfs.img"},
442         {"size": 1344, "fstype": "ext4", "name": "system-data",
443          "binaries": "system-data.img"},
444         {"size": None, "fstype": "ext4", "name": "user",
445          "binaries": "user.img"},
446         {"size": 32,   "fstype": "ext4", "name": "module_a",
447          "binaries": "modules.img"},
448         {"size": 32,   "fstype": "ext4", "name": "ramdisk_a",
449          "binaries": "ramdisk.img"},
450         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a",
451          "binaries": "ramdisk-recovery.img"},
452         {"size": 8,    "fstype": "ext4", "name": "inform"},
453         {"size": 256,  "fstype": "ext4", "name": "hal_a",
454          "binaries": "hal.img"},
455         {"size": 4,    "fstype": "raw",  "name": "reserved0"},
456         {"size": 64,   "fstype": "raw",  "name": "reserved1"},
457         {"size": 125,  "fstype": "raw",  "name": "reserved2"},
458     ]
459     params = (('reboot-param.bin', 'norm'),
460               ('reboot-param.info', 'norm'))
461
462     def __init__(self, device, args):
463         self.user_partition = 6
464         self.reserved_space = 5
465         self.apply_partition_sizes(args.partition_sizes)
466         super().__init__(device, 'gpt')
467
468 class X86emu(SdFusingTarget):
469     part_table = [
470         {"size": 512,    "fstype": "vfat",  "name": "EFI", "start": 4,
471          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
472          "binaries": ""},
473         {"size": 512,    "fstype": "ext2",  "name": "boot",
474          "binaries": "emulator-boot.img",},
475         {"size": 2048,  "fstype": "ext4", "name": "rootfs",
476          "binaries": "emulator-rootfs.img"},
477         {"size": 1344, "fstype": "ext4", "name": "system-data",
478          "binaries": "emulator-sysdata.img"},
479         {"size": 1024, "fstype": "swap", "name": "emulator-swap",
480          "ptype": "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F"},
481     ]
482
483     def __init__(self, device, args):
484         super().__init__(device, 'gpt')
485         for p in self.label.part_table:
486             if p.name == "rootfs":
487                 p.ptype = args._rootfs_uuid
488                 break
489
490 class X86emu32(X86emu):
491     long_name = "QEMU x86 32-bit"
492
493     def __init__(self, device, args):
494         setattr(args, "_rootfs_uuid", "44479540-F297-41B2-9AF7-D131D5F0458A")
495         super().__init__(device, args)
496
497 class X86emu64(X86emu):
498     long_name = "QEMU x86 64-bit"
499
500     def __init__(self, device, args):
501         setattr(args, "_rootfs_uuid", "44479540-F297-41B2-9AF7-D131D5F0458A")
502         super().__init__(device, args)
503
504 TARGETS = {
505     'rpi3': Rpi3,
506     'rpi4': Rpi4,
507     'rpi4s': Rpi4Super,
508     'rpi4aot': Rpi4AoT,
509     'vf2': VF2,
510     'rv64': RV64,
511     'x86emu32': X86emu32,
512     'x86emu64': X86emu64,
513 }
514
515 def device_size(device):
516     argv = ["sfdisk", "-s", device]
517     logging.debug(" ".join(argv))
518     proc = subprocess.run(argv,
519                           stdout=subprocess.PIPE)
520     size = int(proc.stdout.decode('utf-8').strip()) >> 10
521     logging.debug(f"{device} size {size}MiB")
522     return size
523
524 def check_sfdisk():
525     proc = subprocess.run(['sfdisk', '-v'],
526                           stdout=subprocess.PIPE)
527     version = proc.stdout.decode('utf-8').strip()
528     logging.debug(f"Found {version}")
529     major, minor = [int(x) for x in re.findall('[0-9]+', version)][0:2]
530     support_delete = False
531
532     if major < 2 or major == 2 and minor < 26:
533         log.error(f"Your sfdisk {major}.{minor}.{patch} is too old.")
534         return False,False
535     elif major == 2 and minor >= 28:
536         support_delete = True
537
538     return True, support_delete
539
540 def mkpart(args, target):
541     global Device
542     new, support_delete = check_sfdisk()
543
544     if not new:
545         logging.error('sfdisk too old')
546         sys.exit(1)
547
548     with open('/proc/self/mounts') as mounts:
549         device_kname = '/dev/' + get_device_kname(Device)
550         device_re = re.compile(device_kname + '[^ ]*')
551         logging.debug(f"Checking for mounted partitions on {device_kname}")
552         for m in mounts:
553             match = device_re.match(m)
554             if match:
555                 logging.warning('Found mounted device: ' + match[0])
556                 argv = ['umount', match[0]]
557                 logging.debug(" ".join(argv))
558                 proc = subprocess.run(argv)
559                 if proc.returncode != 0:
560                     logging.error(f"Failed to unmount {match[0]}")
561                     sys.exit(1)
562
563     if support_delete:
564         logging.info("Removing old partitions")
565         argv = ['sfdisk', '--delete', Device]
566         logging.debug(" ".join(argv))
567         proc = subprocess.run(argv)
568         if proc.returncode != 0:
569             logging.error(f"Failed to remove the old partitions from {Device}")
570     else:
571         logging.info("Removing old partition table")
572         argv = ['dd', 'if=/dev/zero', 'of=' + Device,
573                 'bs=512', 'count=32', 'conv=notrunc']
574         logging.debug(" ".join(argv))
575         proc = subprocess.run(argv)
576         if proc.returncode != 0:
577             logging.error(f"Failed to clear the old partition table on {Device}")
578             sys.exit(1)
579
580     logging.debug("New partition table:\n" + str(target.label))
581     argv = ['sfdisk', '--wipe-partitions', 'always', Device]
582     logging.debug(" ".join(argv))
583     proc = subprocess.run(argv,
584                           stdout=None,
585                           stderr=None,
586                           input=str(target.label).encode())
587     if proc.returncode != 0:
588         logging.error(f"Failed to create partition a new table on {Device}")
589         logging.error(f"New partition table:\n" + str(target.label))
590         sys.exit(1)
591
592     for i, part in enumerate(target.part_table):
593         d = "/dev/" + get_partition_device(target.device, i+1)
594         if not 'fstype' in part:
595             logging.debug(f"Filesystem not defined for {d}, skipping")
596             continue
597         logging.debug(f"Formatting {d} as {part['fstype']}")
598         if part['fstype'] == 'vfat':
599             argv = ['mkfs.vfat', '-F', '16', '-n', part['name'], d]
600             logging.debug(" ".join(argv))
601             proc = subprocess.run(argv,
602                                   stdin=subprocess.DEVNULL,
603                                   stdout=None, stderr=None)
604             if proc.returncode != 0:
605                 log.error(f"Failed to create FAT filesystem on {d}")
606                 sys.exit(1)
607         elif part['fstype'] == 'ext4':
608             argv = ['mkfs.ext4', '-q', '-L', part['name'], d]
609             logging.debug(" ".join(argv))
610             proc = subprocess.run(argv,
611                                   stdin=subprocess.DEVNULL,
612                                   stdout=None, stderr=None)
613             if proc.returncode != 0:
614                 log.error(f"Failed to create ext4 filesystem on {d}")
615                 sys.exit(1)
616         elif part['fstype'] == 'swap':
617             argv = ['mkswap', '-L', part['name'], d]
618             logging.debug(" ".join(argv))
619             proc = subprocess.run(argv,
620                                   stdin=subprocess.DEVNULL,
621                                   stdout=None, stderr=None)
622             if proc.returncode != 0:
623                 log.error(f"Failed to format swap partition {d}")
624                 sys.exit(1)
625         elif part['fstype'] == 'raw':
626             pass
627     target.initialize_parameters()
628
629 def check_args(args):
630     global Format
631     global Yes
632
633     logging.info(f"Device: {args.device}")
634
635     if args.binaries and len(args.binaries) > 0:
636         logging.info("Fusing binar{}: {}".format("y" if len(args.binaries) == 1 else "ies",
637                      ", ".join(args.binaries)))
638
639     if args.YES:
640         Yes = True
641
642     if args.create:
643         Format = True
644         Yes = True
645
646     if args.format:
647         if Yes:
648             Format = True
649         else:
650             response = input(f"{args.device} will be formatted. Continue? [y/N] ")
651             if response.lower() in ('y', 'yes'):
652                 Format = True
653             else:
654                 Format = False
655
656 def check_device(args):
657     global Format
658     global Device
659     Device = args.device
660
661     if args.create:
662         if os.path.exists(Device):
663             logging.error(f"Failed to create '{Device}', the file alread exists")
664             sys.exit(1)
665         else:
666             argv = ["dd", "if=/dev/zero", f"of={Device}",
667                     "conv=sparse", "bs=1M", f"count={args.size}"]
668             logging.debug(" ".join(argv))
669             rc = subprocess.run(argv)
670             if rc.returncode != 0:
671                 logging.error("Failed to create the backing file")
672                 sys.exit(1)
673
674     if os.path.isfile(Device):
675         global File
676         File = Device
677
678         argv = ["losetup", "--show", "--partscan", "--find", f"{File}"]
679         logging.debug(" ".join(argv))
680         proc = subprocess.run(argv,
681                               stdout=subprocess.PIPE)
682         Device = proc.stdout.decode('utf-8').strip()
683         if proc.returncode != 0:
684             logging.error(f"Failed to attach {File} to a loopback device")
685             sys.exit(1)
686         logging.debug(f"Loop device found: {Device}")
687         atexit.register(lambda: subprocess.run(["losetup", "-d", Device]))
688
689     try:
690         s = os.stat(Device)
691         if not stat.S_ISBLK(s.st_mode):
692             raise TypeError
693     except FileNotFoundError:
694         logging.error(f"No such device: {Device}")
695         sys.exit(1)
696     except TypeError:
697         logging.error(f"{Device} is not a block device")
698         sys.exit(1)
699
700 def check_partition_format(args, target):
701     global Format
702     global Device
703
704     if not Format:
705         logging.info(f"Skip formatting of {Device}".format(Device))
706         return
707     logging.info(f"Start formatting of {Device}")
708     mkpart(args, target)
709     logging.info(f"{Device} formatted")
710
711 def check_ddversion():
712     proc = subprocess.run(["dd", "--version"],
713                             stdout=subprocess.PIPE)
714     version = proc.stdout.decode('utf-8').split('\n')[0].strip()
715     logging.debug(f"Found {version}")
716     major, minor = (int(x) for x in re.findall('[0-9]+', version))
717
718     if major < 8 or major == 8 and minor < 24:
719         return False
720
721     return True
722
723 def get_partition_device(device, idx):
724     argv = ['lsblk', device, '-o', 'TYPE,KNAME']
725     logging.debug(" ".join(argv))
726     proc = subprocess.run(argv,
727                           stdout=subprocess.PIPE)
728     if proc.returncode != 0:
729         logging.error("lsblk has failed")
730         return None
731     part_re = re.compile(f"^part\s+(.*[^0-9]{idx})$")
732     for l in proc.stdout.decode('utf-8').splitlines():
733         match = part_re.match(l)
734         if match:
735             return match[1]
736     return None
737
738 def get_device_kname(device):
739     argv = ['lsblk', device, '-o', 'TYPE,KNAME']
740     logging.debug(" ".join(argv))
741     proc = subprocess.run(argv,
742                           stdout=subprocess.PIPE)
743     for l in proc.stdout.decode('utf-8').splitlines():
744         match = re.search(f"^(disk|loop)\s+(.*)", l)
745         if match:
746             return match[2]
747     return None
748
749 def do_fuse_file(f, name, target):
750     idx = target.get_partition_index(name)
751     if idx is None:
752         logging.info(f"No partition defined for {name}, skipping.")
753         return
754     pdevice = "/dev/" + get_partition_device(Device, idx)
755     argv = ['dd', 'bs=4M',
756             'oflag=direct',
757             'iflag=fullblock',
758             'conv=nocreat',
759             'status=progress',
760             f"of={pdevice}"]
761     logging.debug(" ".join(argv))
762     proc_dd = subprocess.Popen(argv,
763                                bufsize=(4 << 20),
764                                stdin=subprocess.PIPE,
765                                stdout=None, stderr=None)
766     print('\033[32m'+f"Writing {name} to {pdevice}"+'\033[0m')
767     buf = f.read(4 << 20)
768     while len(buf) > 0:
769         proc_dd.stdin.write(buf)
770         buf = f.read(4 << 20)
771     proc_dd.communicate()
772     logging.info("Done")
773     #TODO: verification
774
775 #TODO: functions with the target argument should probably
776 #      be part of some class
777
778 def get_aligned_size(size, target):
779     return target.super_alignment*int(1+(size-1)/target.super_alignment)
780
781 def do_fuse_image_super(tmpd, target):
782     metadata_slots = 2
783     metadata_size = 65536
784     metadata_aligned_size = get_aligned_size(metadata_size, target)
785
786     hal_path = os.path.join(tmpd, 'hal.img')
787     rootfs_path = os.path.join(tmpd, 'rootfs.img')
788     super_path = os.path.join(tmpd, 'super.img')
789
790     try:
791         hal_size = os.stat(hal_path).st_size
792         rootfs_size = os.stat(rootfs_path).st_size
793     except FileNotFoundError as e:
794         fn = os.path.split(e.filename)[-1]
795         logging.warning(f"{fn} is missing, skipping super partition image")
796         return
797
798     hal_aligned_size = get_aligned_size(hal_size, target)
799     rootfs_aligned_size = get_aligned_size(rootfs_size, target)
800     group_size = hal_aligned_size + rootfs_aligned_size
801     super_size = metadata_aligned_size + 2 * group_size
802
803     argv = ["lpmake", "-F",
804             f"-o={super_path}",
805             f"--device-size={super_size}",
806             f"--metadata-size={metadata_size}",
807             f"--metadata-slots={metadata_slots}",
808             "-g", f"tizen_a:{group_size}",
809             "-p", f"rootfs_a:none:{rootfs_aligned_size}:tizen_a",
810             "-p", f"hal_a:none:{hal_aligned_size}:tizen_a",
811             "-g", f"tizen_b:{group_size}",
812             "-p", f"rootfs_b:none:{rootfs_aligned_size}:tizen_b",
813             "-p", f"hal_b:none:{hal_aligned_size}:tizen_b",
814             "-i", f"rootfs_a={rootfs_path}",
815             "-i", f"rootfs_b={rootfs_path}",
816             "-i", f"hal_a={hal_path}",
817             "-i", f"hal_b={hal_path}"]
818     logging.debug(" ".join(argv))
819     proc = subprocess.run(argv,
820                           stdin=subprocess.DEVNULL,
821                           stdout=None, stderr=None)
822
823     if proc.returncode != 0:
824         logging.error("Failed to create super.img")
825     do_fuse_image(super_path, target)
826
827 def do_fuse_image_tarball(tarball, tmpd, target):
828     with tarfile.open(tarball) as tf:
829         for entry in tf:
830             if target.with_super:
831                 if entry.name in('hal.img', 'rootfs.img'):
832                     tf.extract(entry, path=tmpd)
833                     continue
834             f = tf.extractfile(entry)
835             do_fuse_file(f, entry.name, target)
836
837 def do_fuse_image(img, target):
838     with open(img, 'rb') as f:
839         do_fuse_file(f, os.path.basename(img), target)
840
841 def fuse_image(args, target):
842     global Yes
843
844     if args.binaries is None or len(args.binaries) == 0:
845         return
846
847     if not Yes and not Format:
848         print(f"The following images will be written to {args.device} and the "
849               "existing data will be lost.\n")
850         for b in args.binaries:
851             print("  " + b)
852         response = input("\nContinue? [y/N] ")
853         if not response.lower() in ('y', 'yes'):
854             return
855
856     with tempfile.TemporaryDirectory() as tmpd:
857         for b in args.binaries:
858             if re.search('\.(tar|tar\.gz|tgz)$', b):
859                 do_fuse_image_tarball(b, tmpd, target)
860             else:
861                 fn = os.path.split(b)[-1]
862                 if target.with_super and fn in ('rootfs.img', 'hal.img'):
863                     shutil.copy(b, os.path.join(tmpd, fn))
864                 else:
865                     do_fuse_image(b, target)
866
867         if target.with_super:
868             do_fuse_image_super(tmpd, target)
869
870 if __name__ == '__main__':
871     parser = argparse.ArgumentParser(description="For {}, version {}".format(
872         ", ".join([v.long_name for k,v in TARGETS.items()]),
873         __version__
874     ))
875     parser.add_argument("-b", "--binary", action="extend", dest="binaries",
876                         nargs='+',
877                         help="binary to flash, may be used multiple times")
878     parser.add_argument("--create", action="store_true",
879                         help="create the backing file and format the loopback device")
880     parser.add_argument("--debug", action="store_const", const="debug",
881                         default="warning", dest="log_level",
882                         help="set log level to DEBUG")
883     parser.add_argument("-d", "--device",
884                         help="device node or loopback backing file")
885     parser.add_argument("--format", action="store_true",
886                         help="create new partition table on the target device")
887     parser.add_argument("--log-level", dest="log_level", default="warning",
888                         help="Verbosity, possible values: debug, info, warning, "
889                         "error, critical (default: warning)")
890     parser.add_argument("--partition-size", type=str, action="extend", dest="partition_sizes",
891                         nargs='*',
892                         help="override default partition size (in MiB) (used with --format), "
893                         "may be used multiple times, for example: --partition-size hal_a=256")
894     parser.add_argument("--size", type=int, default=8192,
895                         help="size of the backing file to create (in MiB)")
896     parser.add_argument("-t", "--target", required=True,
897                         help="Target device model. Use `--target list`"
898                         " to show supported devices.")
899     parser.add_argument("--update", choices=['a', 'b'], default=None,
900                         help="Choose partition set to update: a or b.")
901     parser.add_argument("--version", action="version",
902                         version=f"%(prog)s {__version__}")
903     parser.add_argument("--YES", action="store_true",
904                         help="agree to destroy data on the DEVICE")
905     args = parser.parse_args()
906
907     if args.target == 'list':
908         print("\nSupported devices:\n")
909         for k,v in TARGETS.items():
910             print(f"  {k:6}  {v.long_name}")
911         sys.exit(0)
912
913     if args.device is None:
914         parser.error('-d/--device argument is required for normal operation')
915
916     if args.partition_sizes is not None:
917         partition_sizes = {}
918         for eqstr in args.partition_sizes:
919             ptstr = eqstr.split('=')
920             if len(ptstr) == 2:
921                 name = ptstr[0]
922                 size = int(ptstr[1])
923                 partition_sizes[name] = size
924             else:
925                 parser.error('--partition-size must follow the name=size pattern')
926         args.partition_sizes = partition_sizes
927
928     conh = ColorStreamHandler(format='%(asctime)s.%(msecs)d %(debuginfo)s%(levelname)-8s %(message)s',
929                               cformat='%(asctime)s.%(msecs)d %(debuginfo)s%(levelcolor)s%(message)s',
930                               datefmt='%Y-%m-%dT%H:%M:%S')
931     log_handlers = [conh]
932     logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
933                         handlers=log_handlers,
934                         level=args.log_level.upper())
935
936     logging.debug(" ".join(sys.argv))
937     check_args(args)
938     check_device(args)
939
940     target = TARGETS[args.target](Device, args)
941
942     check_partition_format(args, target)
943     fuse_image(args, target)
944     subprocess.run(['sync'],
945                    stdin=subprocess.DEVNULL,
946                    stdout=None, stderr=None )