binman: Only write FDT once per node
[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 dtoc import fdt_util
13 from dtoc.fdt import Fdt
14 from patman import tools
15
16 class Entry_fit(Entry):
17     """Entry containing a FIT
18
19     This calls mkimage to create a FIT (U-Boot Flat Image Tree) based on the
20     input provided.
21
22     Nodes for the FIT should be written out in the binman configuration just as
23     they would be in a file passed to mkimage.
24
25     For example, this creates an image containing a FIT with U-Boot SPL:
26
27         binman {
28             fit {
29                 description = "Test FIT";
30                 fit,fdt-list = "of-list";
31
32                 images {
33                     kernel@1 {
34                         description = "SPL";
35                         os = "u-boot";
36                         type = "rkspi";
37                         arch = "arm";
38                         compression = "none";
39                         load = <0>;
40                         entry = <0>;
41
42                         u-boot-spl {
43                         };
44                     };
45                 };
46             };
47         };
48
49     U-Boot supports creating fdt and config nodes automatically. To do this,
50     pass an of-list property (e.g. -a of-list=file1 file2). This tells binman
51     that you want to generates nodes for two files: file1.dtb and file2.dtb
52     The fit,fdt-list property (see above) indicates that of-list should be used.
53     If the property is missing you will get an error.
54
55     Then add a 'generator node', a node with a name starting with '@':
56
57         images {
58             @fdt-SEQ {
59                 description = "fdt-NAME";
60                 type = "flat_dt";
61                 compression = "none";
62             };
63         };
64
65     This tells binman to create nodes fdt-1 and fdt-2 for each of your two
66     files. All the properties you specify will be included in the node. This
67     node acts like a template to generate the nodes. The generator node itself
68     does not appear in the output - it is replaced with what binman generates.
69
70     You can create config nodes in a similar way:
71
72         configurations {
73             default = "@config-DEFAULT-SEQ";
74             @config-SEQ {
75                 description = "NAME";
76                 firmware = "uboot";
77                 loadables = "atf";
78                 fdt = "fdt-SEQ";
79             };
80         };
81
82     This tells binman to create nodes config-1 and config-2, i.e. a config for
83     each of your two files.
84
85     Available substitutions for '@' nodes are:
86
87         SEQ    Sequence number of the generated fdt (1, 2, ...)
88         NAME   Name of the dtb as provided (i.e. without adding '.dtb')
89
90     Note that if no devicetree files are provided (with '-a of-list' as above)
91     then no nodes will be generated.
92
93     The 'default' property, if present, will be automatically set to the name
94     if of configuration whose devicetree matches the 'default-dt' entry
95     argument, e.g. with '-a default-dt=sun50i-a64-pine64-lts'.
96
97     Available substitutions for '@' property values are:
98
99         DEFAULT-SEQ  Sequence number of the default fdt,as provided by the
100                      'default-dt' entry argument
101
102     Properties (in the 'fit' node itself):
103         fit,external-offset: Indicates that the contents of the FIT are external
104             and provides the external offset. This is passsed to mkimage via
105             the -E and -p flags.
106
107     """
108     def __init__(self, section, etype, node):
109         """
110         Members:
111             _fit: FIT file being built
112             _fit_sections: dict:
113                 key: relative path to entry Node (from the base of the FIT)
114                 value: Entry_section object comprising the contents of this
115                     node
116         """
117         super().__init__(section, etype, node)
118         self._fit = None
119         self._fit_sections = {}
120         self._fit_props = {}
121         for pname, prop in self._node.props.items():
122             if pname.startswith('fit,'):
123                 self._fit_props[pname] = prop
124
125         self._fdts = None
126         self._fit_list_prop = self._fit_props.get('fit,fdt-list')
127         if self._fit_list_prop:
128             fdts, = self.GetEntryArgsOrProps(
129                 [EntryArg(self._fit_list_prop.value, str)])
130             if fdts is not None:
131                 self._fdts = fdts.split()
132         self._fit_default_dt = self.GetEntryArgsOrProps([EntryArg('default-dt',
133                                                                   str)])[0]
134
135     def ReadNode(self):
136         self._ReadSubnodes()
137         super().ReadNode()
138
139     def _ReadSubnodes(self):
140         def _AddNode(base_node, depth, node):
141             """Add a node to the FIT
142
143             Args:
144                 base_node: Base Node of the FIT (with 'description' property)
145                 depth: Current node depth (0 is the base node)
146                 node: Current node to process
147
148             There are two cases to deal with:
149                 - hash and signature nodes which become part of the FIT
150                 - binman entries which are used to define the 'data' for each
151                   image
152             """
153             for pname, prop in node.props.items():
154                 if not pname.startswith('fit,'):
155                     if pname == 'default':
156                         val = prop.value
157                         # Handle the 'default' property
158                         if val.startswith('@'):
159                             if not self._fdts:
160                                 continue
161                             if not self._fit_default_dt:
162                                 self.Raise("Generated 'default' node requires default-dt entry argument")
163                             if self._fit_default_dt not in self._fdts:
164                                 self.Raise("default-dt entry argument '%s' not found in fdt list: %s" %
165                                            (self._fit_default_dt,
166                                             ', '.join(self._fdts)))
167                             seq = self._fdts.index(self._fit_default_dt)
168                             val = val[1:].replace('DEFAULT-SEQ', str(seq + 1))
169                             fsw.property_string(pname, val)
170                             continue
171                     fsw.property(pname, prop.bytes)
172
173             rel_path = node.path[len(base_node.path):]
174             in_images = rel_path.startswith('/images')
175             has_images = depth == 2 and in_images
176             if has_images:
177                 # This node is a FIT subimage node (e.g. "/images/kernel")
178                 # containing content nodes. We collect the subimage nodes and
179                 # section entries for them here to merge the content subnodes
180                 # together and put the merged contents in the subimage node's
181                 # 'data' property later.
182                 entry = Entry.Create(self.section, node, etype='section')
183                 entry.ReadNode()
184                 self._fit_sections[rel_path] = entry
185
186             for subnode in node.subnodes:
187                 if has_images and not (subnode.name.startswith('hash') or
188                                        subnode.name.startswith('signature')):
189                     # This subnode is a content node not meant to appear in
190                     # the FIT (e.g. "/images/kernel/u-boot"), so don't call
191                     # fsw.add_node() or _AddNode() for it.
192                     pass
193                 elif subnode.name.startswith('@'):
194                     if self._fdts:
195                         # Generate notes for each FDT
196                         for seq, fdt_fname in enumerate(self._fdts):
197                             node_name = subnode.name[1:].replace('SEQ',
198                                                                  str(seq + 1))
199                             fname = tools.GetInputFilename(fdt_fname + '.dtb')
200                             with fsw.add_node(node_name):
201                                 for pname, prop in subnode.props.items():
202                                     val = prop.bytes.replace(
203                                         b'NAME', tools.ToBytes(fdt_fname))
204                                     val = val.replace(
205                                         b'SEQ', tools.ToBytes(str(seq + 1)))
206                                     fsw.property(pname, val)
207
208                                 # Add data for 'fdt' nodes (but not 'config')
209                                 if depth == 1 and in_images:
210                                     fsw.property('data',
211                                                  tools.ReadFile(fname))
212                     else:
213                         if self._fdts is None:
214                             if self._fit_list_prop:
215                                 self.Raise("Generator node requires '%s' entry argument" %
216                                            self._fit_list_prop.value)
217                             else:
218                                 self.Raise("Generator node requires 'fit,fdt-list' property")
219                 else:
220                     with fsw.add_node(subnode.name):
221                         _AddNode(base_node, depth + 1, subnode)
222
223         # Build a new tree with all nodes and properties starting from the
224         # entry node
225         fsw = libfdt.FdtSw()
226         fsw.finish_reservemap()
227         with fsw.add_node(''):
228             _AddNode(self._node, 0, self._node)
229         fdt = fsw.as_fdt()
230
231         # Pack this new FDT and scan it so we can add the data later
232         fdt.pack()
233         self._fdt = Fdt.FromData(fdt.as_bytearray())
234         self._fdt.Scan()
235
236     def ObtainContents(self):
237         """Obtain the contents of the FIT
238
239         This adds the 'data' properties to the input ITB (Image-tree Binary)
240         then runs mkimage to process it.
241         """
242         # self._BuildInput() either returns bytes or raises an exception.
243         data = self._BuildInput(self._fdt)
244         uniq = self.GetUniqueName()
245         input_fname = tools.GetOutputFilename('%s.itb' % uniq)
246         output_fname = tools.GetOutputFilename('%s.fit' % uniq)
247         tools.WriteFile(input_fname, data)
248         tools.WriteFile(output_fname, data)
249
250         args = []
251         ext_offset = self._fit_props.get('fit,external-offset')
252         if ext_offset is not None:
253             args += ['-E', '-p', '%x' % fdt_util.fdt32_to_cpu(ext_offset.value)]
254         tools.Run('mkimage', '-t', '-F', output_fname, *args)
255
256         self.SetContents(tools.ReadFile(output_fname))
257         return True
258
259     def _BuildInput(self, fdt):
260         """Finish the FIT by adding the 'data' properties to it
261
262         Arguments:
263             fdt: FIT to update
264
265         Returns:
266             New fdt contents (bytes)
267         """
268         for path, section in self._fit_sections.items():
269             node = fdt.GetNode(path)
270             # Entry_section.ObtainContents() either returns True or
271             # raises an exception.
272             section.ObtainContents()
273             section.Pack(0)
274             data = section.GetData()
275             node.AddData('data', data)
276
277         fdt.Sync(auto_resize=True)
278         data = fdt.GetContents()
279         return data
280
281     def CheckMissing(self, missing_list):
282         """Check if any entries in this FIT have missing external blobs
283
284         If there are missing blobs, the entries are added to the list
285
286         Args:
287             missing_list: List of Entry objects to be added to
288         """
289         for path, section in self._fit_sections.items():
290             section.CheckMissing(missing_list)
291
292     def SetAllowMissing(self, allow_missing):
293         for section in self._fit_sections.values():
294             section.SetAllowMissing(allow_missing)