binman: Support expanding entries
[platform/kernel/u-boot.git] / tools / binman / entry.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
3 #
4 # Base class for all entries
5 #
6
7 from __future__ import print_function
8
9 from collections import namedtuple
10
11 # importlib was introduced in Python 2.7 but there was a report of it not
12 # working in 2.7.12, so we work around this:
13 # http://lists.denx.de/pipermail/u-boot/2016-October/269729.html
14 try:
15     import importlib
16     have_importlib = True
17 except:
18     have_importlib = False
19
20 import os
21 from sets import Set
22 import sys
23
24 import fdt_util
25 import state
26 import tools
27
28 modules = {}
29
30 our_path = os.path.dirname(os.path.realpath(__file__))
31
32
33 # An argument which can be passed to entries on the command line, in lieu of
34 # device-tree properties.
35 EntryArg = namedtuple('EntryArg', ['name', 'datatype'])
36
37
38 class Entry(object):
39     """An Entry in the section
40
41     An entry corresponds to a single node in the device-tree description
42     of the section. Each entry ends up being a part of the final section.
43     Entries can be placed either right next to each other, or with padding
44     between them. The type of the entry determines the data that is in it.
45
46     This class is not used by itself. All entry objects are subclasses of
47     Entry.
48
49     Attributes:
50         section: Section object containing this entry
51         node: The node that created this entry
52         offset: Offset of entry within the section, None if not known yet (in
53             which case it will be calculated by Pack())
54         size: Entry size in bytes, None if not known
55         contents_size: Size of contents in bytes, 0 by default
56         align: Entry start offset alignment, or None
57         align_size: Entry size alignment, or None
58         align_end: Entry end offset alignment, or None
59         pad_before: Number of pad bytes before the contents, 0 if none
60         pad_after: Number of pad bytes after the contents, 0 if none
61         data: Contents of entry (string of bytes)
62     """
63     def __init__(self, section, etype, node, read_node=True, name_prefix=''):
64         self.section = section
65         self.etype = etype
66         self._node = node
67         self.name = node and (name_prefix + node.name) or 'none'
68         self.offset = None
69         self.size = None
70         self.data = None
71         self.contents_size = 0
72         self.align = None
73         self.align_size = None
74         self.align_end = None
75         self.pad_before = 0
76         self.pad_after = 0
77         self.offset_unset = False
78         self.image_pos = None
79         self._expand_size = False
80         if read_node:
81             self.ReadNode()
82
83     @staticmethod
84     def Lookup(section, node_path, etype):
85         """Look up the entry class for a node.
86
87         Args:
88             section:   Section object containing this node
89             node_node: Path name of Node object containing information about
90                        the entry to create (used for errors)
91             etype:   Entry type to use
92
93         Returns:
94             The entry class object if found, else None
95         """
96         # Convert something like 'u-boot@0' to 'u_boot' since we are only
97         # interested in the type.
98         module_name = etype.replace('-', '_')
99         if '@' in module_name:
100             module_name = module_name.split('@')[0]
101         module = modules.get(module_name)
102
103         # Also allow entry-type modules to be brought in from the etype directory.
104
105         # Import the module if we have not already done so.
106         if not module:
107             old_path = sys.path
108             sys.path.insert(0, os.path.join(our_path, 'etype'))
109             try:
110                 if have_importlib:
111                     module = importlib.import_module(module_name)
112                 else:
113                     module = __import__(module_name)
114             except ImportError as e:
115                 raise ValueError("Unknown entry type '%s' in node '%s' (expected etype/%s.py, error '%s'" %
116                                  (etype, node_path, module_name, e))
117             finally:
118                 sys.path = old_path
119             modules[module_name] = module
120
121         # Look up the expected class name
122         return getattr(module, 'Entry_%s' % module_name)
123
124     @staticmethod
125     def Create(section, node, etype=None):
126         """Create a new entry for a node.
127
128         Args:
129             section: Section object containing this node
130             node:    Node object containing information about the entry to
131                      create
132             etype:   Entry type to use, or None to work it out (used for tests)
133
134         Returns:
135             A new Entry object of the correct type (a subclass of Entry)
136         """
137         if not etype:
138             etype = fdt_util.GetString(node, 'type', node.name)
139         obj = Entry.Lookup(section, node.path, etype)
140
141         # Call its constructor to get the object we want.
142         return obj(section, etype, node)
143
144     def ReadNode(self):
145         """Read entry information from the node
146
147         This reads all the fields we recognise from the node, ready for use.
148         """
149         if 'pos' in self._node.props:
150             self.Raise("Please use 'offset' instead of 'pos'")
151         self.offset = fdt_util.GetInt(self._node, 'offset')
152         self.size = fdt_util.GetInt(self._node, 'size')
153         self.align = fdt_util.GetInt(self._node, 'align')
154         if tools.NotPowerOfTwo(self.align):
155             raise ValueError("Node '%s': Alignment %s must be a power of two" %
156                              (self._node.path, self.align))
157         self.pad_before = fdt_util.GetInt(self._node, 'pad-before', 0)
158         self.pad_after = fdt_util.GetInt(self._node, 'pad-after', 0)
159         self.align_size = fdt_util.GetInt(self._node, 'align-size')
160         if tools.NotPowerOfTwo(self.align_size):
161             raise ValueError("Node '%s': Alignment size %s must be a power "
162                              "of two" % (self._node.path, self.align_size))
163         self.align_end = fdt_util.GetInt(self._node, 'align-end')
164         self.offset_unset = fdt_util.GetBool(self._node, 'offset-unset')
165         self.expand_size = fdt_util.GetBool(self._node, 'expand-size')
166
167     def GetDefaultFilename(self):
168         return None
169
170     def GetFdtSet(self):
171         """Get the set of device trees used by this entry
172
173         Returns:
174             Set containing the filename from this entry, if it is a .dtb, else
175             an empty set
176         """
177         fname = self.GetDefaultFilename()
178         # It would be better to use isinstance(self, Entry_blob_dtb) here but
179         # we cannot access Entry_blob_dtb
180         if fname and fname.endswith('.dtb'):
181             return Set([fname])
182         return Set()
183
184     def ExpandEntries(self):
185         pass
186
187     def AddMissingProperties(self):
188         """Add new properties to the device tree as needed for this entry"""
189         for prop in ['offset', 'size', 'image-pos']:
190             if not prop in self._node.props:
191                 state.AddZeroProp(self._node, prop)
192
193     def SetCalculatedProperties(self):
194         """Set the value of device-tree properties calculated by binman"""
195         state.SetInt(self._node, 'offset', self.offset)
196         state.SetInt(self._node, 'size', self.size)
197         state.SetInt(self._node, 'image-pos', self.image_pos)
198
199     def ProcessFdt(self, fdt):
200         """Allow entries to adjust the device tree
201
202         Some entries need to adjust the device tree for their purposes. This
203         may involve adding or deleting properties.
204
205         Returns:
206             True if processing is complete
207             False if processing could not be completed due to a dependency.
208                 This will cause the entry to be retried after others have been
209                 called
210         """
211         return True
212
213     def SetPrefix(self, prefix):
214         """Set the name prefix for a node
215
216         Args:
217             prefix: Prefix to set, or '' to not use a prefix
218         """
219         if prefix:
220             self.name = prefix + self.name
221
222     def SetContents(self, data):
223         """Set the contents of an entry
224
225         This sets both the data and content_size properties
226
227         Args:
228             data: Data to set to the contents (string)
229         """
230         self.data = data
231         self.contents_size = len(self.data)
232
233     def ProcessContentsUpdate(self, data):
234         """Update the contens of an entry, after the size is fixed
235
236         This checks that the new data is the same size as the old.
237
238         Args:
239             data: Data to set to the contents (string)
240
241         Raises:
242             ValueError if the new data size is not the same as the old
243         """
244         if len(data) != self.contents_size:
245             self.Raise('Cannot update entry size from %d to %d' %
246                        (len(data), self.contents_size))
247         self.SetContents(data)
248
249     def ObtainContents(self):
250         """Figure out the contents of an entry.
251
252         Returns:
253             True if the contents were found, False if another call is needed
254             after the other entries are processed.
255         """
256         # No contents by default: subclasses can implement this
257         return True
258
259     def Pack(self, offset):
260         """Figure out how to pack the entry into the section
261
262         Most of the time the entries are not fully specified. There may be
263         an alignment but no size. In that case we take the size from the
264         contents of the entry.
265
266         If an entry has no hard-coded offset, it will be placed at @offset.
267
268         Once this function is complete, both the offset and size of the
269         entry will be know.
270
271         Args:
272             Current section offset pointer
273
274         Returns:
275             New section offset pointer (after this entry)
276         """
277         if self.offset is None:
278             if self.offset_unset:
279                 self.Raise('No offset set with offset-unset: should another '
280                            'entry provide this correct offset?')
281             self.offset = tools.Align(offset, self.align)
282         needed = self.pad_before + self.contents_size + self.pad_after
283         needed = tools.Align(needed, self.align_size)
284         size = self.size
285         if not size:
286             size = needed
287         new_offset = self.offset + size
288         aligned_offset = tools.Align(new_offset, self.align_end)
289         if aligned_offset != new_offset:
290             size = aligned_offset - self.offset
291             new_offset = aligned_offset
292
293         if not self.size:
294             self.size = size
295
296         if self.size < needed:
297             self.Raise("Entry contents size is %#x (%d) but entry size is "
298                        "%#x (%d)" % (needed, needed, self.size, self.size))
299         # Check that the alignment is correct. It could be wrong if the
300         # and offset or size values were provided (i.e. not calculated), but
301         # conflict with the provided alignment values
302         if self.size != tools.Align(self.size, self.align_size):
303             self.Raise("Size %#x (%d) does not match align-size %#x (%d)" %
304                   (self.size, self.size, self.align_size, self.align_size))
305         if self.offset != tools.Align(self.offset, self.align):
306             self.Raise("Offset %#x (%d) does not match align %#x (%d)" %
307                   (self.offset, self.offset, self.align, self.align))
308
309         return new_offset
310
311     def Raise(self, msg):
312         """Convenience function to raise an error referencing a node"""
313         raise ValueError("Node '%s': %s" % (self._node.path, msg))
314
315     def GetEntryArgsOrProps(self, props, required=False):
316         """Return the values of a set of properties
317
318         Args:
319             props: List of EntryArg objects
320
321         Raises:
322             ValueError if a property is not found
323         """
324         values = []
325         missing = []
326         for prop in props:
327             python_prop = prop.name.replace('-', '_')
328             if hasattr(self, python_prop):
329                 value = getattr(self, python_prop)
330             else:
331                 value = None
332             if value is None:
333                 value = self.GetArg(prop.name, prop.datatype)
334             if value is None and required:
335                 missing.append(prop.name)
336             values.append(value)
337         if missing:
338             self.Raise('Missing required properties/entry args: %s' %
339                        (', '.join(missing)))
340         return values
341
342     def GetPath(self):
343         """Get the path of a node
344
345         Returns:
346             Full path of the node for this entry
347         """
348         return self._node.path
349
350     def GetData(self):
351         return self.data
352
353     def GetOffsets(self):
354         return {}
355
356     def SetOffsetSize(self, pos, size):
357         self.offset = pos
358         self.size = size
359
360     def SetImagePos(self, image_pos):
361         """Set the position in the image
362
363         Args:
364             image_pos: Position of this entry in the image
365         """
366         self.image_pos = image_pos + self.offset
367
368     def ProcessContents(self):
369         pass
370
371     def WriteSymbols(self, section):
372         """Write symbol values into binary files for access at run time
373
374         Args:
375           section: Section containing the entry
376         """
377         pass
378
379     def CheckOffset(self):
380         """Check that the entry offsets are correct
381
382         This is used for entries which have extra offset requirements (other
383         than having to be fully inside their section). Sub-classes can implement
384         this function and raise if there is a problem.
385         """
386         pass
387
388     @staticmethod
389     def WriteMapLine(fd, indent, name, offset, size, image_pos):
390         print('%08x  %s%08x  %08x  %s' % (image_pos, ' ' * indent, offset,
391                                           size, name), file=fd)
392
393     def WriteMap(self, fd, indent):
394         """Write a map of the entry to a .map file
395
396         Args:
397             fd: File to write the map to
398             indent: Curent indent level of map (0=none, 1=one level, etc.)
399         """
400         self.WriteMapLine(fd, indent, self.name, self.offset, self.size,
401                           self.image_pos)
402
403     def GetEntries(self):
404         """Return a list of entries contained by this entry
405
406         Returns:
407             List of entries, or None if none. A normal entry has no entries
408                 within it so will return None
409         """
410         return None
411
412     def GetArg(self, name, datatype=str):
413         """Get the value of an entry argument or device-tree-node property
414
415         Some node properties can be provided as arguments to binman. First check
416         the entry arguments, and fall back to the device tree if not found
417
418         Args:
419             name: Argument name
420             datatype: Data type (str or int)
421
422         Returns:
423             Value of argument as a string or int, or None if no value
424
425         Raises:
426             ValueError if the argument cannot be converted to in
427         """
428         value = state.GetEntryArg(name)
429         if value is not None:
430             if datatype == int:
431                 try:
432                     value = int(value)
433                 except ValueError:
434                     self.Raise("Cannot convert entry arg '%s' (value '%s') to integer" %
435                                (name, value))
436             elif datatype == str:
437                 pass
438             else:
439                 raise ValueError("GetArg() internal error: Unknown data type '%s'" %
440                                  datatype)
441         else:
442             value = fdt_util.GetDatatype(self._node, name, datatype)
443         return value
444
445     @staticmethod
446     def WriteDocs(modules, test_missing=None):
447         """Write out documentation about the various entry types to stdout
448
449         Args:
450             modules: List of modules to include
451             test_missing: Used for testing. This is a module to report
452                 as missing
453         """
454         print('''Binman Entry Documentation
455 ===========================
456
457 This file describes the entry types supported by binman. These entry types can
458 be placed in an image one by one to build up a final firmware image. It is
459 fairly easy to create new entry types. Just add a new file to the 'etype'
460 directory. You can use the existing entries as examples.
461
462 Note that some entries are subclasses of others, using and extending their
463 features to produce new behaviours.
464
465
466 ''')
467         modules = sorted(modules)
468
469         # Don't show the test entry
470         if '_testing' in modules:
471             modules.remove('_testing')
472         missing = []
473         for name in modules:
474             module = Entry.Lookup(name, name, name)
475             docs = getattr(module, '__doc__')
476             if test_missing == name:
477                 docs = None
478             if docs:
479                 lines = docs.splitlines()
480                 first_line = lines[0]
481                 rest = [line[4:] for line in lines[1:]]
482                 hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line)
483                 print(hdr)
484                 print('-' * len(hdr))
485                 print('\n'.join(rest))
486                 print()
487                 print()
488             else:
489                 missing.append(name)
490
491         if missing:
492             raise ValueError('Documentation is missing for modules: %s' %
493                              ', '.join(missing))
494
495     def GetUniqueName(self):
496         """Get a unique name for a node
497
498         Returns:
499             String containing a unique name for a node, consisting of the name
500             of all ancestors (starting from within the 'binman' node) separated
501             by a dot ('.'). This can be useful for generating unique filesnames
502             in the output directory.
503         """
504         name = self.name
505         node = self._node
506         while node.parent:
507             node = node.parent
508             if node.name == 'binman':
509                 break
510             name = '%s.%s' % (node.name, name)
511         return name
512
513     def ExpandToLimit(self, limit):
514         """Expand an entry so that it ends at the given offset limit"""
515         if self.offset + self.size < limit:
516             self.size = limit - self.offset
517             # Request the contents again, since changing the size requires that
518             # the data grows. This should not fail, but check it to be sure.
519             if not self.ObtainContents():
520                 self.Raise('Cannot obtain contents when expanding entry')