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