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