xref: /openbmc/u-boot/tools/binman/entry.py (revision 725e09b8)
1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2016 Google, Inc
3#
4# Base class for all entries
5#
6
7from __future__ import print_function
8
9from 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
14try:
15    import importlib
16    have_importlib = True
17except:
18    have_importlib = False
19
20import fdt_util
21import control
22import os
23import sys
24import tools
25
26modules = {}
27
28our_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.
33EntryArg = namedtuple('EntryArg', ['name', 'datatype'])
34
35
36class 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
422This file describes the entry types supported by binman. These entry types can
423be placed in an image one by one to build up a final firmware image. It is
424fairly easy to create new entry types. Just add a new file to the 'etype'
425directory. You can use the existing entries as examples.
426
427Note that some entries are subclasses of others, using and extending their
428features 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))
459