binman: Support shrinking a entry after packing
[oweals/u-boot.git] / tools / binman / state.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright 2018 Google, Inc
3 # Written by Simon Glass <sjg@chromium.org>
4 #
5 # Holds and modifies the state information held by binman
6 #
7
8 import hashlib
9 import re
10
11 import fdt
12 import os
13 import tools
14 import tout
15
16 # Records the device-tree files known to binman, keyed by entry type (e.g.
17 # 'u-boot-spl-dtb'). These are the output FDT files, which can be updated by
18 # binman. They have been copied to <xxx>.out files.
19 #
20 #   key: entry type
21 #   value: tuple:
22 #       Fdt object
23 #       Filename
24 #       Entry object, or None if not known
25 output_fdt_info = {}
26
27 # Prefix to add to an fdtmap path to turn it into a path to the /binman node
28 fdt_path_prefix = ''
29
30 # Arguments passed to binman to provide arguments to entries
31 entry_args = {}
32
33 # True to use fake device-tree files for testing (see U_BOOT_DTB_DATA in
34 # ftest.py)
35 use_fake_dtb = False
36
37 # The DTB which contains the full image information
38 main_dtb = None
39
40 # Allow entries to expand after they have been packed. This is detected and
41 # forces a re-pack. If not allowed, any attempted expansion causes an error in
42 # Entry.ProcessContentsUpdate()
43 allow_entry_expansion = True
44
45 # Don't allow entries to contract after they have been packed. Instead just
46 # leave some wasted space. If allowed, this is detected and forces a re-pack,
47 # but may result in entries that oscillate in size, thus causing a pack error.
48 # An example is a compressed device tree where the original offset values
49 # result in a larger compressed size than the new ones, but then after updating
50 # to the new ones, the compressed size increases, etc.
51 allow_entry_contraction = False
52
53 def GetFdtForEtype(etype):
54     """Get the Fdt object for a particular device-tree entry
55
56     Binman keeps track of at least one device-tree file called u-boot.dtb but
57     can also have others (e.g. for SPL). This function looks up the given
58     entry and returns the associated Fdt object.
59
60     Args:
61         etype: Entry type of device tree (e.g. 'u-boot-dtb')
62
63     Returns:
64         Fdt object associated with the entry type
65     """
66     value = output_fdt_info.get(etype);
67     if not value:
68         return None
69     return value[0]
70
71 def GetFdtPath(etype):
72     """Get the full pathname of a particular Fdt object
73
74     Similar to GetFdtForEtype() but returns the pathname associated with the
75     Fdt.
76
77     Args:
78         etype: Entry type of device tree (e.g. 'u-boot-dtb')
79
80     Returns:
81         Full path name to the associated Fdt
82     """
83     return output_fdt_info[etype][0]._fname
84
85 def GetFdtContents(etype='u-boot-dtb'):
86     """Looks up the FDT pathname and contents
87
88     This is used to obtain the Fdt pathname and contents when needed by an
89     entry. It supports a 'fake' dtb, allowing tests to substitute test data for
90     the real dtb.
91
92     Args:
93         etype: Entry type to look up (e.g. 'u-boot.dtb').
94
95     Returns:
96         tuple:
97             pathname to Fdt
98             Fdt data (as bytes)
99     """
100     if etype not in output_fdt_info:
101         return None, None
102     if not use_fake_dtb:
103         pathname = GetFdtPath(etype)
104         data = GetFdtForEtype(etype).GetContents()
105     else:
106         fname = output_fdt_info[etype][1]
107         pathname = tools.GetInputFilename(fname)
108         data = tools.ReadFile(pathname)
109     return pathname, data
110
111 def SetEntryArgs(args):
112     """Set the value of the entry args
113
114     This sets up the entry_args dict which is used to supply entry arguments to
115     entries.
116
117     Args:
118         args: List of entry arguments, each in the format "name=value"
119     """
120     global entry_args
121
122     entry_args = {}
123     if args:
124         for arg in args:
125             m = re.match('([^=]*)=(.*)', arg)
126             if not m:
127                 raise ValueError("Invalid entry arguemnt '%s'" % arg)
128             entry_args[m.group(1)] = m.group(2)
129
130 def GetEntryArg(name):
131     """Get the value of an entry argument
132
133     Args:
134         name: Name of argument to retrieve
135
136     Returns:
137         String value of argument
138     """
139     return entry_args.get(name)
140
141 def Prepare(images, dtb):
142     """Get device tree files ready for use
143
144     This sets up a set of device tree files that can be retrieved by
145     GetAllFdts(). This includes U-Boot proper and any SPL device trees.
146
147     Args:
148         images: List of images being used
149         dtb: Main dtb
150     """
151     global output_fdt_info, main_dtb, fdt_path_prefix
152     # Import these here in case libfdt.py is not available, in which case
153     # the above help option still works.
154     import fdt
155     import fdt_util
156
157     # If we are updating the DTBs we need to put these updated versions
158     # where Entry_blob_dtb can find them. We can ignore 'u-boot.dtb'
159     # since it is assumed to be the one passed in with options.dt, and
160     # was handled just above.
161     main_dtb = dtb
162     output_fdt_info.clear()
163     fdt_path_prefix = ''
164     output_fdt_info['u-boot-dtb'] = [dtb, 'u-boot.dtb', None]
165     output_fdt_info['u-boot-spl-dtb'] = [dtb, 'spl/u-boot-spl.dtb', None]
166     output_fdt_info['u-boot-tpl-dtb'] = [dtb, 'tpl/u-boot-tpl.dtb', None]
167     if not use_fake_dtb:
168         fdt_set = {}
169         for image in images.values():
170             fdt_set.update(image.GetFdts())
171         for etype, other in fdt_set.items():
172             entry, other_fname = other
173             infile = tools.GetInputFilename(other_fname)
174             other_fname_dtb = fdt_util.EnsureCompiled(infile)
175             out_fname = tools.GetOutputFilename('%s.out' %
176                     os.path.split(other_fname)[1])
177             tools.WriteFile(out_fname, tools.ReadFile(other_fname_dtb))
178             other_dtb = fdt.FdtScan(out_fname)
179             output_fdt_info[etype] = [other_dtb, out_fname, entry]
180
181 def PrepareFromLoadedData(image):
182     """Get device tree files ready for use with a loaded image
183
184     Loaded images are different from images that are being created by binman,
185     since there is generally already an fdtmap and we read the description from
186     that. This provides the position and size of every entry in the image with
187     no calculation required.
188
189     This function uses the same output_fdt_info[] as Prepare(). It finds the
190     device tree files, adds a reference to the fdtmap and sets the FDT path
191     prefix to translate from the fdtmap (where the root node is the image node)
192     to the normal device tree (where the image node is under a /binman node).
193
194     Args:
195         images: List of images being used
196     """
197     global output_fdt_info, main_dtb, fdt_path_prefix
198
199     tout.Info('Preparing device trees')
200     output_fdt_info.clear()
201     fdt_path_prefix = ''
202     output_fdt_info['fdtmap'] = [image.fdtmap_dtb, 'u-boot.dtb', None]
203     main_dtb = None
204     tout.Info("   Found device tree type 'fdtmap' '%s'" % image.fdtmap_dtb.name)
205     for etype, value in image.GetFdts().items():
206         entry, fname = value
207         out_fname = tools.GetOutputFilename('%s.dtb' % entry.etype)
208         tout.Info("   Found device tree type '%s' at '%s' path '%s'" %
209                   (etype, out_fname, entry.GetPath()))
210         entry._filename = entry.GetDefaultFilename()
211         data = entry.ReadData()
212
213         tools.WriteFile(out_fname, data)
214         dtb = fdt.Fdt(out_fname)
215         dtb.Scan()
216         image_node = dtb.GetNode('/binman')
217         if 'multiple-images' in image_node.props:
218             image_node = dtb.GetNode('/binman/%s' % image.image_node)
219         fdt_path_prefix = image_node.path
220         output_fdt_info[etype] = [dtb, None, entry]
221     tout.Info("   FDT path prefix '%s'" % fdt_path_prefix)
222
223
224 def GetAllFdts():
225     """Yield all device tree files being used by binman
226
227     Yields:
228         Device trees being used (U-Boot proper, SPL, TPL)
229     """
230     if main_dtb:
231         yield main_dtb
232     for etype in output_fdt_info:
233         dtb = output_fdt_info[etype][0]
234         if dtb != main_dtb:
235             yield dtb
236
237 def GetUpdateNodes(node, for_repack=False):
238     """Yield all the nodes that need to be updated in all device trees
239
240     The property referenced by this node is added to any device trees which
241     have the given node. Due to removable of unwanted notes, SPL and TPL may
242     not have this node.
243
244     Args:
245         node: Node object in the main device tree to look up
246         for_repack: True if we want only nodes which need 'repack' properties
247             added to them (e.g. 'orig-offset'), False to return all nodes. We
248             don't add repack properties to SPL/TPL device trees.
249
250     Yields:
251         Node objects in each device tree that is in use (U-Boot proper, which
252             is node, SPL and TPL)
253     """
254     yield node
255     for dtb, fname, entry in output_fdt_info.values():
256         if dtb != node.GetFdt():
257             if for_repack and entry.etype != 'u-boot-dtb':
258                 continue
259             other_node = dtb.GetNode(fdt_path_prefix + node.path)
260             #print('   try', fdt_path_prefix + node.path, other_node)
261             if other_node:
262                 yield other_node
263
264 def AddZeroProp(node, prop, for_repack=False):
265     """Add a new property to affected device trees with an integer value of 0.
266
267     Args:
268         prop_name: Name of property
269         for_repack: True is this property is only needed for repacking
270     """
271     for n in GetUpdateNodes(node, for_repack):
272         n.AddZeroProp(prop)
273
274 def AddSubnode(node, name):
275     """Add a new subnode to a node in affected device trees
276
277     Args:
278         node: Node to add to
279         name: name of node to add
280
281     Returns:
282         New subnode that was created in main tree
283     """
284     first = None
285     for n in GetUpdateNodes(node):
286         subnode = n.AddSubnode(name)
287         if not first:
288             first = subnode
289     return first
290
291 def AddString(node, prop, value):
292     """Add a new string property to affected device trees
293
294     Args:
295         prop_name: Name of property
296         value: String value (which will be \0-terminated in the DT)
297     """
298     for n in GetUpdateNodes(node):
299         n.AddString(prop, value)
300
301 def SetInt(node, prop, value, for_repack=False):
302     """Update an integer property in affected device trees with an integer value
303
304     This is not allowed to change the size of the FDT.
305
306     Args:
307         prop_name: Name of property
308         for_repack: True is this property is only needed for repacking
309     """
310     for n in GetUpdateNodes(node, for_repack):
311         tout.Detail("File %s: Update node '%s' prop '%s' to %#x" %
312                     (n.GetFdt().name, n.path, prop, value))
313         n.SetInt(prop, value)
314
315 def CheckAddHashProp(node):
316     hash_node = node.FindNode('hash')
317     if hash_node:
318         algo = hash_node.props.get('algo')
319         if not algo:
320             return "Missing 'algo' property for hash node"
321         if algo.value == 'sha256':
322             size = 32
323         else:
324             return "Unknown hash algorithm '%s'" % algo
325         for n in GetUpdateNodes(hash_node):
326             n.AddEmptyProp('value', size)
327
328 def CheckSetHashValue(node, get_data_func):
329     hash_node = node.FindNode('hash')
330     if hash_node:
331         algo = hash_node.props.get('algo').value
332         if algo == 'sha256':
333             m = hashlib.sha256()
334             m.update(get_data_func())
335             data = m.digest()
336         for n in GetUpdateNodes(hash_node):
337             n.SetData('value', data)
338
339 def SetAllowEntryExpansion(allow):
340     """Set whether post-pack expansion of entries is allowed
341
342     Args:
343        allow: True to allow expansion, False to raise an exception
344     """
345     global allow_entry_expansion
346
347     allow_entry_expansion = allow
348
349 def AllowEntryExpansion():
350     """Check whether post-pack expansion of entries is allowed
351
352     Returns:
353         True if expansion should be allowed, False if an exception should be
354             raised
355     """
356     return allow_entry_expansion
357
358 def SetAllowEntryContraction(allow):
359     """Set whether post-pack contraction of entries is allowed
360
361     Args:
362        allow: True to allow contraction, False to raise an exception
363     """
364     global allow_entry_contraction
365
366     allow_entry_contraction = allow
367
368 def AllowEntryContraction():
369     """Check whether post-pack contraction of entries is allowed
370
371     Returns:
372         True if contraction should be allowed, False if an exception should be
373             raised
374     """
375     return allow_entry_contraction