fd7223477eb02bbb142b8f50ff75245bf9573062
[oweals/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         err = state.CheckAddHashProp(self._node)
193         if err:
194             self.Raise(err)
195
196     def SetCalculatedProperties(self):
197         """Set the value of device-tree properties calculated by binman"""
198         state.SetInt(self._node, 'offset', self.offset)
199         state.SetInt(self._node, 'size', self.size)
200         state.SetInt(self._node, 'image-pos', self.image_pos)
201         state.CheckSetHashValue(self._node, self.GetData)
202
203     def ProcessFdt(self, fdt):
204         """Allow entries to adjust the device tree
205
206         Some entries need to adjust the device tree for their purposes. This
207         may involve adding or deleting properties.
208
209         Returns:
210             True if processing is complete
211             False if processing could not be completed due to a dependency.
212                 This will cause the entry to be retried after others have been
213                 called
214         """
215         return True
216
217     def SetPrefix(self, prefix):
218         """Set the name prefix for a node
219
220         Args:
221             prefix: Prefix to set, or '' to not use a prefix
222         """
223         if prefix:
224             self.name = prefix + self.name
225
226     def SetContents(self, data):
227         """Set the contents of an entry
228
229         This sets both the data and content_size properties
230
231         Args:
232             data: Data to set to the contents (string)
233         """
234         self.data = data
235         self.contents_size = len(self.data)
236
237     def ProcessContentsUpdate(self, data):
238         """Update the contens of an entry, after the size is fixed
239
240         This checks that the new data is the same size as the old.
241
242         Args:
243             data: Data to set to the contents (string)
244
245         Raises:
246             ValueError if the new data size is not the same as the old
247         """
248         if len(data) != self.contents_size:
249             self.Raise('Cannot update entry size from %d to %d' %
250                        (len(data), self.contents_size))
251         self.SetContents(data)
252
253     def ObtainContents(self):
254         """Figure out the contents of an entry.
255
256         Returns:
257             True if the contents were found, False if another call is needed
258             after the other entries are processed.
259         """
260         # No contents by default: subclasses can implement this
261         return True
262
263     def Pack(self, offset):
264         """Figure out how to pack the entry into the section
265
266         Most of the time the entries are not fully specified. There may be
267         an alignment but no size. In that case we take the size from the
268         contents of the entry.
269
270         If an entry has no hard-coded offset, it will be placed at @offset.
271
272         Once this function is complete, both the offset and size of the
273         entry will be know.
274
275         Args:
276             Current section offset pointer
277
278         Returns:
279             New section offset pointer (after this entry)
280         """
281         if self.offset is None:
282             if self.offset_unset:
283                 self.Raise('No offset set with offset-unset: should another '
284                            'entry provide this correct offset?')
285             self.offset = tools.Align(offset, self.align)
286         needed = self.pad_before + self.contents_size + self.pad_after
287         needed = tools.Align(needed, self.align_size)
288         size = self.size
289         if not size:
290             size = needed
291         new_offset = self.offset + size
292         aligned_offset = tools.Align(new_offset, self.align_end)
293         if aligned_offset != new_offset:
294             size = aligned_offset - self.offset
295             new_offset = aligned_offset
296
297         if not self.size:
298             self.size = size
299
300         if self.size < needed:
301             self.Raise("Entry contents size is %#x (%d) but entry size is "
302                        "%#x (%d)" % (needed, needed, self.size, self.size))
303         # Check that the alignment is correct. It could be wrong if the
304         # and offset or size values were provided (i.e. not calculated), but
305         # conflict with the provided alignment values
306         if self.size != tools.Align(self.size, self.align_size):
307             self.Raise("Size %#x (%d) does not match align-size %#x (%d)" %
308                   (self.size, self.size, self.align_size, self.align_size))
309         if self.offset != tools.Align(self.offset, self.align):
310             self.Raise("Offset %#x (%d) does not match align %#x (%d)" %
311                   (self.offset, self.offset, self.align, self.align))
312
313         return new_offset
314
315     def Raise(self, msg):
316         """Convenience function to raise an error referencing a node"""
317         raise ValueError("Node '%s': %s" % (self._node.path, msg))
318
319     def GetEntryArgsOrProps(self, props, required=False):
320         """Return the values of a set of properties
321
322         Args:
323             props: List of EntryArg objects
324
325         Raises:
326             ValueError if a property is not found
327         """
328         values = []
329         missing = []
330         for prop in props:
331             python_prop = prop.name.replace('-', '_')
332             if hasattr(self, python_prop):
333                 value = getattr(self, python_prop)
334             else:
335                 value = None
336             if value is None:
337                 value = self.GetArg(prop.name, prop.datatype)
338             if value is None and required:
339                 missing.append(prop.name)
340             values.append(value)
341         if missing:
342             self.Raise('Missing required properties/entry args: %s' %
343                        (', '.join(missing)))
344         return values
345
346     def GetPath(self):
347         """Get the path of a node
348
349         Returns:
350             Full path of the node for this entry
351         """
352         return self._node.path
353
354     def GetData(self):
355         return self.data
356
357     def GetOffsets(self):
358         return {}
359
360     def SetOffsetSize(self, pos, size):
361         self.offset = pos
362         self.size = size
363
364     def SetImagePos(self, image_pos):
365         """Set the position in the image
366
367         Args:
368             image_pos: Position of this entry in the image
369         """
370         self.image_pos = image_pos + self.offset
371
372     def ProcessContents(self):
373         pass
374
375     def WriteSymbols(self, section):
376         """Write symbol values into binary files for access at run time
377
378         Args:
379           section: Section containing the entry
380         """
381         pass
382
383     def CheckOffset(self):
384         """Check that the entry offsets are correct
385
386         This is used for entries which have extra offset requirements (other
387         than having to be fully inside their section). Sub-classes can implement
388         this function and raise if there is a problem.
389         """
390         pass
391
392     @staticmethod
393     def WriteMapLine(fd, indent, name, offset, size, image_pos):
394         print('%08x  %s%08x  %08x  %s' % (image_pos, ' ' * indent, offset,
395                                           size, name), file=fd)
396
397     def WriteMap(self, fd, indent):
398         """Write a map of the entry to a .map file
399
400         Args:
401             fd: File to write the map to
402             indent: Curent indent level of map (0=none, 1=one level, etc.)
403         """
404         self.WriteMapLine(fd, indent, self.name, self.offset, self.size,
405                           self.image_pos)
406
407     def GetEntries(self):
408         """Return a list of entries contained by this entry
409
410         Returns:
411             List of entries, or None if none. A normal entry has no entries
412                 within it so will return None
413         """
414         return None
415
416     def GetArg(self, name, datatype=str):
417         """Get the value of an entry argument or device-tree-node property
418
419         Some node properties can be provided as arguments to binman. First check
420         the entry arguments, and fall back to the device tree if not found
421
422         Args:
423             name: Argument name
424             datatype: Data type (str or int)
425
426         Returns:
427             Value of argument as a string or int, or None if no value
428
429         Raises:
430             ValueError if the argument cannot be converted to in
431         """
432         value = state.GetEntryArg(name)
433         if value is not None:
434             if datatype == int:
435                 try:
436                     value = int(value)
437                 except ValueError:
438                     self.Raise("Cannot convert entry arg '%s' (value '%s') to integer" %
439                                (name, value))
440             elif datatype == str:
441                 pass
442             else:
443                 raise ValueError("GetArg() internal error: Unknown data type '%s'" %
444                                  datatype)
445         else:
446             value = fdt_util.GetDatatype(self._node, name, datatype)
447         return value
448
449     @staticmethod
450     def WriteDocs(modules, test_missing=None):
451         """Write out documentation about the various entry types to stdout
452
453         Args:
454             modules: List of modules to include
455             test_missing: Used for testing. This is a module to report
456                 as missing
457         """
458         print('''Binman Entry Documentation
459 ===========================
460
461 This file describes the entry types supported by binman. These entry types can
462 be placed in an image one by one to build up a final firmware image. It is
463 fairly easy to create new entry types. Just add a new file to the 'etype'
464 directory. You can use the existing entries as examples.
465
466 Note that some entries are subclasses of others, using and extending their
467 features to produce new behaviours.
468
469
470 ''')
471         modules = sorted(modules)
472
473         # Don't show the test entry
474         if '_testing' in modules:
475             modules.remove('_testing')
476         missing = []
477         for name in modules:
478             module = Entry.Lookup(name, name, name)
479             docs = getattr(module, '__doc__')
480             if test_missing == name:
481                 docs = None
482             if docs:
483                 lines = docs.splitlines()
484                 first_line = lines[0]
485                 rest = [line[4:] for line in lines[1:]]
486                 hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line)
487                 print(hdr)
488                 print('-' * len(hdr))
489                 print('\n'.join(rest))
490                 print()
491                 print()
492             else:
493                 missing.append(name)
494
495         if missing:
496             raise ValueError('Documentation is missing for modules: %s' %
497                              ', '.join(missing))
498
499     def GetUniqueName(self):
500         """Get a unique name for a node
501
502         Returns:
503             String containing a unique name for a node, consisting of the name
504             of all ancestors (starting from within the 'binman' node) separated
505             by a dot ('.'). This can be useful for generating unique filesnames
506             in the output directory.
507         """
508         name = self.name
509         node = self._node
510         while node.parent:
511             node = node.parent
512             if node.name == 'binman':
513                 break
514             name = '%s.%s' % (node.name, name)
515         return name
516
517     def ExpandToLimit(self, limit):
518         """Expand an entry so that it ends at the given offset limit"""
519         if self.offset + self.size < limit:
520             self.size = limit - self.offset
521             # Request the contents again, since changing the size requires that
522             # the data grows. This should not fail, but check it to be sure.
523             if not self.ObtainContents():
524                 self.Raise('Cannot obtain contents when expanding entry')