1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2016 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Creates binary images from input files controlled by a description
6#
7
8from collections import OrderedDict
9import glob
10import os
11import pkg_resources
12import re
13
14import sys
15from patman import tools
16
17from binman import cbfs_util
18from binman import elf
19from patman import command
20from patman import tout
21
22# List of images we plan to create
23# Make this global so that it can be referenced from tests
24images = OrderedDict()
25
26# Help text for each type of missing blob, dict:
27#    key: Value of the entry's 'missing-msg' or entry name
28#    value: Text for the help
29missing_blob_help = {}
30
31def _ReadImageDesc(binman_node):
32    """Read the image descriptions from the /binman node
33
34    This normally produces a single Image object called 'image'. But if
35    multiple images are present, they will all be returned.
36
37    Args:
38        binman_node: Node object of the /binman node
39    Returns:
40        OrderedDict of Image objects, each of which describes an image
41    """
42    images = OrderedDict()
43    if 'multiple-images' in binman_node.props:
44        for node in binman_node.subnodes:
45            images[node.name] = Image(node.name, node)
46    else:
47        images['image'] = Image('image', binman_node)
48    return images
49
50def _FindBinmanNode(dtb):
51    """Find the 'binman' node in the device tree
52
53    Args:
54        dtb: Fdt object to scan
55    Returns:
56        Node object of /binman node, or None if not found
57    """
58    for node in dtb.GetRoot().subnodes:
59        if node.name == 'binman':
60            return node
61    return None
62
63def _ReadMissingBlobHelp():
64    """Read the missing-blob-help file
65
66    This file containins help messages explaining what to do when external blobs
67    are missing.
68
69    Returns:
70        Dict:
71            key: Message tag (str)
72            value: Message text (str)
73    """
74
75    def _FinishTag(tag, msg, result):
76        if tag:
77            result[tag] = msg.rstrip()
78            tag = None
79            msg = ''
80        return tag, msg
81
82    my_data = pkg_resources.resource_string(__name__, 'missing-blob-help')
83    re_tag = re.compile('^([-a-z0-9]+):$')
84    result = {}
85    tag = None
86    msg = ''
87    for line in my_data.decode('utf-8').splitlines():
88        if not line.startswith('#'):
89            m_tag = re_tag.match(line)
90            if m_tag:
91                _, msg = _FinishTag(tag, msg, result)
92                tag = m_tag.group(1)
93            elif tag:
94                msg += line + '\n'
95    _FinishTag(tag, msg, result)
96    return result
97
98def _ShowBlobHelp(path, text):
99    tout.Warning('\n%s:' % path)
100    for line in text.splitlines():
101        tout.Warning('   %s' % line)
102
103def _ShowHelpForMissingBlobs(missing_list):
104    """Show help for each missing blob to help the user take action
105
106    Args:
107        missing_list: List of Entry objects to show help for
108    """
109    global missing_blob_help
110
111    if not missing_blob_help:
112        missing_blob_help = _ReadMissingBlobHelp()
113
114    for entry in missing_list:
115        tags = entry.GetHelpTags()
116
117        # Show the first match help message
118        for tag in tags:
119            if tag in missing_blob_help:
120                _ShowBlobHelp(entry._node.path, missing_blob_help[tag])
121                break
122
123def GetEntryModules(include_testing=True):
124    """Get a set of entry class implementations
125
126    Returns:
127        Set of paths to entry class filenames
128    """
129    glob_list = pkg_resources.resource_listdir(__name__, 'etype')
130    glob_list = [fname for fname in glob_list if fname.endswith('.py')]
131    return set([os.path.splitext(os.path.basename(item))[0]
132                for item in glob_list
133                if include_testing or '_testing' not in item])
134
135def WriteEntryDocs(modules, test_missing=None):
136    """Write out documentation for all entries
137
138    Args:
139        modules: List of Module objects to get docs for
140        test_missing: Used for testing only, to force an entry's documeentation
141            to show as missing even if it is present. Should be set to None in
142            normal use.
143    """
144    from binman.entry import Entry
145    Entry.WriteDocs(modules, test_missing)
146
147
148def ListEntries(image_fname, entry_paths):
149    """List the entries in an image
150
151    This decodes the supplied image and displays a table of entries from that
152    image, preceded by a header.
153
154    Args:
155        image_fname: Image filename to process
156        entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
157                                                     'section/u-boot'])
158    """
159    image = Image.FromFile(image_fname)
160
161    entries, lines, widths = image.GetListEntries(entry_paths)
162
163    num_columns = len(widths)
164    for linenum, line in enumerate(lines):
165        if linenum == 1:
166            # Print header line
167            print('-' * (sum(widths) + num_columns * 2))
168        out = ''
169        for i, item in enumerate(line):
170            width = -widths[i]
171            if item.startswith('>'):
172                width = -width
173                item = item[1:]
174            txt = '%*s  ' % (width, item)
175            out += txt
176        print(out.rstrip())
177
178
179def ReadEntry(image_fname, entry_path, decomp=True):
180    """Extract an entry from an image
181
182    This extracts the data from a particular entry in an image
183
184    Args:
185        image_fname: Image filename to process
186        entry_path: Path to entry to extract
187        decomp: True to return uncompressed data, if the data is compress
188            False to return the raw data
189
190    Returns:
191        data extracted from the entry
192    """
193    global Image
194    from binman.image import Image
195
196    image = Image.FromFile(image_fname)
197    entry = image.FindEntryPath(entry_path)
198    return entry.ReadData(decomp)
199
200
201def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
202                   decomp=True):
203    """Extract the data from one or more entries and write it to files
204
205    Args:
206        image_fname: Image filename to process
207        output_fname: Single output filename to use if extracting one file, None
208            otherwise
209        outdir: Output directory to use (for any number of files), else None
210        entry_paths: List of entry paths to extract
211        decomp: True to decompress the entry data
212
213    Returns:
214        List of EntryInfo records that were written
215    """
216    image = Image.FromFile(image_fname)
217
218    # Output an entry to a single file, as a special case
219    if output_fname:
220        if not entry_paths:
221            raise ValueError('Must specify an entry path to write with -f')
222        if len(entry_paths) != 1:
223            raise ValueError('Must specify exactly one entry path to write with -f')
224        entry = image.FindEntryPath(entry_paths[0])
225        data = entry.ReadData(decomp)
226        tools.WriteFile(output_fname, data)
227        tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
228        return
229
230    # Otherwise we will output to a path given by the entry path of each entry.
231    # This means that entries will appear in subdirectories if they are part of
232    # a sub-section.
233    einfos = image.GetListEntries(entry_paths)[0]
234    tout.Notice('%d entries match and will be written' % len(einfos))
235    for einfo in einfos:
236        entry = einfo.entry
237        data = entry.ReadData(decomp)
238        path = entry.GetPath()[1:]
239        fname = os.path.join(outdir, path)
240
241        # If this entry has children, create a directory for it and put its
242        # data in a file called 'root' in that directory
243        if entry.GetEntries():
244            if not os.path.exists(fname):
245                os.makedirs(fname)
246            fname = os.path.join(fname, 'root')
247        tout.Notice("Write entry '%s' size %x to '%s'" %
248                    (entry.GetPath(), len(data), fname))
249        tools.WriteFile(fname, data)
250    return einfos
251
252
253def BeforeReplace(image, allow_resize):
254    """Handle getting an image ready for replacing entries in it
255
256    Args:
257        image: Image to prepare
258    """
259    state.PrepareFromLoadedData(image)
260    image.LoadData()
261
262    # If repacking, drop the old offset/size values except for the original
263    # ones, so we are only left with the constraints.
264    if allow_resize:
265        image.ResetForPack()
266
267
268def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
269    """Handle replacing a single entry an an image
270
271    Args:
272        image: Image to update
273        entry: Entry to write
274        data: Data to replace with
275        do_compress: True to compress the data if needed, False if data is
276            already compressed so should be used as is
277        allow_resize: True to allow entries to change size (this does a re-pack
278            of the entries), False to raise an exception
279    """
280    if not entry.WriteData(data, do_compress):
281        if not image.allow_repack:
282            entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
283        if not allow_resize:
284            entry.Raise('Entry data size does not match, but resize is disabled')
285
286
287def AfterReplace(image, allow_resize, write_map):
288    """Handle write out an image after replacing entries in it
289
290    Args:
291        image: Image to write
292        allow_resize: True to allow entries to change size (this does a re-pack
293            of the entries), False to raise an exception
294        write_map: True to write a map file
295    """
296    tout.Info('Processing image')
297    ProcessImage(image, update_fdt=True, write_map=write_map,
298                 get_contents=False, allow_resize=allow_resize)
299
300
301def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
302                      write_map=False):
303    BeforeReplace(image, allow_resize)
304    tout.Info('Writing data to %s' % entry.GetPath())
305    ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
306    AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
307
308
309def WriteEntry(image_fname, entry_path, data, do_compress=True,
310               allow_resize=True, write_map=False):
311    """Replace an entry in an image
312
313    This replaces the data in a particular entry in an image. This size of the
314    new data must match the size of the old data unless allow_resize is True.
315
316    Args:
317        image_fname: Image filename to process
318        entry_path: Path to entry to extract
319        data: Data to replace with
320        do_compress: True to compress the data if needed, False if data is
321            already compressed so should be used as is
322        allow_resize: True to allow entries to change size (this does a re-pack
323            of the entries), False to raise an exception
324        write_map: True to write a map file
325
326    Returns:
327        Image object that was updated
328    """
329    tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
330    image = Image.FromFile(image_fname)
331    entry = image.FindEntryPath(entry_path)
332    WriteEntryToImage(image, entry, data, do_compress=do_compress,
333                      allow_resize=allow_resize, write_map=write_map)
334
335    return image
336
337
338def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
339                   do_compress=True, allow_resize=True, write_map=False):
340    """Replace the data from one or more entries from input files
341
342    Args:
343        image_fname: Image filename to process
344        input_fname: Single input ilename to use if replacing one file, None
345            otherwise
346        indir: Input directory to use (for any number of files), else None
347        entry_paths: List of entry paths to extract
348        do_compress: True if the input data is uncompressed and may need to be
349            compressed if the entry requires it, False if the data is already
350            compressed.
351        write_map: True to write a map file
352
353    Returns:
354        List of EntryInfo records that were written
355    """
356    image = Image.FromFile(image_fname)
357
358    # Replace an entry from a single file, as a special case
359    if input_fname:
360        if not entry_paths:
361            raise ValueError('Must specify an entry path to read with -f')
362        if len(entry_paths) != 1:
363            raise ValueError('Must specify exactly one entry path to write with -f')
364        entry = image.FindEntryPath(entry_paths[0])
365        data = tools.ReadFile(input_fname)
366        tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
367        WriteEntryToImage(image, entry, data, do_compress=do_compress,
368                          allow_resize=allow_resize, write_map=write_map)
369        return
370
371    # Otherwise we will input from a path given by the entry path of each entry.
372    # This means that files must appear in subdirectories if they are part of
373    # a sub-section.
374    einfos = image.GetListEntries(entry_paths)[0]
375    tout.Notice("Replacing %d matching entries in image '%s'" %
376                (len(einfos), image_fname))
377
378    BeforeReplace(image, allow_resize)
379
380    for einfo in einfos:
381        entry = einfo.entry
382        if entry.GetEntries():
383            tout.Info("Skipping section entry '%s'" % entry.GetPath())
384            continue
385
386        path = entry.GetPath()[1:]
387        fname = os.path.join(indir, path)
388
389        if os.path.exists(fname):
390            tout.Notice("Write entry '%s' from file '%s'" %
391                        (entry.GetPath(), fname))
392            data = tools.ReadFile(fname)
393            ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
394        else:
395            tout.Warning("Skipping entry '%s' from missing file '%s'" %
396                         (entry.GetPath(), fname))
397
398    AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
399    return image
400
401
402def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
403    """Prepare the images to be processed and select the device tree
404
405    This function:
406    - reads in the device tree
407    - finds and scans the binman node to create all entries
408    - selects which images to build
409    - Updates the device tress with placeholder properties for offset,
410        image-pos, etc.
411
412    Args:
413        dtb_fname: Filename of the device tree file to use (.dts or .dtb)
414        selected_images: List of images to output, or None for all
415        update_fdt: True to update the FDT wth entry offsets, etc.
416
417    Returns:
418        OrderedDict of images:
419            key: Image name (str)
420            value: Image object
421    """
422    # Import these here in case libfdt.py is not available, in which case
423    # the above help option still works.
424    from dtoc import fdt
425    from dtoc import fdt_util
426    global images
427
428    # Get the device tree ready by compiling it and copying the compiled
429    # output into a file in our output directly. Then scan it for use
430    # in binman.
431    dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
432    fname = tools.GetOutputFilename('u-boot.dtb.out')
433    tools.WriteFile(fname, tools.ReadFile(dtb_fname))
434    dtb = fdt.FdtScan(fname)
435
436    node = _FindBinmanNode(dtb)
437    if not node:
438        raise ValueError("Device tree '%s' does not have a 'binman' "
439                            "node" % dtb_fname)
440
441    images = _ReadImageDesc(node)
442
443    if select_images:
444        skip = []
445        new_images = OrderedDict()
446        for name, image in images.items():
447            if name in select_images:
448                new_images[name] = image
449            else:
450                skip.append(name)
451        images = new_images
452        tout.Notice('Skipping images: %s' % ', '.join(skip))
453
454    state.Prepare(images, dtb)
455
456    # Prepare the device tree by making sure that any missing
457    # properties are added (e.g. 'pos' and 'size'). The values of these
458    # may not be correct yet, but we add placeholders so that the
459    # size of the device tree is correct. Later, in
460    # SetCalculatedProperties() we will insert the correct values
461    # without changing the device-tree size, thus ensuring that our
462    # entry offsets remain the same.
463    for image in images.values():
464        image.ExpandEntries()
465        if update_fdt:
466            image.AddMissingProperties(True)
467        image.ProcessFdt(dtb)
468
469    for dtb_item in state.GetAllFdts():
470        dtb_item.Sync(auto_resize=True)
471        dtb_item.Pack()
472        dtb_item.Flush()
473    return images
474
475
476def ProcessImage(image, update_fdt, write_map, get_contents=True,
477                 allow_resize=True, allow_missing=False):
478    """Perform all steps for this image, including checking and # writing it.
479
480    This means that errors found with a later image will be reported after
481    earlier images are already completed and written, but that does not seem
482    important.
483
484    Args:
485        image: Image to process
486        update_fdt: True to update the FDT wth entry offsets, etc.
487        write_map: True to write a map file
488        get_contents: True to get the image contents from files, etc., False if
489            the contents is already present
490        allow_resize: True to allow entries to change size (this does a re-pack
491            of the entries), False to raise an exception
492        allow_missing: Allow blob_ext objects to be missing
493
494    Returns:
495        True if one or more external blobs are missing, False if all are present
496    """
497    if get_contents:
498        image.SetAllowMissing(allow_missing)
499        image.GetEntryContents()
500    image.GetEntryOffsets()
501
502    # We need to pack the entries to figure out where everything
503    # should be placed. This sets the offset/size of each entry.
504    # However, after packing we call ProcessEntryContents() which
505    # may result in an entry changing size. In that case we need to
506    # do another pass. Since the device tree often contains the
507    # final offset/size information we try to make space for this in
508    # AddMissingProperties() above. However, if the device is
509    # compressed we cannot know this compressed size in advance,
510    # since changing an offset from 0x100 to 0x104 (for example) can
511    # alter the compressed size of the device tree. So we need a
512    # third pass for this.
513    passes = 5
514    for pack_pass in range(passes):
515        try:
516            image.PackEntries()
517        except Exception as e:
518            if write_map:
519                fname = image.WriteMap()
520                print("Wrote map file '%s' to show errors"  % fname)
521            raise
522        image.SetImagePos()
523        if update_fdt:
524            image.SetCalculatedProperties()
525            for dtb_item in state.GetAllFdts():
526                dtb_item.Sync()
527                dtb_item.Flush()
528        image.WriteSymbols()
529        sizes_ok = image.ProcessEntryContents()
530        if sizes_ok:
531            break
532        image.ResetForPack()
533    tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
534    if not sizes_ok:
535        image.Raise('Entries changed size after packing (tried %s passes)' %
536                    passes)
537
538    image.BuildImage()
539    if write_map:
540        image.WriteMap()
541    missing_list = []
542    image.CheckMissing(missing_list)
543    if missing_list:
544        tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" %
545                     (image.name, ' '.join([e.name for e in missing_list])))
546        _ShowHelpForMissingBlobs(missing_list)
547    return bool(missing_list)
548
549
550def Binman(args):
551    """The main control code for binman
552
553    This assumes that help and test options have already been dealt with. It
554    deals with the core task of building images.
555
556    Args:
557        args: Command line arguments Namespace object
558    """
559    global Image
560    global state
561
562    if args.full_help:
563        pager = os.getenv('PAGER')
564        if not pager:
565            pager = 'more'
566        fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
567                            'README')
568        command.Run(pager, fname)
569        return 0
570
571    # Put these here so that we can import this module without libfdt
572    from binman.image import Image
573    from binman import state
574
575    if args.cmd in ['ls', 'extract', 'replace']:
576        try:
577            tout.Init(args.verbosity)
578            tools.PrepareOutputDir(None)
579            if args.cmd == 'ls':
580                ListEntries(args.image, args.paths)
581
582            if args.cmd == 'extract':
583                ExtractEntries(args.image, args.filename, args.outdir, args.paths,
584                               not args.uncompressed)
585
586            if args.cmd == 'replace':
587                ReplaceEntries(args.image, args.filename, args.indir, args.paths,
588                               do_compress=not args.compressed,
589                               allow_resize=not args.fix_size, write_map=args.map)
590        except:
591            raise
592        finally:
593            tools.FinaliseOutputDir()
594        return 0
595
596    # Try to figure out which device tree contains our image description
597    if args.dt:
598        dtb_fname = args.dt
599    else:
600        board = args.board
601        if not board:
602            raise ValueError('Must provide a board to process (use -b <board>)')
603        board_pathname = os.path.join(args.build_dir, board)
604        dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
605        if not args.indir:
606            args.indir = ['.']
607        args.indir.append(board_pathname)
608
609    try:
610        tout.Init(args.verbosity)
611        elf.debug = args.debug
612        cbfs_util.VERBOSE = args.verbosity > 2
613        state.use_fake_dtb = args.fake_dtb
614        try:
615            tools.SetInputDirs(args.indir)
616            tools.PrepareOutputDir(args.outdir, args.preserve)
617            tools.SetToolPaths(args.toolpath)
618            state.SetEntryArgs(args.entry_arg)
619
620            images = PrepareImagesAndDtbs(dtb_fname, args.image,
621                                          args.update_fdt)
622            missing = False
623            for image in images.values():
624                missing |= ProcessImage(image, args.update_fdt, args.map,
625                                        allow_missing=args.allow_missing)
626
627            # Write the updated FDTs to our output files
628            for dtb_item in state.GetAllFdts():
629                tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
630
631            if missing:
632                tout.Warning("\nSome images are invalid")
633        finally:
634            tools.FinaliseOutputDir()
635    finally:
636        tout.Uninit()
637
638    return 0
639