05008040294068637a5a650eb87127aa34bf0551
[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.1"
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": 6657, "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": "reserved0"},
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": "reserved0"},
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": "reserved0"},
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 LicheePi4ASuper(InitParams, SdFusingTargetAB):
557     long_name = "LicheePi4A w/ super partition"
558     part_table = [
559         {"size": None, "fstype": "raw",  "name": "spl+uboot",
560          "start_sector": 34, "size_sectors": 4062,
561          "ptype": "8DA63339-0007-60C0-C436-083AC8230908",
562          "binaries": ["u-boot-with-spl.bin"],},
563         {"size": 128,  "fstype": "vfat", "name": "boot_a",
564          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
565          "binaries": "boot.img"},
566         {"size": 6656, "fstype": "ext4", "name": "super",
567          "binaries": "super.img"},
568         {"size": 1344, "fstype": "ext4", "name": "system-data",
569          "binaries": "system-data.img"},
570         {"size": None, "fstype": "ext4", "name": "user",
571          "binaries": "user.img"},
572         {"size": 32,   "fstype": "ext4", "name": "module_a",
573          "binaries": "modules.img"},
574         {"size": 32,   "fstype": "ext4", "name": "ramdisk_a",
575          "binaries": "ramdisk.img"},
576         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_a",
577          "binaries": "ramdisk-recovery.img"},
578         {"size": 8,    "fstype": "ext4", "name": "inform"},
579         {"size": 128,   "fstype": "vfat", "name": "boot_b",
580          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
581          "binaries_b": "boot.img"},
582         {"size": 32,   "fstype": "ext4", "name": "module_b",
583          "binaries_b": "modules.img"},
584         {"size": 32,   "fstype": "ext4", "name": "ramdisk_b",
585          "binaries_b": "ramdisk.img"},
586         {"size": 32,   "fstype": "ext4", "name": "ramdisk-recovery_b",
587          "binaries_b": "ramdisk-recovery.img"},
588         {"size": 4,    "fstype": "ext4", "name": "reserved0"},
589         {"size": 64,   "fstype": "ext4", "name": "reserved1"},
590         {"size": 125,  "fstype": "ext4", "name": "reserved2"}
591     ]
592     params = (('reboot-param.bin', 'norm'),
593               ('reboot-param.info', 'norm'),
594               ('partition-ab.info', 'a'),
595               ('partition-ab-cloned.info', '1'),
596               ('upgrade-status.info', '0'),
597               ('partition-a-status.info', 'ok'),
598               ('partition-b-status.info', 'ok'))
599
600     # bootcode written to the protective MBR, aka RV64 'J 0x4400' (sector 34)
601     bootcode = b'\x6f\x40\x00\x40'
602
603     def __init__(self, device, args):
604         self.reserved_space = 8
605         self.user_partition = 4
606         self.update = args.update
607         super().__init__(device, 'gpt')
608         self.with_super = True
609         self.super_alignment = 1048576
610
611
612 class X86emu(SdFusingTarget):
613     part_table = [
614         {"size": 512,    "fstype": "vfat",  "name": "EFI", "start": 4,
615          "ptype": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
616          "binaries": ""},
617         {"size": 512,    "fstype": "ext2",  "name": "boot",
618          "binaries": "emulator-boot.img",},
619         {"size": 2048,  "fstype": "ext4", "name": "rootfs",
620          "binaries": "emulator-rootfs.img"},
621         {"size": 1344, "fstype": "ext4", "name": "system-data",
622          "binaries": "emulator-sysdata.img"},
623         {"size": 1024, "fstype": "swap", "name": "emulator-swap",
624          "ptype": "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F"},
625     ]
626
627     def __init__(self, device, args):
628         super().__init__(device, 'gpt')
629         for p in self.label.part_table:
630             if p.name == "rootfs":
631                 p.ptype = args._rootfs_uuid
632                 break
633
634 class X86emu32(X86emu):
635     long_name = "QEMU x86 32-bit"
636
637     def __init__(self, device, args):
638         setattr(args, "_rootfs_uuid", "44479540-F297-41B2-9AF7-D131D5F0458A")
639         super().__init__(device, args)
640
641 class X86emu64(X86emu):
642     long_name = "QEMU x86 64-bit"
643
644     def __init__(self, device, args):
645         setattr(args, "_rootfs_uuid", "44479540-F297-41B2-9AF7-D131D5F0458A")
646         super().__init__(device, args)
647
648 TARGETS = {
649     'rpi3': Rpi3,
650     'rpi4': Rpi4,
651     'rpi4s': Rpi4Super,
652     'rpi4aot': Rpi4AoT,
653     'vf2': VF2,
654     'rv64': RV64,
655     'lpi4a': LicheePi4A,
656     'lpi4as': LicheePi4ASuper,
657     'x86emu32': X86emu32,
658     'x86emu64': X86emu64,
659 }
660
661 def device_size(device):
662     argv = ["sfdisk", "-s", device]
663     logging.debug(" ".join(argv))
664     proc = subprocess.run(argv,
665                           stdout=subprocess.PIPE)
666     size = int(proc.stdout.decode('utf-8').strip()) >> 10
667     logging.debug(f"{device} size {size}MiB")
668     return size
669
670 def check_sfdisk():
671     proc = subprocess.run(['sfdisk', '-v'],
672                           stdout=subprocess.PIPE)
673     version = proc.stdout.decode('utf-8').strip()
674     logging.debug(f"Found {version}")
675     major, minor = [int(x) for x in re.findall('[0-9]+', version)][0:2]
676     support_delete = False
677
678     if major < 2 or major == 2 and minor < 26:
679         log.error(f"Your sfdisk {major}.{minor}.{patch} is too old.")
680         return False,False
681     elif major == 2 and minor >= 28:
682         support_delete = True
683
684     return True, support_delete
685
686 def mkpart(args, target):
687     global Device
688     new, support_delete = check_sfdisk()
689
690     if not new:
691         logging.error('sfdisk too old')
692         sys.exit(1)
693
694     with open('/proc/self/mounts') as mounts:
695         device_kname = '/dev/' + get_device_kname(Device)
696         device_re = re.compile(device_kname + '[^ ]*')
697         logging.debug(f"Checking for mounted partitions on {device_kname}")
698         for m in mounts:
699             match = device_re.match(m)
700             if match:
701                 logging.warning('Found mounted device: ' + match[0])
702                 argv = ['umount', match[0]]
703                 logging.debug(" ".join(argv))
704                 proc = subprocess.run(argv)
705                 if proc.returncode != 0:
706                     logging.error(f"Failed to unmount {match[0]}")
707                     sys.exit(1)
708
709     if support_delete:
710         logging.info("Removing old partitions")
711         argv = ['sfdisk', '--delete', Device]
712         logging.debug(" ".join(argv))
713         proc = subprocess.run(argv)
714         if proc.returncode != 0:
715             logging.error(f"Failed to remove the old partitions from {Device}")
716     else:
717         logging.info("Removing old partition table")
718         argv = ['dd', 'if=/dev/zero', 'of=' + Device,
719                 'bs=512', 'count=32', 'conv=notrunc']
720         logging.debug(" ".join(argv))
721         proc = subprocess.run(argv)
722         if proc.returncode != 0:
723             logging.error(f"Failed to clear the old partition table on {Device}")
724             sys.exit(1)
725
726     logging.debug("New partition table:\n" + str(target.label))
727     argv = ['sfdisk', '--no-reread', '--wipe-partitions', 'always', Device]
728     logging.debug(" ".join(argv))
729     proc = subprocess.run(argv,
730                           stdout=None,
731                           stderr=None,
732                           input=str(target.label).encode())
733     if proc.returncode != 0:
734         logging.error(f"Failed to create partition a new table on {Device}")
735         logging.error(f"New partition table:\n" + str(target.label))
736         sys.exit(1)
737
738     logging.debug("Requesting kernel to re-read partition table:\n" + str(target.label))
739     argv = ['blockdev', '--rereadpt', Device]
740     logging.debug(" ".join(argv))
741     proc = subprocess.run(argv,
742                           stdout=None,
743                           stderr=None)
744     if proc.returncode != 0:
745         logging.error(f"Failed to request kernel to reread partition table on {Device}. (Insufficient permissions?)")
746         sys.exit(1)
747
748     if target.bootcode:
749         logging.debug("Writing bootcode\n")
750         with open(Device, "wb") as f:
751             f.write(target.bootcode)
752             f.close
753
754     for i, part in enumerate(target.part_table):
755         d = "/dev/" + get_partition_device(target.device, i+1)
756         if not 'fstype' in part:
757             logging.debug(f"Filesystem not defined for {d}, skipping")
758             continue
759         logging.debug(f"Formatting {d} as {part['fstype']}")
760         if part['fstype'] == 'vfat':
761             argv = ['mkfs.vfat', '-F', '16', '-n', part['name'], d]
762             logging.debug(" ".join(argv))
763             proc = subprocess.run(argv,
764                                   stdin=subprocess.DEVNULL,
765                                   stdout=None, stderr=None)
766             if proc.returncode != 0:
767                 log.error(f"Failed to create FAT filesystem on {d}")
768                 sys.exit(1)
769         elif part['fstype'] == 'ext4':
770             argv = ['mkfs.ext4', '-q', '-L', part['name'], d]
771             logging.debug(" ".join(argv))
772             proc = subprocess.run(argv,
773                                   stdin=subprocess.DEVNULL,
774                                   stdout=None, stderr=None)
775             if proc.returncode != 0:
776                 log.error(f"Failed to create ext4 filesystem on {d}")
777                 sys.exit(1)
778         elif part['fstype'] == 'swap':
779             argv = ['mkswap', '-L', part['name'], d]
780             logging.debug(" ".join(argv))
781             proc = subprocess.run(argv,
782                                   stdin=subprocess.DEVNULL,
783                                   stdout=None, stderr=None)
784             if proc.returncode != 0:
785                 log.error(f"Failed to format swap partition {d}")
786                 sys.exit(1)
787         elif part['fstype'] == 'raw':
788             pass
789     target.initialize_parameters()
790
791 def check_args(args):
792     global Format
793     global Yes
794
795     logging.info(f"Device: {args.device}")
796
797     if args.binaries and len(args.binaries) > 0:
798         logging.info("Fusing binar{}: {}".format("y" if len(args.binaries) == 1 else "ies",
799                      ", ".join(args.binaries)))
800
801     if args.YES:
802         Yes = True
803
804     if args.create:
805         Format = True
806         Yes = True
807
808     if args.format:
809         if Yes:
810             Format = True
811         else:
812             response = input(f"{args.device} will be formatted. Continue? [y/N] ")
813             if response.lower() in ('y', 'yes'):
814                 Format = True
815             else:
816                 Format = False
817
818 def check_device(args):
819     global Format
820     global Device
821     Device = args.device
822
823     if args.create:
824         if os.path.exists(Device):
825             logging.error(f"Failed to create '{Device}', the file alread exists")
826             sys.exit(1)
827         else:
828             argv = ["dd", "if=/dev/zero", f"of={Device}",
829                     "conv=sparse", "bs=1M", f"count={args.size}"]
830             logging.debug(" ".join(argv))
831             rc = subprocess.run(argv)
832             if rc.returncode != 0:
833                 logging.error("Failed to create the backing file")
834                 sys.exit(1)
835
836     if os.path.isfile(Device):
837         global File
838         File = Device
839
840         argv = ["losetup", "--show", "--partscan", "--find", f"{File}"]
841         logging.debug(" ".join(argv))
842         proc = subprocess.run(argv,
843                               stdout=subprocess.PIPE)
844         Device = proc.stdout.decode('utf-8').strip()
845         if proc.returncode != 0:
846             logging.error(f"Failed to attach {File} to a loopback device")
847             sys.exit(1)
848         logging.debug(f"Loop device found: {Device}")
849         atexit.register(lambda: subprocess.run(["losetup", "-d", Device]))
850
851     try:
852         s = os.stat(Device)
853         if not stat.S_ISBLK(s.st_mode):
854             raise TypeError
855     except FileNotFoundError:
856         logging.error(f"No such device: {Device}")
857         sys.exit(1)
858     except TypeError:
859         logging.error(f"{Device} is not a block device")
860         sys.exit(1)
861
862 def check_partition_format(args, target):
863     global Format
864     global Device
865
866     if not Format:
867         logging.info(f"Skip formatting of {Device}".format(Device))
868         return
869     logging.info(f"Start formatting of {Device}")
870     mkpart(args, target)
871     logging.info(f"{Device} formatted")
872
873 def check_ddversion():
874     proc = subprocess.run(["dd", "--version"],
875                             stdout=subprocess.PIPE)
876     version = proc.stdout.decode('utf-8').split('\n')[0].strip()
877     logging.debug(f"Found {version}")
878     major, minor = (int(x) for x in re.findall('[0-9]+', version))
879
880     if major < 8 or major == 8 and minor < 24:
881         return False
882
883     return True
884
885 def get_partition_device(device, idx):
886     argv = ['lsblk', device, '-o', 'TYPE,KNAME']
887     logging.debug(" ".join(argv))
888     proc = subprocess.run(argv,
889                           stdout=subprocess.PIPE)
890     if proc.returncode != 0:
891         logging.error("lsblk has failed")
892         return None
893     part_re = re.compile(f"^part\s+(.*[^0-9]{idx})$")
894     for l in proc.stdout.decode('utf-8').splitlines():
895         match = part_re.match(l)
896         if match:
897             return match[1]
898     return None
899
900 def get_device_kname(device):
901     argv = ['lsblk', device, '-o', 'TYPE,KNAME']
902     logging.debug(" ".join(argv))
903     proc = subprocess.run(argv,
904                           stdout=subprocess.PIPE)
905     for l in proc.stdout.decode('utf-8').splitlines():
906         match = re.search(f"^(disk|loop)\s+(.*)", l)
907         if match:
908             return match[2]
909     return None
910
911 def do_fuse_file(f, name, target):
912     idx = target.get_partition_index(name)
913     if idx is None:
914         logging.info(f"No partition defined for {name}, skipping.")
915         return
916     pdevice = "/dev/" + get_partition_device(Device, idx)
917     argv = ['dd', 'bs=4M',
918             'oflag=direct',
919             'iflag=fullblock',
920             'conv=nocreat',
921             'status=progress',
922             f"of={pdevice}"]
923     logging.debug(" ".join(argv))
924     proc_dd = subprocess.Popen(argv,
925                                bufsize=(4 << 20),
926                                stdin=subprocess.PIPE,
927                                stdout=None, stderr=None)
928     logging.notice(f"Writing {name} to {pdevice}")
929     buf = f.read(4 << 20)
930     while len(buf) > 0:
931         proc_dd.stdin.write(buf)
932         buf = f.read(4 << 20)
933     proc_dd.communicate()
934     logging.info("Done")
935     #TODO: verification
936
937 #TODO: functions with the target argument should probably
938 #      be part of some class
939
940 def do_fuse_image_super(tmpd, target):
941     metadata_slots = 2
942     metadata_size = 65536
943
944     hal_path = os.path.join(tmpd, 'hal.img')
945     rootfs_path = os.path.join(tmpd, 'rootfs.img')
946     super_path = os.path.join(tmpd, 'super.img')
947
948     try:
949         hal_size = os.stat(hal_path).st_size
950         rootfs_size = os.stat(rootfs_path).st_size
951     except FileNotFoundError as e:
952         fn = os.path.split(e.filename)[-1]
953         logging.warning(f"{fn} is missing, skipping super partition image")
954         return
955
956     group_size = 2 * hal_size + rootfs_size
957     super_size = metadata_size + 2 * group_size
958
959     argv = ["lpmake", "-F",
960             f"-o={super_path}",
961             f"--device-size={super_size}",
962             f"--metadata-size={metadata_size}",
963             f"--metadata-slots={metadata_slots}",
964             "-g", f"tizen_a:{group_size}",
965             "-p", f"rootfs_a:none:{rootfs_size}:tizen_a",
966             "-p", f"hal_a:none:{hal_size}:tizen_a",
967             "-g", f"tizen_b:{group_size}",
968             "-p", f"rootfs_b:none:{rootfs_size}:tizen_b",
969             "-p", f"hal_b:none:{hal_size}:tizen_b",
970             "-i", f"rootfs_a={rootfs_path}",
971             "-i", f"rootfs_b={rootfs_path}",
972             "-i", f"hal_a={hal_path}",
973             "-i", f"hal_b={hal_path}"]
974     logging.debug(" ".join(argv))
975     proc = subprocess.run(argv,
976                           stdin=subprocess.DEVNULL,
977                           stdout=None, stderr=None)
978
979     if proc.returncode != 0:
980         logging.error("Failed to create super.img")
981     do_fuse_image(super_path, target)
982
983 def do_fuse_image_tarball(tarball, tmpd, target):
984     with tarfile.open(tarball) as tf:
985         for entry in tf:
986             if target.with_super:
987                 if entry.name in('hal.img', 'rootfs.img'):
988                     tf.extract(entry, path=tmpd)
989                     continue
990             f = tf.extractfile(entry)
991             do_fuse_file(f, entry.name, target)
992
993 def do_fuse_image(img, target):
994     with open(img, 'rb') as f:
995         do_fuse_file(f, os.path.basename(img), target)
996
997 def fuse_image(args, target):
998     global Yes
999
1000     if args.binaries is None or len(args.binaries) == 0:
1001         return
1002
1003     if not Yes and not Format:
1004         print(f"The following images will be written to {args.device} and the "
1005               "existing data will be lost.\n")
1006         for b in args.binaries:
1007             print("  " + b)
1008         response = input("\nContinue? [y/N] ")
1009         if not response.lower() in ('y', 'yes'):
1010             return
1011
1012     with tempfile.TemporaryDirectory() as tmpd:
1013         for b in args.binaries:
1014             if re.search('\.(tar|tar\.gz|tgz)$', b):
1015                 do_fuse_image_tarball(b, tmpd, target)
1016             else:
1017                 fn = os.path.split(b)[-1]
1018                 if target.with_super and fn in ('rootfs.img', 'hal.img'):
1019                     shutil.copy(b, os.path.join(tmpd, fn))
1020                 else:
1021                     do_fuse_image(b, target)
1022
1023         if target.with_super:
1024             do_fuse_image_super(tmpd, target)
1025
1026 def logger_notice(self, msg, *args, **kws):
1027     if self.isEnabledFor(LOGGING_NOTICE):
1028         self._log(LOGGING_NOTICE, msg, args, **kws)
1029 logging.Logger.notice = logger_notice
1030
1031 def logging_notice(msg, *args, **kws):
1032     if len(logging.root.handlers) == 0:
1033         basicConfig()
1034     logging.root.notice(msg, *args, **kws)
1035 logging.notice = logging_notice
1036
1037
1038 if __name__ == '__main__':
1039     parser = argparse.ArgumentParser(description="For {}, version {}".format(
1040         ", ".join([v.long_name for k,v in TARGETS.items()]),
1041         __version__
1042     ))
1043     parser.add_argument("-b", "--binary", action="extend", dest="binaries",
1044                         nargs='+',
1045                         help="binary to flash, may be used multiple times")
1046     parser.add_argument("--create", action="store_true",
1047                         help="create the backing file and format the loopback device")
1048     parser.add_argument("--debug", action="store_const", const="debug",
1049                         default="notice", dest="log_level",
1050                         help="set log level to DEBUG")
1051     parser.add_argument("-d", "--device",
1052                         help="device node or loopback backing file")
1053     parser.add_argument("--format", action="store_true",
1054                         help="create new partition table on the target device")
1055     parser.add_argument("--log-level", dest="log_level", default="notice",
1056                         help="Verbosity, possible values: debug, info, notice, warning, "
1057                         "error, critical (default: notice)")
1058     parser.add_argument("--partition-size", type=str, action="extend", dest="partition_sizes",
1059                         nargs='*',
1060                         help="override default partition size (in MiB) (used with --format), "
1061                         "may be used multiple times, for example: --partition-size hal_a=256")
1062     parser.add_argument("--size", type=int, default=8192,
1063                         help="size of the backing file to create (in MiB)")
1064     parser.add_argument("-t", "--target", required=True,
1065                         help="Target device model. Use `--target list`"
1066                         " to show supported devices.")
1067     parser.add_argument("--update", choices=['a', 'b'], default=None,
1068                         help="Choose partition set to update: a or b.")
1069     parser.add_argument("--version", action="version",
1070                         version=f"%(prog)s {__version__}")
1071     parser.add_argument("--YES", action="store_true",
1072                         help="agree to destroy data on the DEVICE")
1073     args = parser.parse_args()
1074
1075     if args.target == 'list':
1076         print("\nSupported devices:\n")
1077         for k,v in TARGETS.items():
1078             print(f"  {k:6}  {v.long_name}")
1079         sys.exit(0)
1080
1081     if args.device is None:
1082         parser.error('-d/--device argument is required for normal operation')
1083
1084     if args.partition_sizes is not None:
1085         partition_sizes = {}
1086         for eqstr in args.partition_sizes:
1087             ptstr = eqstr.split('=')
1088             if len(ptstr) == 2:
1089                 name = ptstr[0]
1090                 size = int(ptstr[1])
1091                 partition_sizes[name] = size
1092             else:
1093                 parser.error('--partition-size must follow the name=size pattern')
1094         args.partition_sizes = partition_sizes
1095
1096     logging.addLevelName(LOGGING_NOTICE, "NOTICE")
1097     conh = ColorStreamHandler(format='%(asctime)s.%(msecs)d %(debuginfo)s%(levelname)-8s %(message)s',
1098                               cformat='%(asctime)s.%(msecs)d %(debuginfo)s%(levelcolor)s%(message)s',
1099                               datefmt='%Y-%m-%dT%H:%M:%S')
1100     log_handlers = [conh]
1101     logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
1102                         handlers=log_handlers,
1103                         level=args.log_level.upper())
1104
1105     logging.debug(" ".join(sys.argv))
1106     check_args(args)
1107     check_device(args)
1108
1109     target = TARGETS[args.target](Device, args)
1110
1111     check_partition_format(args, target)
1112     fuse_image(args, target)
1113     subprocess.run(['sync'],
1114                    stdin=subprocess.DEVNULL,
1115                    stdout=None, stderr=None )