tizen: sd_fusing.py: rpi4s: getting rid of unnecessary alignments
[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": "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', '--no-reread', '--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     logging.debug("Requesting kernel to re-read partition table:\n" + str(target.label))
682     argv = ['blockdev', '--rereadpt', Device]
683     logging.debug(" ".join(argv))
684     proc = subprocess.run(argv,
685                           stdout=None,
686                           stderr=None)
687     if proc.returncode != 0:
688         logging.error(f"Failed to request kernel to reread partition table on {Device}. (Insufficient permissions?)")
689         sys.exit(1)
690
691     if target.bootcode:
692         logging.debug("Writing bootcode\n")
693         with open(Device, "wb") as f:
694             f.write(target.bootcode)
695             f.close
696
697     for i, part in enumerate(target.part_table):
698         d = "/dev/" + get_partition_device(target.device, i+1)
699         if not 'fstype' in part:
700             logging.debug(f"Filesystem not defined for {d}, skipping")
701             continue
702         logging.debug(f"Formatting {d} as {part['fstype']}")
703         if part['fstype'] == 'vfat':
704             argv = ['mkfs.vfat', '-F', '16', '-n', part['name'], d]
705             logging.debug(" ".join(argv))
706             proc = subprocess.run(argv,
707                                   stdin=subprocess.DEVNULL,
708                                   stdout=None, stderr=None)
709             if proc.returncode != 0:
710                 log.error(f"Failed to create FAT filesystem on {d}")
711                 sys.exit(1)
712         elif part['fstype'] == 'ext4':
713             argv = ['mkfs.ext4', '-q', '-L', part['name'], d]
714             logging.debug(" ".join(argv))
715             proc = subprocess.run(argv,
716                                   stdin=subprocess.DEVNULL,
717                                   stdout=None, stderr=None)
718             if proc.returncode != 0:
719                 log.error(f"Failed to create ext4 filesystem on {d}")
720                 sys.exit(1)
721         elif part['fstype'] == 'swap':
722             argv = ['mkswap', '-L', part['name'], d]
723             logging.debug(" ".join(argv))
724             proc = subprocess.run(argv,
725                                   stdin=subprocess.DEVNULL,
726                                   stdout=None, stderr=None)
727             if proc.returncode != 0:
728                 log.error(f"Failed to format swap partition {d}")
729                 sys.exit(1)
730         elif part['fstype'] == 'raw':
731             pass
732     target.initialize_parameters()
733
734 def check_args(args):
735     global Format
736     global Yes
737
738     logging.info(f"Device: {args.device}")
739
740     if args.binaries and len(args.binaries) > 0:
741         logging.info("Fusing binar{}: {}".format("y" if len(args.binaries) == 1 else "ies",
742                      ", ".join(args.binaries)))
743
744     if args.YES:
745         Yes = True
746
747     if args.create:
748         Format = True
749         Yes = True
750
751     if args.format:
752         if Yes:
753             Format = True
754         else:
755             response = input(f"{args.device} will be formatted. Continue? [y/N] ")
756             if response.lower() in ('y', 'yes'):
757                 Format = True
758             else:
759                 Format = False
760
761 def check_device(args):
762     global Format
763     global Device
764     Device = args.device
765
766     if args.create:
767         if os.path.exists(Device):
768             logging.error(f"Failed to create '{Device}', the file alread exists")
769             sys.exit(1)
770         else:
771             argv = ["dd", "if=/dev/zero", f"of={Device}",
772                     "conv=sparse", "bs=1M", f"count={args.size}"]
773             logging.debug(" ".join(argv))
774             rc = subprocess.run(argv)
775             if rc.returncode != 0:
776                 logging.error("Failed to create the backing file")
777                 sys.exit(1)
778
779     if os.path.isfile(Device):
780         global File
781         File = Device
782
783         argv = ["losetup", "--show", "--partscan", "--find", f"{File}"]
784         logging.debug(" ".join(argv))
785         proc = subprocess.run(argv,
786                               stdout=subprocess.PIPE)
787         Device = proc.stdout.decode('utf-8').strip()
788         if proc.returncode != 0:
789             logging.error(f"Failed to attach {File} to a loopback device")
790             sys.exit(1)
791         logging.debug(f"Loop device found: {Device}")
792         atexit.register(lambda: subprocess.run(["losetup", "-d", Device]))
793
794     try:
795         s = os.stat(Device)
796         if not stat.S_ISBLK(s.st_mode):
797             raise TypeError
798     except FileNotFoundError:
799         logging.error(f"No such device: {Device}")
800         sys.exit(1)
801     except TypeError:
802         logging.error(f"{Device} is not a block device")
803         sys.exit(1)
804
805 def check_partition_format(args, target):
806     global Format
807     global Device
808
809     if not Format:
810         logging.info(f"Skip formatting of {Device}".format(Device))
811         return
812     logging.info(f"Start formatting of {Device}")
813     mkpart(args, target)
814     logging.info(f"{Device} formatted")
815
816 def check_ddversion():
817     proc = subprocess.run(["dd", "--version"],
818                             stdout=subprocess.PIPE)
819     version = proc.stdout.decode('utf-8').split('\n')[0].strip()
820     logging.debug(f"Found {version}")
821     major, minor = (int(x) for x in re.findall('[0-9]+', version))
822
823     if major < 8 or major == 8 and minor < 24:
824         return False
825
826     return True
827
828 def get_partition_device(device, idx):
829     argv = ['lsblk', device, '-o', 'TYPE,KNAME']
830     logging.debug(" ".join(argv))
831     proc = subprocess.run(argv,
832                           stdout=subprocess.PIPE)
833     if proc.returncode != 0:
834         logging.error("lsblk has failed")
835         return None
836     part_re = re.compile(f"^part\s+(.*[^0-9]{idx})$")
837     for l in proc.stdout.decode('utf-8').splitlines():
838         match = part_re.match(l)
839         if match:
840             return match[1]
841     return None
842
843 def get_device_kname(device):
844     argv = ['lsblk', device, '-o', 'TYPE,KNAME']
845     logging.debug(" ".join(argv))
846     proc = subprocess.run(argv,
847                           stdout=subprocess.PIPE)
848     for l in proc.stdout.decode('utf-8').splitlines():
849         match = re.search(f"^(disk|loop)\s+(.*)", l)
850         if match:
851             return match[2]
852     return None
853
854 def do_fuse_file(f, name, target):
855     idx = target.get_partition_index(name)
856     if idx is None:
857         logging.info(f"No partition defined for {name}, skipping.")
858         return
859     pdevice = "/dev/" + get_partition_device(Device, idx)
860     argv = ['dd', 'bs=4M',
861             'oflag=direct',
862             'iflag=fullblock',
863             'conv=nocreat',
864             'status=progress',
865             f"of={pdevice}"]
866     logging.debug(" ".join(argv))
867     proc_dd = subprocess.Popen(argv,
868                                bufsize=(4 << 20),
869                                stdin=subprocess.PIPE,
870                                stdout=None, stderr=None)
871     logging.notice(f"Writing {name} to {pdevice}")
872     buf = f.read(4 << 20)
873     while len(buf) > 0:
874         proc_dd.stdin.write(buf)
875         buf = f.read(4 << 20)
876     proc_dd.communicate()
877     logging.info("Done")
878     #TODO: verification
879
880 #TODO: functions with the target argument should probably
881 #      be part of some class
882
883 def do_fuse_image_super(tmpd, target):
884     metadata_slots = 2
885     metadata_size = 65536
886
887     hal_path = os.path.join(tmpd, 'hal.img')
888     rootfs_path = os.path.join(tmpd, 'rootfs.img')
889     super_path = os.path.join(tmpd, 'super.img')
890
891     try:
892         hal_size = os.stat(hal_path).st_size
893         rootfs_size = os.stat(rootfs_path).st_size
894     except FileNotFoundError as e:
895         fn = os.path.split(e.filename)[-1]
896         logging.warning(f"{fn} is missing, skipping super partition image")
897         return
898
899     group_size = 2 * hal_size + rootfs_size
900     super_size = metadata_size + 2 * group_size
901
902     argv = ["lpmake", "-F",
903             f"-o={super_path}",
904             f"--device-size={super_size}",
905             f"--metadata-size={metadata_size}",
906             f"--metadata-slots={metadata_slots}",
907             "-g", f"tizen_a:{group_size}",
908             "-p", f"rootfs_a:none:{rootfs_size}:tizen_a",
909             "-p", f"hal_a:none:{hal_size}:tizen_a",
910             "-g", f"tizen_b:{group_size}",
911             "-p", f"rootfs_b:none:{rootfs_size}:tizen_b",
912             "-p", f"hal_b:none:{hal_size}:tizen_b",
913             "-i", f"rootfs_a={rootfs_path}",
914             "-i", f"rootfs_b={rootfs_path}",
915             "-i", f"hal_a={hal_path}",
916             "-i", f"hal_b={hal_path}"]
917     logging.debug(" ".join(argv))
918     proc = subprocess.run(argv,
919                           stdin=subprocess.DEVNULL,
920                           stdout=None, stderr=None)
921
922     if proc.returncode != 0:
923         logging.error("Failed to create super.img")
924     do_fuse_image(super_path, target)
925
926 def do_fuse_image_tarball(tarball, tmpd, target):
927     with tarfile.open(tarball) as tf:
928         for entry in tf:
929             if target.with_super:
930                 if entry.name in('hal.img', 'rootfs.img'):
931                     tf.extract(entry, path=tmpd)
932                     continue
933             f = tf.extractfile(entry)
934             do_fuse_file(f, entry.name, target)
935
936 def do_fuse_image(img, target):
937     with open(img, 'rb') as f:
938         do_fuse_file(f, os.path.basename(img), target)
939
940 def fuse_image(args, target):
941     global Yes
942
943     if args.binaries is None or len(args.binaries) == 0:
944         return
945
946     if not Yes and not Format:
947         print(f"The following images will be written to {args.device} and the "
948               "existing data will be lost.\n")
949         for b in args.binaries:
950             print("  " + b)
951         response = input("\nContinue? [y/N] ")
952         if not response.lower() in ('y', 'yes'):
953             return
954
955     with tempfile.TemporaryDirectory() as tmpd:
956         for b in args.binaries:
957             if re.search('\.(tar|tar\.gz|tgz)$', b):
958                 do_fuse_image_tarball(b, tmpd, target)
959             else:
960                 fn = os.path.split(b)[-1]
961                 if target.with_super and fn in ('rootfs.img', 'hal.img'):
962                     shutil.copy(b, os.path.join(tmpd, fn))
963                 else:
964                     do_fuse_image(b, target)
965
966         if target.with_super:
967             do_fuse_image_super(tmpd, target)
968
969 def logger_notice(self, msg, *args, **kws):
970     if self.isEnabledFor(LOGGING_NOTICE):
971         self._log(LOGGING_NOTICE, msg, args, **kws)
972 logging.Logger.notice = logger_notice
973
974 def logging_notice(msg, *args, **kws):
975     if len(logging.root.handlers) == 0:
976         basicConfig()
977     logging.root.notice(msg, *args, **kws)
978 logging.notice = logging_notice
979
980
981 if __name__ == '__main__':
982     parser = argparse.ArgumentParser(description="For {}, version {}".format(
983         ", ".join([v.long_name for k,v in TARGETS.items()]),
984         __version__
985     ))
986     parser.add_argument("-b", "--binary", action="extend", dest="binaries",
987                         nargs='+',
988                         help="binary to flash, may be used multiple times")
989     parser.add_argument("--create", action="store_true",
990                         help="create the backing file and format the loopback device")
991     parser.add_argument("--debug", action="store_const", const="debug",
992                         default="notice", dest="log_level",
993                         help="set log level to DEBUG")
994     parser.add_argument("-d", "--device",
995                         help="device node or loopback backing file")
996     parser.add_argument("--format", action="store_true",
997                         help="create new partition table on the target device")
998     parser.add_argument("--log-level", dest="log_level", default="notice",
999                         help="Verbosity, possible values: debug, info, notice, warning, "
1000                         "error, critical (default: notice)")
1001     parser.add_argument("--partition-size", type=str, action="extend", dest="partition_sizes",
1002                         nargs='*',
1003                         help="override default partition size (in MiB) (used with --format), "
1004                         "may be used multiple times, for example: --partition-size hal_a=256")
1005     parser.add_argument("--size", type=int, default=8192,
1006                         help="size of the backing file to create (in MiB)")
1007     parser.add_argument("-t", "--target", required=True,
1008                         help="Target device model. Use `--target list`"
1009                         " to show supported devices.")
1010     parser.add_argument("--update", choices=['a', 'b'], default=None,
1011                         help="Choose partition set to update: a or b.")
1012     parser.add_argument("--version", action="version",
1013                         version=f"%(prog)s {__version__}")
1014     parser.add_argument("--YES", action="store_true",
1015                         help="agree to destroy data on the DEVICE")
1016     args = parser.parse_args()
1017
1018     if args.target == 'list':
1019         print("\nSupported devices:\n")
1020         for k,v in TARGETS.items():
1021             print(f"  {k:6}  {v.long_name}")
1022         sys.exit(0)
1023
1024     if args.device is None:
1025         parser.error('-d/--device argument is required for normal operation')
1026
1027     if args.partition_sizes is not None:
1028         partition_sizes = {}
1029         for eqstr in args.partition_sizes:
1030             ptstr = eqstr.split('=')
1031             if len(ptstr) == 2:
1032                 name = ptstr[0]
1033                 size = int(ptstr[1])
1034                 partition_sizes[name] = size
1035             else:
1036                 parser.error('--partition-size must follow the name=size pattern')
1037         args.partition_sizes = partition_sizes
1038
1039     logging.addLevelName(LOGGING_NOTICE, "NOTICE")
1040     conh = ColorStreamHandler(format='%(asctime)s.%(msecs)d %(debuginfo)s%(levelname)-8s %(message)s',
1041                               cformat='%(asctime)s.%(msecs)d %(debuginfo)s%(levelcolor)s%(message)s',
1042                               datefmt='%Y-%m-%dT%H:%M:%S')
1043     log_handlers = [conh]
1044     logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
1045                         handlers=log_handlers,
1046                         level=args.log_level.upper())
1047
1048     logging.debug(" ".join(sys.argv))
1049     check_args(args)
1050     check_device(args)
1051
1052     target = TARGETS[args.target](Device, args)
1053
1054     check_partition_format(args, target)
1055     fuse_image(args, target)
1056     subprocess.run(['sync'],
1057                    stdin=subprocess.DEVNULL,
1058                    stdout=None, stderr=None )