1# SPDX-License-Identifier: GPL-2.0+
2# Copyright 2018 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Holds and modifies the state information held by binman
6#
7
8from collections import defaultdict
9import hashlib
10import re
11import time
12import threading
13
14from dtoc import fdt
15import os
16from u_boot_pylib import tools
17from u_boot_pylib import tout
18
19OUR_PATH = os.path.dirname(os.path.realpath(__file__))
20
21# Map an dtb etype to its expected filename
22DTB_TYPE_FNAME = {
23    'u-boot-spl-dtb': 'spl/u-boot-spl.dtb',
24    'u-boot-tpl-dtb': 'tpl/u-boot-tpl.dtb',
25    'u-boot-vpl-dtb': 'vpl/u-boot-vpl.dtb',
26    }
27
28# Records the device-tree files known to binman, keyed by entry type (e.g.
29# 'u-boot-spl-dtb'). These are the output FDT files, which can be updated by
30# binman. They have been copied to <xxx>.out files.
31#
32#   key: entry type (e.g. 'u-boot-dtb)
33#   value: tuple:
34#       Fdt object
35#       Filename
36output_fdt_info = {}
37
38# Prefix to add to an fdtmap path to turn it into a path to the /binman node
39fdt_path_prefix = ''
40
41# Arguments passed to binman to provide arguments to entries
42entry_args = {}
43
44# True to use fake device-tree files for testing (see U_BOOT_DTB_DATA in
45# ftest.py)
46use_fake_dtb = False
47
48# The DTB which contains the full image information
49main_dtb = None
50
51# Allow entries to expand after they have been packed. This is detected and
52# forces a re-pack. If not allowed, any attempted expansion causes an error in
53# Entry.ProcessContentsUpdate()
54allow_entry_expansion = True
55
56# Don't allow entries to contract after they have been packed. Instead just
57# leave some wasted space. If allowed, this is detected and forces a re-pack,
58# but may result in entries that oscillate in size, thus causing a pack error.
59# An example is a compressed device tree where the original offset values
60# result in a larger compressed size than the new ones, but then after updating
61# to the new ones, the compressed size increases, etc.
62allow_entry_contraction = False
63
64# Number of threads to use for binman (None means machine-dependent)
65num_threads = None
66
67
68class Timing:
69    """Holds information about an operation that is being timed
70
71    Properties:
72        name: Operation name (only one of each name is stored)
73        start: Start time of operation in seconds (None if not start)
74        accum:: Amount of time spent on this operation so far, in seconds
75    """
76    def __init__(self, name):
77        self.name = name
78        self.start = None # cause an error if TimingStart() is not called
79        self.accum = 0.0
80
81
82# Holds timing info for each name:
83#    key: name of Timing info (Timing.name)
84#    value: Timing object
85timing_info = {}
86
87
88def GetFdtForEtype(etype):
89    """Get the Fdt object for a particular device-tree entry
90
91    Binman keeps track of at least one device-tree file called u-boot.dtb but
92    can also have others (e.g. for SPL). This function looks up the given
93    entry and returns the associated Fdt object.
94
95    Args:
96        etype: Entry type of device tree (e.g. 'u-boot-dtb')
97
98    Returns:
99        Fdt object associated with the entry type
100    """
101    value = output_fdt_info.get(etype);
102    if not value:
103        return None
104    return value[0]
105
106def GetFdtPath(etype):
107    """Get the full pathname of a particular Fdt object
108
109    Similar to GetFdtForEtype() but returns the pathname associated with the
110    Fdt.
111
112    Args:
113        etype: Entry type of device tree (e.g. 'u-boot-dtb')
114
115    Returns:
116        Full path name to the associated Fdt
117    """
118    return output_fdt_info[etype][0]._fname
119
120def GetFdtContents(etype='u-boot-dtb'):
121    """Looks up the FDT pathname and contents
122
123    This is used to obtain the Fdt pathname and contents when needed by an
124    entry. It supports a 'fake' dtb, allowing tests to substitute test data for
125    the real dtb.
126
127    Args:
128        etype: Entry type to look up (e.g. 'u-boot.dtb').
129
130    Returns:
131        tuple:
132            pathname to Fdt
133            Fdt data (as bytes)
134    """
135    if etype not in output_fdt_info:
136        return None, None
137    if not use_fake_dtb:
138        pathname = GetFdtPath(etype)
139        data = GetFdtForEtype(etype).GetContents()
140    else:
141        fname = output_fdt_info[etype][1]
142        pathname = tools.get_input_filename(fname)
143        data = tools.read_file(pathname)
144    return pathname, data
145
146def UpdateFdtContents(etype, data):
147    """Update the contents of a particular device tree
148
149    The device tree is updated and written back to its file. This affects what
150    is returned from future called to GetFdtContents(), etc.
151
152    Args:
153        etype: Entry type (e.g. 'u-boot-dtb')
154        data: Data to replace the DTB with
155    """
156    dtb, fname = output_fdt_info[etype]
157    dtb_fname = dtb.GetFilename()
158    tools.write_file(dtb_fname, data)
159    dtb = fdt.FdtScan(dtb_fname)
160    output_fdt_info[etype] = [dtb, fname]
161
162def SetEntryArgs(args):
163    """Set the value of the entry args
164
165    This sets up the entry_args dict which is used to supply entry arguments to
166    entries.
167
168    Args:
169        args: List of entry arguments, each in the format "name=value"
170    """
171    global entry_args
172
173    entry_args = {}
174    tout.debug('Processing entry args:')
175    if args:
176        for arg in args:
177            m = re.match('([^=]*)=(.*)', arg)
178            if not m:
179                raise ValueError("Invalid entry arguemnt '%s'" % arg)
180            name, value = m.groups()
181            tout.debug('   %20s = %s' % (name, value))
182            entry_args[name] = value
183    tout.debug('Processing entry args done')
184
185def GetEntryArg(name):
186    """Get the value of an entry argument
187
188    Args:
189        name: Name of argument to retrieve
190
191    Returns:
192        String value of argument
193    """
194    return entry_args.get(name)
195
196def GetEntryArgBool(name):
197    """Get the value of an entry argument as a boolean
198
199    Args:
200        name: Name of argument to retrieve
201
202    Returns:
203        False if the entry argument is consider False (empty, '0' or 'n'), else
204            True
205    """
206    val = GetEntryArg(name)
207    return val and val not in ['n', '0']
208
209def Prepare(images, dtb):
210    """Get device tree files ready for use
211
212    This sets up a set of device tree files that can be retrieved by
213    GetAllFdts(). This includes U-Boot proper and any SPL device trees.
214
215    Args:
216        images: List of images being used
217        dtb: Main dtb
218    """
219    global output_fdt_info, main_dtb, fdt_path_prefix
220    # Import these here in case libfdt.py is not available, in which case
221    # the above help option still works.
222    from dtoc import fdt
223    from dtoc import fdt_util
224
225    # If we are updating the DTBs we need to put these updated versions
226    # where Entry_blob_dtb can find them. We can ignore 'u-boot.dtb'
227    # since it is assumed to be the one passed in with options.dt, and
228    # was handled just above.
229    main_dtb = dtb
230    output_fdt_info.clear()
231    fdt_path_prefix = ''
232    output_fdt_info['u-boot-dtb'] = [dtb, 'u-boot.dtb']
233    if use_fake_dtb:
234        for etype, fname in DTB_TYPE_FNAME.items():
235            output_fdt_info[etype] = [dtb, fname]
236    else:
237        fdt_set = {}
238        for etype, fname in DTB_TYPE_FNAME.items():
239            infile = tools.get_input_filename(fname, allow_missing=True)
240            if infile and os.path.exists(infile):
241                fname_dtb = fdt_util.EnsureCompiled(infile)
242                out_fname = tools.get_output_filename('%s.out' %
243                        os.path.split(fname)[1])
244                tools.write_file(out_fname, tools.read_file(fname_dtb))
245                other_dtb = fdt.FdtScan(out_fname)
246                output_fdt_info[etype] = [other_dtb, out_fname]
247
248
249def PrepareFromLoadedData(image):
250    """Get device tree files ready for use with a loaded image
251
252    Loaded images are different from images that are being created by binman,
253    since there is generally already an fdtmap and we read the description from
254    that. This provides the position and size of every entry in the image with
255    no calculation required.
256
257    This function uses the same output_fdt_info[] as Prepare(). It finds the
258    device tree files, adds a reference to the fdtmap and sets the FDT path
259    prefix to translate from the fdtmap (where the root node is the image node)
260    to the normal device tree (where the image node is under a /binman node).
261
262    Args:
263        images: List of images being used
264    """
265    global output_fdt_info, main_dtb, fdt_path_prefix
266
267    tout.info('Preparing device trees')
268    output_fdt_info.clear()
269    fdt_path_prefix = ''
270    output_fdt_info['fdtmap'] = [image.fdtmap_dtb, 'u-boot.dtb']
271    main_dtb = None
272    tout.info("   Found device tree type 'fdtmap' '%s'" % image.fdtmap_dtb.name)
273    for etype, value in image.GetFdts().items():
274        entry, fname = value
275        out_fname = tools.get_output_filename('%s.dtb' % entry.etype)
276        tout.info("   Found device tree type '%s' at '%s' path '%s'" %
277                  (etype, out_fname, entry.GetPath()))
278        entry._filename = entry.GetDefaultFilename()
279        data = entry.ReadData()
280
281        tools.write_file(out_fname, data)
282        dtb = fdt.Fdt(out_fname)
283        dtb.Scan()
284        image_node = dtb.GetNode('/binman')
285        if 'multiple-images' in image_node.props:
286            image_node = dtb.GetNode('/binman/%s' % image.image_node)
287        fdt_path_prefix = image_node.path
288        output_fdt_info[etype] = [dtb, None]
289    tout.info("   FDT path prefix '%s'" % fdt_path_prefix)
290
291
292def GetAllFdts():
293    """Yield all device tree files being used by binman
294
295    Yields:
296        Device trees being used (U-Boot proper, SPL, TPL, VPL)
297    """
298    if main_dtb:
299        yield main_dtb
300    for etype in output_fdt_info:
301        dtb = output_fdt_info[etype][0]
302        if dtb != main_dtb:
303            yield dtb
304
305def GetUpdateNodes(node, for_repack=False):
306    """Yield all the nodes that need to be updated in all device trees
307
308    The property referenced by this node is added to any device trees which
309    have the given node. Due to removable of unwanted nodes, SPL and TPL may
310    not have this node.
311
312    Args:
313        node: Node object in the main device tree to look up
314        for_repack: True if we want only nodes which need 'repack' properties
315            added to them (e.g. 'orig-offset'), False to return all nodes. We
316            don't add repack properties to SPL/TPL device trees.
317
318    Yields:
319        Node objects in each device tree that is in use (U-Boot proper, which
320            is node, SPL and TPL)
321    """
322    yield node
323    for entry_type, (dtb, fname) in output_fdt_info.items():
324        if dtb != node.GetFdt():
325            if for_repack and entry_type != 'u-boot-dtb':
326                continue
327            other_node = dtb.GetNode(fdt_path_prefix + node.path)
328            if other_node:
329                yield other_node
330
331def AddZeroProp(node, prop, for_repack=False):
332    """Add a new property to affected device trees with an integer value of 0.
333
334    Args:
335        prop_name: Name of property
336        for_repack: True is this property is only needed for repacking
337    """
338    for n in GetUpdateNodes(node, for_repack):
339        n.AddZeroProp(prop)
340
341def AddSubnode(node, name):
342    """Add a new subnode to a node in affected device trees
343
344    Args:
345        node: Node to add to
346        name: name of node to add
347
348    Returns:
349        New subnode that was created in main tree
350    """
351    first = None
352    for n in GetUpdateNodes(node):
353        subnode = n.AddSubnode(name)
354        if not first:
355            first = subnode
356    return first
357
358def AddString(node, prop, value):
359    """Add a new string property to affected device trees
360
361    Args:
362        prop_name: Name of property
363        value: String value (which will be \0-terminated in the DT)
364    """
365    for n in GetUpdateNodes(node):
366        n.AddString(prop, value)
367
368def AddInt(node, prop, value):
369    """Add a new string property to affected device trees
370
371    Args:
372        prop_name: Name of property
373        val: Integer value of property
374    """
375    for n in GetUpdateNodes(node):
376        n.AddInt(prop, value)
377
378def SetInt(node, prop, value, for_repack=False):
379    """Update an integer property in affected device trees with an integer value
380
381    This is not allowed to change the size of the FDT.
382
383    Args:
384        prop_name: Name of property
385        for_repack: True is this property is only needed for repacking
386    """
387    for n in GetUpdateNodes(node, for_repack):
388        tout.debug("File %s: Update node '%s' prop '%s' to %#x" %
389                   (n.GetFdt().name, n.path, prop, value))
390        n.SetInt(prop, value)
391
392def CheckAddHashProp(node):
393    hash_node = node.FindNode('hash')
394    if hash_node:
395        algo = hash_node.props.get('algo')
396        if not algo:
397            return "Missing 'algo' property for hash node"
398        if algo.value == 'sha256':
399            size = 32
400        else:
401            return "Unknown hash algorithm '%s'" % algo.value
402        for n in GetUpdateNodes(hash_node):
403            n.AddEmptyProp('value', size)
404
405def CheckSetHashValue(node, get_data_func):
406    hash_node = node.FindNode('hash')
407    if hash_node:
408        algo = hash_node.props.get('algo').value
409        data = None
410        if algo == 'sha256':
411            m = hashlib.sha256()
412            m.update(get_data_func())
413            data = m.digest()
414        assert data
415        for n in GetUpdateNodes(hash_node):
416            n.SetData('value', data)
417
418def SetAllowEntryExpansion(allow):
419    """Set whether post-pack expansion of entries is allowed
420
421    Args:
422       allow: True to allow expansion, False to raise an exception
423    """
424    global allow_entry_expansion
425
426    allow_entry_expansion = allow
427
428def AllowEntryExpansion():
429    """Check whether post-pack expansion of entries is allowed
430
431    Returns:
432        True if expansion should be allowed, False if an exception should be
433            raised
434    """
435    return allow_entry_expansion
436
437def SetAllowEntryContraction(allow):
438    """Set whether post-pack contraction of entries is allowed
439
440    Args:
441       allow: True to allow contraction, False to raise an exception
442    """
443    global allow_entry_contraction
444
445    allow_entry_contraction = allow
446
447def AllowEntryContraction():
448    """Check whether post-pack contraction of entries is allowed
449
450    Returns:
451        True if contraction should be allowed, False if an exception should be
452            raised
453    """
454    return allow_entry_contraction
455
456def SetThreads(threads):
457    """Set the number of threads to use when building sections
458
459    Args:
460        threads: Number of threads to use (None for default, 0 for
461            single-threaded)
462    """
463    global num_threads
464
465    num_threads = threads
466
467def GetThreads():
468    """Get the number of threads to use when building sections
469
470    Returns:
471        Number of threads to use (None for default, 0 for single-threaded)
472    """
473    return num_threads
474
475def GetTiming(name):
476    """Get the timing info for a particular operation
477
478    The object is created if it does not already exist.
479
480    Args:
481        name: Operation name to get
482
483    Returns:
484        Timing object for the current thread
485    """
486    threaded_name = '%s:%d' % (name, threading.get_ident())
487    timing = timing_info.get(threaded_name)
488    if not timing:
489        timing = Timing(threaded_name)
490        timing_info[threaded_name] = timing
491    return timing
492
493def TimingStart(name):
494    """Start the timer for an operation
495
496    Args:
497        name: Operation name to start
498    """
499    timing = GetTiming(name)
500    timing.start = time.monotonic()
501
502def TimingAccum(name):
503    """Stop and accumlate the time for an operation
504
505    This measures the time since the last TimingStart() and adds that to the
506    accumulated time.
507
508    Args:
509        name: Operation name to start
510    """
511    timing = GetTiming(name)
512    timing.accum += time.monotonic() - timing.start
513
514def TimingShow():
515    """Show all timing information"""
516    duration = defaultdict(float)
517    for threaded_name, timing in timing_info.items():
518        name = threaded_name.split(':')[0]
519        duration[name] += timing.accum
520
521    for name, seconds in duration.items():
522        print('%10s: %10.1fms' % (name, seconds * 1000))
523
524def GetVersion(path=OUR_PATH):
525    """Get the version string for binman
526
527    Args:
528        path: Path to 'version' file
529
530    Returns:
531        str: String version, e.g. 'v2021.10'
532    """
533    version_fname = os.path.join(path, 'version')
534    if os.path.exists(version_fname):
535        version = tools.read_file(version_fname, binary=False)
536    else:
537        version = '(unreleased)'
538    return version
539