binman: Update fit to move node reading into the ReadNode() method
[platform/kernel/u-boot.git] / tools / binman / etype / fit.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
3 # Written by Simon Glass <sjg@chromium.org>
4 #
5 # Entry-type module for producing a FIT
6 #
7
8 from collections import defaultdict, OrderedDict
9 import libfdt
10
11 from binman.entry import Entry, EntryArg
12 from binman.etype.section import Entry_section
13 from dtoc import fdt_util
14 from dtoc.fdt import Fdt
15 from patman import tools
16
17 # Supported operations, with the fit,operation property
18 OP_GEN_FDT_NODES = range(1)
19 OPERATIONS = {
20     'gen-fdt-nodes': OP_GEN_FDT_NODES,
21     }
22
23 class Entry_fit(Entry_section):
24
25     """Flat Image Tree (FIT)
26
27     This calls mkimage to create a FIT (U-Boot Flat Image Tree) based on the
28     input provided.
29
30     Nodes for the FIT should be written out in the binman configuration just as
31     they would be in a file passed to mkimage.
32
33     For example, this creates an image containing a FIT with U-Boot SPL::
34
35         binman {
36             fit {
37                 description = "Test FIT";
38                 fit,fdt-list = "of-list";
39
40                 images {
41                     kernel@1 {
42                         description = "SPL";
43                         os = "u-boot";
44                         type = "rkspi";
45                         arch = "arm";
46                         compression = "none";
47                         load = <0>;
48                         entry = <0>;
49
50                         u-boot-spl {
51                         };
52                     };
53                 };
54             };
55         };
56
57     More complex setups can be created, with generated nodes, as described
58     below.
59
60     Properties (in the 'fit' node itself)
61     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62
63     Special properties have a `fit,` prefix, indicating that they should be
64     processed but not included in the final FIT.
65
66     The top-level 'fit' node supports the following special properties:
67
68         fit,external-offset
69             Indicates that the contents of the FIT are external and provides the
70             external offset. This is passed to mkimage via the -E and -p flags.
71
72         fit,fdt-list
73             Indicates the entry argument which provides the list of device tree
74             files for the gen-fdt-nodes operation (as below). This is often
75             `of-list` meaning that `-a of-list="dtb1 dtb2..."` should be passed
76             to binman.
77
78     Substitutions
79     ~~~~~~~~~~~~~
80
81     Node names and property values support a basic string-substitution feature.
82     Available substitutions for '@' nodes (and property values) are:
83
84     SEQ:
85         Sequence number of the generated fdt (1, 2, ...)
86     NAME
87         Name of the dtb as provided (i.e. without adding '.dtb')
88
89     The `default` property, if present, will be automatically set to the name
90     if of configuration whose devicetree matches the `default-dt` entry
91     argument, e.g. with `-a default-dt=sun50i-a64-pine64-lts`.
92
93     Available substitutions for property values in these nodes are:
94
95     DEFAULT-SEQ:
96         Sequence number of the default fdt, as provided by the 'default-dt'
97         entry argument
98
99     Available operations
100     ~~~~~~~~~~~~~~~~~~~~
101
102     You can add an operation to an '@' node to indicate which operation is
103     required::
104
105         @fdt-SEQ {
106             fit,operation = "gen-fdt-nodes";
107             ...
108         };
109
110     Available operations are:
111
112     gen-fdt-nodes
113         Generate FDT nodes as above. This is the default if there is no
114         `fit,operation` property.
115
116     Generating nodes from an FDT list (gen-fdt-nodes)
117     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118
119     U-Boot supports creating fdt and config nodes automatically. To do this,
120     pass an `of-list` property (e.g. `-a of-list=file1 file2`). This tells
121     binman that you want to generates nodes for two files: `file1.dtb` and
122     `file2.dtb`. The `fit,fdt-list` property (see above) indicates that
123     `of-list` should be used. If the property is missing you will get an error.
124
125     Then add a 'generator node', a node with a name starting with '@'::
126
127         images {
128             @fdt-SEQ {
129                 description = "fdt-NAME";
130                 type = "flat_dt";
131                 compression = "none";
132             };
133         };
134
135     This tells binman to create nodes `fdt-1` and `fdt-2` for each of your two
136     files. All the properties you specify will be included in the node. This
137     node acts like a template to generate the nodes. The generator node itself
138     does not appear in the output - it is replaced with what binman generates.
139     A 'data' property is created with the contents of the FDT file.
140
141     You can create config nodes in a similar way::
142
143         configurations {
144             default = "@config-DEFAULT-SEQ";
145             @config-SEQ {
146                 description = "NAME";
147                 firmware = "atf";
148                 loadables = "uboot";
149                 fdt = "fdt-SEQ";
150             };
151         };
152
153     This tells binman to create nodes `config-1` and `config-2`, i.e. a config
154     for each of your two files.
155
156     Note that if no devicetree files are provided (with '-a of-list' as above)
157     then no nodes will be generated.
158     """
159     def __init__(self, section, etype, node):
160         """
161         Members:
162             _fit: FIT file being built
163             _entries: dict from Entry_section:
164                 key: relative path to entry Node (from the base of the FIT)
165                 value: Entry_section object comprising the contents of this
166                     node
167         """
168         super().__init__(section, etype, node)
169         self._fit = None
170         self._fit_props = {}
171         self._fdts = None
172         self.mkimage = None
173
174     def ReadNode(self):
175         super().ReadNode()
176         for pname, prop in self._node.props.items():
177             if pname.startswith('fit,'):
178                 self._fit_props[pname] = prop
179         self._fit_list_prop = self._fit_props.get('fit,fdt-list')
180         if self._fit_list_prop:
181             fdts, = self.GetEntryArgsOrProps(
182                 [EntryArg(self._fit_list_prop.value, str)])
183             if fdts is not None:
184                 self._fdts = fdts.split()
185         self._fit_default_dt = self.GetEntryArgsOrProps([EntryArg('default-dt',
186                                                                   str)])[0]
187
188     def _get_operation(self, subnode):
189         """Get the operation referenced by a subnode
190
191         Args:
192             subnode (Node): Subnode (of the FIT) to check
193
194         Returns:
195             int: Operation to perform
196
197         Raises:
198             ValueError: Invalid operation name
199         """
200         oper_name = subnode.props.get('fit,operation')
201         if not oper_name:
202             return OP_GEN_FDT_NODES
203         oper = OPERATIONS.get(oper_name.value)
204         if not oper:
205             self.Raise(f"Unknown operation '{oper_name.value}'")
206         return oper
207
208     def ReadEntries(self):
209         def _add_entries(base_node, depth, node):
210             """Add entries for any nodes that need them
211
212             Args:
213                 base_node: Base Node of the FIT (with 'description' property)
214                 depth: Current node depth (0 is the base 'fit' node)
215                 node: Current node to process
216
217             Here we only need to provide binman entries which are used to define
218             the 'data' for each image. We create an entry_Section for each.
219             """
220             rel_path = node.path[len(base_node.path):]
221             in_images = rel_path.startswith('/images')
222             has_images = depth == 2 and in_images
223             if has_images:
224                 # This node is a FIT subimage node (e.g. "/images/kernel")
225                 # containing content nodes. We collect the subimage nodes and
226                 # section entries for them here to merge the content subnodes
227                 # together and put the merged contents in the subimage node's
228                 # 'data' property later.
229                 entry = Entry.Create(self.section, node, etype='section')
230                 entry.ReadNode()
231                 # The hash subnodes here are for mkimage, not binman.
232                 entry.SetUpdateHash(False)
233                 self._entries[rel_path] = entry
234
235             for subnode in node.subnodes:
236                 _add_entries(base_node, depth + 1, subnode)
237
238         _add_entries(self._node, 0, self._node)
239
240     def BuildSectionData(self, required):
241         """Build FIT entry contents
242
243         This adds the 'data' properties to the input ITB (Image-tree Binary)
244         then runs mkimage to process it.
245
246         Args:
247             required: True if the data must be present, False if it is OK to
248                 return None
249
250         Returns:
251             Contents of the section (bytes)
252         """
253         data = self._BuildInput()
254         uniq = self.GetUniqueName()
255         input_fname = tools.get_output_filename('%s.itb' % uniq)
256         output_fname = tools.get_output_filename('%s.fit' % uniq)
257         tools.write_file(input_fname, data)
258         tools.write_file(output_fname, data)
259
260         args = {}
261         ext_offset = self._fit_props.get('fit,external-offset')
262         if ext_offset is not None:
263             args = {
264                 'external': True,
265                 'pad': fdt_util.fdt32_to_cpu(ext_offset.value)
266                 }
267         if self.mkimage.run(reset_timestamp=True, output_fname=output_fname,
268                             **args) is None:
269             # Bintool is missing; just use empty data as the output
270             self.record_missing_bintool(self.mkimage)
271             return tools.get_bytes(0, 1024)
272
273         return tools.read_file(output_fname)
274
275     def _BuildInput(self):
276         """Finish the FIT by adding the 'data' properties to it
277
278         Arguments:
279             fdt: FIT to update
280
281         Returns:
282             New fdt contents (bytes)
283         """
284         def _process_prop(pname, prop):
285             """Process special properties
286
287             Handles properties with generated values. At present the only
288             supported property is 'default', i.e. the default device tree in
289             the configurations node.
290
291             Args:
292                 pname (str): Name of property
293                 prop (Prop): Property to process
294             """
295             if pname == 'default':
296                 val = prop.value
297                 # Handle the 'default' property
298                 if val.startswith('@'):
299                     if not self._fdts:
300                         return
301                     if not self._fit_default_dt:
302                         self.Raise("Generated 'default' node requires default-dt entry argument")
303                     if self._fit_default_dt not in self._fdts:
304                         self.Raise("default-dt entry argument '%s' not found in fdt list: %s" %
305                                    (self._fit_default_dt,
306                                     ', '.join(self._fdts)))
307                     seq = self._fdts.index(self._fit_default_dt)
308                     val = val[1:].replace('DEFAULT-SEQ', str(seq + 1))
309                     fsw.property_string(pname, val)
310                     return
311             elif pname.startswith('fit,'):
312                 # Ignore these, which are commands for binman to process
313                 return
314             elif pname in ['offset', 'size', 'image-pos']:
315                 # Don't add binman's calculated properties
316                 return
317             fsw.property(pname, prop.bytes)
318
319         def _gen_fdt_nodes(subnode, depth, in_images):
320             """Generate FDT nodes
321
322             This creates one node for each member of self._fdts using the
323             provided template. If a property value contains 'NAME' it is
324             replaced with the filename of the FDT. If a property value contains
325             SEQ it is replaced with the node sequence number, where 1 is the
326             first.
327
328             Args:
329                 subnode (None): Generator node to process
330                 depth: Current node depth (0 is the base 'fit' node)
331                 in_images: True if this is inside the 'images' node, so that
332                     'data' properties should be generated
333             """
334             if self._fdts:
335                 # Generate nodes for each FDT
336                 for seq, fdt_fname in enumerate(self._fdts):
337                     node_name = subnode.name[1:].replace('SEQ', str(seq + 1))
338                     fname = tools.get_input_filename(fdt_fname + '.dtb')
339                     with fsw.add_node(node_name):
340                         for pname, prop in subnode.props.items():
341                             val = prop.bytes.replace(
342                                 b'NAME', tools.to_bytes(fdt_fname))
343                             val = val.replace(
344                                 b'SEQ', tools.to_bytes(str(seq + 1)))
345                             fsw.property(pname, val)
346
347                         # Add data for 'images' nodes (but not 'config')
348                         if depth == 1 and in_images:
349                             fsw.property('data', tools.read_file(fname))
350
351                         for subnode in node.subnodes:
352                             with fsw.add_node(subnode.name):
353                                 _add_node(node, depth + 1, subnode)
354             else:
355                 if self._fdts is None:
356                     if self._fit_list_prop:
357                         self.Raise("Generator node requires '%s' entry argument" %
358                                    self._fit_list_prop.value)
359                     else:
360                         self.Raise("Generator node requires 'fit,fdt-list' property")
361
362         def _gen_node(subnode, depth, in_images):
363             """Generate nodes from a template
364
365             This creates one node for each member of self._fdts using the
366             provided template. If a property value contains 'NAME' it is
367             replaced with the filename of the FDT. If a property value contains
368             SEQ it is replaced with the node sequence number, where 1 is the
369             first.
370
371             Args:
372                 subnode (None): Generator node to process
373                 depth: Current node depth (0 is the base 'fit' node)
374                 in_images: True if this is inside the 'images' node, so that
375                     'data' properties should be generated
376             """
377             oper = self._get_operation(subnode)
378             if oper == OP_GEN_FDT_NODES:
379                 _gen_fdt_nodes(subnode, depth, in_images)
380
381         def _add_node(base_node, depth, node):
382             """Add nodes to the output FIT
383
384             Args:
385                 base_node: Base Node of the FIT (with 'description' property)
386                 depth: Current node depth (0 is the base 'fit' node)
387                 node: Current node to process
388
389             There are two cases to deal with:
390                 - hash and signature nodes which become part of the FIT
391                 - binman entries which are used to define the 'data' for each
392                   image, so don't appear in the FIT
393             """
394             # Copy over all the relevant properties
395             for pname, prop in node.props.items():
396                 _process_prop(pname, prop)
397
398             rel_path = node.path[len(base_node.path):]
399             in_images = rel_path.startswith('/images')
400
401             has_images = depth == 2 and in_images
402             if has_images:
403                 entry = self._entries[rel_path]
404                 data = entry.GetData()
405                 fsw.property('data', bytes(data))
406
407             for subnode in node.subnodes:
408                 if has_images and not (subnode.name.startswith('hash') or
409                                        subnode.name.startswith('signature')):
410                     # This subnode is a content node not meant to appear in
411                     # the FIT (e.g. "/images/kernel/u-boot"), so don't call
412                     # fsw.add_node() or _add_node() for it.
413                     pass
414                 elif self.GetImage().generate and subnode.name.startswith('@'):
415                     subnode_path = f'{rel_path}/{subnode.name}'
416                     entry = self._entries.get(subnode_path)
417                     _gen_node(subnode, depth, in_images)
418                     if entry:
419                         del self._entries[subnode_path]
420                 else:
421                     with fsw.add_node(subnode.name):
422                         _add_node(base_node, depth + 1, subnode)
423
424         # Build a new tree with all nodes and properties starting from the
425         # entry node
426         fsw = libfdt.FdtSw()
427         fsw.finish_reservemap()
428         with fsw.add_node(''):
429             _add_node(self._node, 0, self._node)
430         fdt = fsw.as_fdt()
431
432         # Pack this new FDT and scan it so we can add the data later
433         fdt.pack()
434         data = fdt.as_bytearray()
435         return data
436
437     def SetImagePos(self, image_pos):
438         """Set the position in the image
439
440         This sets each subentry's offsets, sizes and positions-in-image
441         according to where they ended up in the packed FIT file.
442
443         Args:
444             image_pos: Position of this entry in the image
445         """
446         super().SetImagePos(image_pos)
447
448         # If mkimage is missing we'll have empty data,
449         # which will cause a FDT_ERR_BADMAGIC error
450         if self.mkimage in self.missing_bintools:
451             return
452
453         fdt = Fdt.FromData(self.GetData())
454         fdt.Scan()
455
456         for path, section in self._entries.items():
457             node = fdt.GetNode(path)
458
459             data_prop = node.props.get("data")
460             data_pos = fdt_util.GetInt(node, "data-position")
461             data_offset = fdt_util.GetInt(node, "data-offset")
462             data_size = fdt_util.GetInt(node, "data-size")
463
464             # Contents are inside the FIT
465             if data_prop is not None:
466                 # GetOffset() returns offset of a fdt_property struct,
467                 # which has 3 fdt32_t members before the actual data.
468                 offset = data_prop.GetOffset() + 12
469                 size = len(data_prop.bytes)
470
471             # External offset from the base of the FIT
472             elif data_pos is not None:
473                 offset = data_pos
474                 size = data_size
475
476             # External offset from the end of the FIT, not used in binman
477             elif data_offset is not None: # pragma: no cover
478                 offset = fdt.GetFdtObj().totalsize() + data_offset
479                 size = data_size
480
481             # This should never happen
482             else: # pragma: no cover
483                 self.Raise("%s: missing data properties" % (path))
484
485             section.SetOffsetSize(offset, size)
486             section.SetImagePos(self.image_pos)
487
488     def AddBintools(self, btools):
489         super().AddBintools(btools)
490         self.mkimage = self.AddBintool(btools, 'mkimage')