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