summaryrefslogtreecommitdiffstats
path: root/meta/lib/oe/fitimage.py
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/oe/fitimage.py')
-rw-r--r--meta/lib/oe/fitimage.py547
1 files changed, 547 insertions, 0 deletions
diff --git a/meta/lib/oe/fitimage.py b/meta/lib/oe/fitimage.py
new file mode 100644
index 0000000000..f303799155
--- /dev/null
+++ b/meta/lib/oe/fitimage.py
@@ -0,0 +1,547 @@
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# This file contains common functions for the fitimage generation
7
8import os
9import shlex
10import subprocess
11import bb
12
13from oeqa.utils.commands import runCmd
14
15class ItsNode:
16 INDENT_SIZE = 8
17
18 def __init__(self, name, parent_node, sub_nodes=None, properties=None):
19 self.name = name
20 self.parent_node = parent_node
21
22 self.sub_nodes = []
23 if sub_nodes:
24 self.sub_nodes = sub_nodes
25
26 self.properties = {}
27 if properties:
28 self.properties = properties
29
30 if parent_node:
31 parent_node.add_sub_node(self)
32
33 def add_sub_node(self, sub_node):
34 self.sub_nodes.append(sub_node)
35
36 def add_property(self, key, value):
37 self.properties[key] = value
38
39 def emit(self, f, indent):
40 indent_str_name = " " * indent
41 indent_str_props = " " * (indent + self.INDENT_SIZE)
42 f.write("%s%s {\n" % (indent_str_name, self.name))
43 for key, value in self.properties.items():
44 bb.debug(1, "key: %s, value: %s" % (key, str(value)))
45 # Single integer: <0x12ab>
46 if isinstance(value, int):
47 f.write(indent_str_props + key + ' = <0x%x>;\n' % value)
48 # list of strings: "string1", "string2" or integers: <0x12ab 0x34cd>
49 elif isinstance(value, list):
50 if len(value) == 0:
51 f.write(indent_str_props + key + ' = "";\n')
52 elif isinstance(value[0], int):
53 list_entries = ' '.join('0x%x' % entry for entry in value)
54 f.write(indent_str_props + key + ' = <%s>;\n' % list_entries)
55 else:
56 list_entries = ', '.join('"%s"' % entry for entry in value)
57 f.write(indent_str_props + key + ' = %s;\n' % list_entries)
58 elif isinstance(value, str):
59 # path: /incbin/("path/to/file")
60 if key in ["data"] and value.startswith('/incbin/('):
61 f.write(indent_str_props + key + ' = %s;\n' % value)
62 # Integers which are already string formatted
63 elif value.startswith("<") and value.endswith(">"):
64 f.write(indent_str_props + key + ' = %s;\n' % value)
65 else:
66 f.write(indent_str_props + key + ' = "%s";\n' % value)
67 else:
68 bb.fatal("%s has unexpexted data type." % str(value))
69 for sub_node in self.sub_nodes:
70 sub_node.emit(f, indent + self.INDENT_SIZE)
71 f.write(indent_str_name + '};\n')
72
73class ItsNodeImages(ItsNode):
74 def __init__(self, parent_node):
75 super().__init__("images", parent_node)
76
77class ItsNodeConfigurations(ItsNode):
78 def __init__(self, parent_node):
79 super().__init__("configurations", parent_node)
80
81class ItsNodeHash(ItsNode):
82 def __init__(self, name, parent_node, algo, opt_props=None):
83 properties = {
84 "algo": algo
85 }
86 if opt_props:
87 properties.update(opt_props)
88 super().__init__(name, parent_node, None, properties)
89
90class ItsImageSignature(ItsNode):
91 def __init__(self, name, parent_node, algo, keyname, opt_props=None):
92 properties = {
93 "algo": algo,
94 "key-name-hint": keyname
95 }
96 if opt_props:
97 properties.update(opt_props)
98 super().__init__(name, parent_node, None, properties)
99
100class ItsNodeImage(ItsNode):
101 def __init__(self, name, parent_node, description, type, compression, sub_nodes=None, opt_props=None):
102 properties = {
103 "description": description,
104 "type": type,
105 "compression": compression,
106 }
107 if opt_props:
108 properties.update(opt_props)
109 super().__init__(name, parent_node, sub_nodes, properties)
110
111class ItsNodeDtb(ItsNodeImage):
112 def __init__(self, name, parent_node, description, type, compression,
113 sub_nodes=None, opt_props=None, compatible=None):
114 super().__init__(name, parent_node, description, type, compression, sub_nodes, opt_props)
115 self.compatible = compatible
116
117class ItsNodeDtbAlias(ItsNode):
118 """Additional Configuration Node for a DTB
119
120 Symlinks pointing to a DTB file are handled by an addtitional
121 configuration node referring to another DTB image node.
122 """
123 def __init__(self, name, alias_name, compatible=None):
124 super().__init__(name, parent_node=None, sub_nodes=None, properties=None)
125 self.alias_name = alias_name
126 self.compatible = compatible
127
128class ItsNodeConfigurationSignature(ItsNode):
129 def __init__(self, name, parent_node, algo, keyname, opt_props=None):
130 properties = {
131 "algo": algo,
132 "key-name-hint": keyname
133 }
134 if opt_props:
135 properties.update(opt_props)
136 super().__init__(name, parent_node, None, properties)
137
138class ItsNodeConfiguration(ItsNode):
139 def __init__(self, name, parent_node, description, sub_nodes=None, opt_props=None):
140 properties = {
141 "description": description,
142 }
143 if opt_props:
144 properties.update(opt_props)
145 super().__init__(name, parent_node, sub_nodes, properties)
146
147class ItsNodeRootKernel(ItsNode):
148 """Create FIT images for the kernel
149
150 Currently only a single kernel (no less or more) can be added to the FIT
151 image along with 0 or more device trees and 0 or 1 ramdisk.
152
153 If a device tree included in the FIT image, the default configuration is the
154 firt DTB. If there is no dtb present than the default configuation the kernel.
155 """
156 def __init__(self, description, address_cells, host_prefix, arch, conf_prefix,
157 sign_enable=False, sign_keydir=None,
158 mkimage=None, mkimage_dtcopts=None,
159 mkimage_sign=None, mkimage_sign_args=None,
160 hash_algo=None, sign_algo=None, pad_algo=None,
161 sign_keyname_conf=None,
162 sign_individual=False, sign_keyname_img=None):
163 props = {
164 "description": description,
165 "#address-cells": f"<{address_cells}>"
166 }
167 super().__init__("/", None, None, props)
168 self.images = ItsNodeImages(self)
169 self.configurations = ItsNodeConfigurations(self)
170
171 self._host_prefix = host_prefix
172 self._arch = arch
173 self._conf_prefix = conf_prefix
174
175 # Signature related properties
176 self._sign_enable = sign_enable
177 self._sign_keydir = sign_keydir
178 self._mkimage = mkimage
179 self._mkimage_dtcopts = mkimage_dtcopts
180 self._mkimage_sign = mkimage_sign
181 self._mkimage_sign_args = mkimage_sign_args
182 self._hash_algo = hash_algo
183 self._sign_algo = sign_algo
184 self._pad_algo = pad_algo
185 self._sign_keyname_conf = sign_keyname_conf
186 self._sign_individual = sign_individual
187 self._sign_keyname_img = sign_keyname_img
188 self._sanitize_sign_config()
189
190 self._dtbs = []
191 self._dtb_alias = []
192 self._kernel = None
193 self._ramdisk = None
194 self._bootscr = None
195 self._setup = None
196
197 def _sanitize_sign_config(self):
198 if self._sign_enable:
199 if not self._hash_algo:
200 bb.fatal("FIT image signing is enabled but no hash algorithm is provided.")
201 if not self._sign_algo:
202 bb.fatal("FIT image signing is enabled but no signature algorithm is provided.")
203 if not self._pad_algo:
204 bb.fatal("FIT image signing is enabled but no padding algorithm is provided.")
205 if not self._sign_keyname_conf:
206 bb.fatal("FIT image signing is enabled but no configuration key name is provided.")
207 if self._sign_individual and not self._sign_keyname_img:
208 bb.fatal("FIT image signing is enabled for individual images but no image key name is provided.")
209
210 def write_its_file(self, itsfile):
211 with open(itsfile, 'w') as f:
212 f.write("/dts-v1/;\n\n")
213 self.emit(f, 0)
214
215 def its_add_node_image(self, image_id, description, image_type, compression, opt_props):
216 image_node = ItsNodeImage(
217 image_id,
218 self.images,
219 description,
220 image_type,
221 compression,
222 opt_props=opt_props
223 )
224 if self._hash_algo:
225 ItsNodeHash(
226 "hash-1",
227 image_node,
228 self._hash_algo
229 )
230 if self._sign_individual:
231 ItsImageSignature(
232 "signature-1",
233 image_node,
234 f"{self._hash_algo},{self._sign_algo}",
235 self._sign_keyname_img
236 )
237 return image_node
238
239 def its_add_node_dtb(self, image_id, description, image_type, compression, opt_props, compatible):
240 dtb_node = ItsNodeDtb(
241 image_id,
242 self.images,
243 description,
244 image_type,
245 compression,
246 opt_props=opt_props,
247 compatible=compatible
248 )
249 if self._hash_algo:
250 ItsNodeHash(
251 "hash-1",
252 dtb_node,
253 self._hash_algo
254 )
255 if self._sign_individual:
256 ItsImageSignature(
257 "signature-1",
258 dtb_node,
259 f"{self._hash_algo},{self._sign_algo}",
260 self._sign_keyname_img
261 )
262 return dtb_node
263
264 def fitimage_emit_section_kernel(self, kernel_id, kernel_path, compression,
265 load, entrypoint, mkimage_kernel_type, entrysymbol=None):
266 """Emit the fitImage ITS kernel section"""
267 if self._kernel:
268 bb.fatal("Kernel section already exists in the ITS file.")
269 if entrysymbol:
270 result = subprocess.run([self._host_prefix + "nm", "vmlinux"], capture_output=True, text=True)
271 for line in result.stdout.splitlines():
272 parts = line.split()
273 if len(parts) == 3 and parts[2] == entrysymbol:
274 entrypoint = "<0x%s>" % parts[0]
275 break
276 kernel_node = self.its_add_node_image(
277 kernel_id,
278 "Linux kernel",
279 mkimage_kernel_type,
280 compression,
281 {
282 "data": '/incbin/("' + kernel_path + '")',
283 "arch": self._arch,
284 "os": "linux",
285 "load": f"<{load}>",
286 "entry": f"<{entrypoint}>"
287 }
288 )
289 self._kernel = kernel_node
290
291 def fitimage_emit_section_dtb(self, dtb_id, dtb_path, dtb_loadaddress=None,
292 dtbo_loadaddress=None, add_compatible=False):
293 """Emit the fitImage ITS DTB section"""
294 load=None
295 dtb_ext = os.path.splitext(dtb_path)[1]
296 if dtb_ext == ".dtbo":
297 if dtbo_loadaddress:
298 load = dtbo_loadaddress
299 elif dtb_loadaddress:
300 load = dtb_loadaddress
301
302 opt_props = {
303 "data": '/incbin/("' + dtb_path + '")',
304 "arch": self._arch
305 }
306 if load:
307 opt_props["load"] = f"<{load}>"
308
309 # Preserve the DTB's compatible string to be added to the configuration node
310 compatible = None
311 if add_compatible:
312 compatible = get_compatible_from_dtb(dtb_path)
313
314 dtb_node = self.its_add_node_dtb(
315 "fdt-" + dtb_id,
316 "Flattened Device Tree blob",
317 "flat_dt",
318 "none",
319 opt_props,
320 compatible
321 )
322 self._dtbs.append(dtb_node)
323
324 def fitimage_emit_section_dtb_alias(self, dtb_alias_id, dtb_path, add_compatible=False):
325 """Add a configuration node referring to another DTB"""
326 # Preserve the DTB's compatible string to be added to the configuration node
327 compatible = None
328 if add_compatible:
329 compatible = get_compatible_from_dtb(dtb_path)
330
331 dtb_id = os.path.basename(dtb_path)
332 dtb_alias_node = ItsNodeDtbAlias("fdt-" + dtb_id, dtb_alias_id, compatible)
333 self._dtb_alias.append(dtb_alias_node)
334 bb.warn(f"compatible: {compatible}, dtb_alias_id: {dtb_alias_id}, dtb_id: {dtb_id}, dtb_path: {dtb_path}")
335
336 def fitimage_emit_section_boot_script(self, bootscr_id, bootscr_path):
337 """Emit the fitImage ITS u-boot script section"""
338 if self._bootscr:
339 bb.fatal("U-boot script section already exists in the ITS file.")
340 bootscr_node = self.its_add_node_image(
341 bootscr_id,
342 "U-boot script",
343 "script",
344 "none",
345 {
346 "data": '/incbin/("' + bootscr_path + '")',
347 "arch": self._arch,
348 "type": "script"
349 }
350 )
351 self._bootscr = bootscr_node
352
353 def fitimage_emit_section_setup(self, setup_id, setup_path):
354 """Emit the fitImage ITS setup section"""
355 if self._setup:
356 bb.fatal("Setup section already exists in the ITS file.")
357 load = "<0x00090000>"
358 entry = "<0x00090000>"
359 setup_node = self.its_add_node_image(
360 setup_id,
361 "Linux setup.bin",
362 "x86_setup",
363 "none",
364 {
365 "data": '/incbin/("' + setup_path + '")',
366 "arch": self._arch,
367 "os": "linux",
368 "load": load,
369 "entry": entry
370 }
371 )
372 self._setup = setup_node
373
374 def fitimage_emit_section_ramdisk(self, ramdisk_id, ramdisk_path, description="ramdisk", load=None, entry=None):
375 """Emit the fitImage ITS ramdisk section"""
376 if self._ramdisk:
377 bb.fatal("Ramdisk section already exists in the ITS file.")
378 opt_props = {
379 "data": '/incbin/("' + ramdisk_path + '")',
380 "type": "ramdisk",
381 "arch": self._arch,
382 "os": "linux"
383 }
384 if load:
385 opt_props["load"] = f"<{load}>"
386 if entry:
387 opt_props["entry"] = f"<{entry}>"
388
389 ramdisk_node = self.its_add_node_image(
390 ramdisk_id,
391 description,
392 "ramdisk",
393 "none",
394 opt_props
395 )
396 self._ramdisk = ramdisk_node
397
398 def _fitimage_emit_one_section_config(self, conf_node_name, dtb=None):
399 """Emit the fitImage ITS configuration section"""
400 opt_props = {}
401 conf_desc = []
402 sign_entries = []
403
404 if self._kernel:
405 conf_desc.append("Linux kernel")
406 opt_props["kernel"] = self._kernel.name
407 if self._sign_enable:
408 sign_entries.append("kernel")
409
410 if dtb:
411 conf_desc.append("FDT blob")
412 opt_props["fdt"] = dtb.name
413 if dtb.compatible:
414 opt_props["compatible"] = dtb.compatible
415 if self._sign_enable:
416 sign_entries.append("fdt")
417
418 if self._ramdisk:
419 conf_desc.append("ramdisk")
420 opt_props["ramdisk"] = self._ramdisk.name
421 if self._sign_enable:
422 sign_entries.append("ramdisk")
423
424 if self._bootscr:
425 conf_desc.append("u-boot script")
426 opt_props["bootscr"] = self._bootscr.name
427 if self._sign_enable:
428 sign_entries.append("bootscr")
429
430 if self._setup:
431 conf_desc.append("setup")
432 opt_props["setup"] = self._setup.name
433 if self._sign_enable:
434 sign_entries.append("setup")
435
436 # First added configuration is the default configuration
437 default_flag = "0"
438 if len(self.configurations.sub_nodes) == 0:
439 default_flag = "1"
440
441 conf_node = ItsNodeConfiguration(
442 conf_node_name,
443 self.configurations,
444 f"{default_flag} {', '.join(conf_desc)}",
445 opt_props=opt_props
446 )
447 if self._hash_algo:
448 ItsNodeHash(
449 "hash-1",
450 conf_node,
451 self._hash_algo
452 )
453 if self._sign_enable:
454 ItsNodeConfigurationSignature(
455 "signature-1",
456 conf_node,
457 f"{self._hash_algo},{self._sign_algo}",
458 self._sign_keyname_conf,
459 opt_props={
460 "padding": self._pad_algo,
461 "sign-images": sign_entries
462 }
463 )
464
465 def fitimage_emit_section_config(self, default_dtb_image=None):
466 if self._dtbs:
467 for dtb in self._dtbs:
468 dtb_name = dtb.name
469 if dtb.name.startswith("fdt-"):
470 dtb_name = dtb.name[len("fdt-"):]
471 self._fitimage_emit_one_section_config(self._conf_prefix + dtb_name, dtb)
472 for dtb in self._dtb_alias:
473 self._fitimage_emit_one_section_config(self._conf_prefix + dtb.alias_name, dtb)
474 else:
475 # Currently exactly one kernel is supported.
476 self._fitimage_emit_one_section_config(self._conf_prefix + "1")
477
478 default_conf = self.configurations.sub_nodes[0].name
479 if default_dtb_image and self._dtbs:
480 default_conf = self._conf_prefix + default_dtb_image
481 self.configurations.add_property('default', default_conf)
482
483 def run_mkimage_assemble(self, itsfile, fitfile):
484 cmd = [
485 self._mkimage,
486 '-f', itsfile,
487 fitfile
488 ]
489 if self._mkimage_dtcopts:
490 cmd.insert(1, '-D')
491 cmd.insert(2, self._mkimage_dtcopts)
492 try:
493 subprocess.run(cmd, check=True, capture_output=True)
494 except subprocess.CalledProcessError as e:
495 bb.fatal(f"Command '{' '.join(cmd)}' failed with return code {e.returncode}\nstdout: {e.stdout.decode()}\nstderr: {e.stderr.decode()}\nitsflile: {os.path.abspath(itsfile)}")
496
497 def run_mkimage_sign(self, fitfile):
498 if not self._sign_enable:
499 bb.debug(1, "FIT image signing is disabled. Skipping signing.")
500 return
501
502 # Some sanity checks because mkimage exits with 0 also without needed keys
503 sign_key_path = os.path.join(self._sign_keydir, self._sign_keyname_conf)
504 if not os.path.exists(sign_key_path + '.key') or not os.path.exists(sign_key_path + '.crt'):
505 bb.fatal("%s.key or .crt does not exist" % sign_key_path)
506 if self._sign_individual:
507 sign_key_img_path = os.path.join(self._sign_keydir, self._sign_keyname_img)
508 if not os.path.exists(sign_key_img_path + '.key') or not os.path.exists(sign_key_img_path + '.crt'):
509 bb.fatal("%s.key or .crt does not exist" % sign_key_img_path)
510
511 cmd = [
512 self._mkimage_sign,
513 '-F',
514 '-k', self._sign_keydir,
515 '-r', fitfile
516 ]
517 if self._mkimage_dtcopts:
518 cmd.extend(['-D', self._mkimage_dtcopts])
519 if self._mkimage_sign_args:
520 cmd.extend(shlex.split(self._mkimage_sign_args))
521 try:
522 subprocess.run(cmd, check=True, capture_output=True)
523 except subprocess.CalledProcessError as e:
524 bb.fatal(f"Command '{' '.join(cmd)}' failed with return code {e.returncode}\nstdout: {e.stdout.decode()}\nstderr: {e.stderr.decode()}")
525
526
527def symlink_points_below(file_or_symlink, expected_parent_dir):
528 """returns symlink destination if it points below directory"""
529 file_path = os.path.join(expected_parent_dir, file_or_symlink)
530 if not os.path.islink(file_path):
531 return None
532
533 realpath = os.path.relpath(os.path.realpath(file_path), expected_parent_dir)
534 if realpath.startswith(".."):
535 return None
536
537 return realpath
538
539def get_compatible_from_dtb(dtb_path, fdtget_path="fdtget"):
540 compatible = None
541 cmd = [fdtget_path, "-t", "s", dtb_path, "/", "compatible"]
542 try:
543 ret = subprocess.run(cmd, check=True, capture_output=True, text=True)
544 compatible = ret.stdout.strip().split()
545 except subprocess.CalledProcessError:
546 compatible = None
547 return compatible