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