1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
3 # Written by Simon Glass <sjg@chromium.org>
5 # Creates binary images from input files controlled by a description
8 from collections import OrderedDict
10 import importlib.resources
17 from binman import bintool
18 from binman import cbfs_util
19 from binman import elf
20 from binman import entry
21 from u_boot_pylib import command
22 from u_boot_pylib import tools
23 from u_boot_pylib import tout
25 # These are imported if needed since they import libfdt
29 # List of images we plan to create
30 # Make this global so that it can be referenced from tests
31 images = OrderedDict()
33 # Help text for each type of missing blob, dict:
34 # key: Value of the entry's 'missing-msg' or entry name
35 # value: Text for the help
36 missing_blob_help = {}
38 def _ReadImageDesc(binman_node, use_expanded):
39 """Read the image descriptions from the /binman node
41 This normally produces a single Image object called 'image'. But if
42 multiple images are present, they will all be returned.
45 binman_node: Node object of the /binman node
46 use_expanded: True if the FDT will be updated with the entry information
48 OrderedDict of Image objects, each of which describes an image
51 # pylint: disable=E1102
52 images = OrderedDict()
53 if 'multiple-images' in binman_node.props:
54 for node in binman_node.subnodes:
55 images[node.name] = Image(node.name, node,
56 use_expanded=use_expanded)
58 images['image'] = Image('image', binman_node, use_expanded=use_expanded)
61 def _FindBinmanNode(dtb):
62 """Find the 'binman' node in the device tree
65 dtb: Fdt object to scan
67 Node object of /binman node, or None if not found
69 for node in dtb.GetRoot().subnodes:
70 if node.name == 'binman':
74 def _ReadMissingBlobHelp():
75 """Read the missing-blob-help file
77 This file containins help messages explaining what to do when external blobs
82 key: Message tag (str)
83 value: Message text (str)
86 def _FinishTag(tag, msg, result):
88 result[tag] = msg.rstrip()
93 my_data = pkg_resources.resource_string(__name__, 'missing-blob-help')
94 re_tag = re.compile('^([-a-z0-9]+):$')
98 for line in my_data.decode('utf-8').splitlines():
99 if not line.startswith('#'):
100 m_tag = re_tag.match(line)
102 _, msg = _FinishTag(tag, msg, result)
106 _FinishTag(tag, msg, result)
109 def _ShowBlobHelp(path, text):
110 tout.warning('\n%s:' % path)
111 for line in text.splitlines():
112 tout.warning(' %s' % line)
114 def _ShowHelpForMissingBlobs(missing_list):
115 """Show help for each missing blob to help the user take action
118 missing_list: List of Entry objects to show help for
120 global missing_blob_help
122 if not missing_blob_help:
123 missing_blob_help = _ReadMissingBlobHelp()
125 for entry in missing_list:
126 tags = entry.GetHelpTags()
128 # Show the first match help message
130 if tag in missing_blob_help:
131 _ShowBlobHelp(entry._node.path, missing_blob_help[tag])
134 def GetEntryModules(include_testing=True):
135 """Get a set of entry class implementations
138 Set of paths to entry class filenames
140 glob_list = pkg_resources.resource_listdir(__name__, 'etype')
141 glob_list = [fname for fname in glob_list if fname.endswith('.py')]
142 return set([os.path.splitext(os.path.basename(item))[0]
143 for item in glob_list
144 if include_testing or '_testing' not in item])
146 def WriteEntryDocs(modules, test_missing=None):
147 """Write out documentation for all entries
150 modules: List of Module objects to get docs for
151 test_missing: Used for testing only, to force an entry's documentation
152 to show as missing even if it is present. Should be set to None in
155 from binman.entry import Entry
156 Entry.WriteDocs(modules, test_missing)
159 def write_bintool_docs(modules, test_missing=None):
160 """Write out documentation for all bintools
163 modules: List of Module objects to get docs for
164 test_missing: Used for testing only, to force an entry's documentation
165 to show as missing even if it is present. Should be set to None in
168 bintool.Bintool.WriteDocs(modules, test_missing)
171 def ListEntries(image_fname, entry_paths):
172 """List the entries in an image
174 This decodes the supplied image and displays a table of entries from that
175 image, preceded by a header.
178 image_fname: Image filename to process
179 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
182 image = Image.FromFile(image_fname)
184 entries, lines, widths = image.GetListEntries(entry_paths)
186 num_columns = len(widths)
187 for linenum, line in enumerate(lines):
190 print('-' * (sum(widths) + num_columns * 2))
192 for i, item in enumerate(line):
194 if item.startswith('>'):
197 txt = '%*s ' % (width, item)
202 def ReadEntry(image_fname, entry_path, decomp=True):
203 """Extract an entry from an image
205 This extracts the data from a particular entry in an image
208 image_fname: Image filename to process
209 entry_path: Path to entry to extract
210 decomp: True to return uncompressed data, if the data is compress
211 False to return the raw data
214 data extracted from the entry
217 from binman.image import Image
219 image = Image.FromFile(image_fname)
220 image.CollectBintools()
221 entry = image.FindEntryPath(entry_path)
222 return entry.ReadData(decomp)
225 def ShowAltFormats(image):
226 """Show alternative formats available for entries in the image
228 This shows a list of formats available.
231 image (Image): Image to check
234 image.CheckAltFormats(alt_formats)
235 print('%-10s %-20s %s' % ('Flag (-F)', 'Entry type', 'Description'))
236 for name, val in alt_formats.items():
237 entry, helptext = val
238 print('%-10s %-20s %s' % (name, entry.etype, helptext))
241 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
242 decomp=True, alt_format=None):
243 """Extract the data from one or more entries and write it to files
246 image_fname: Image filename to process
247 output_fname: Single output filename to use if extracting one file, None
249 outdir: Output directory to use (for any number of files), else None
250 entry_paths: List of entry paths to extract
251 decomp: True to decompress the entry data
254 List of EntryInfo records that were written
256 image = Image.FromFile(image_fname)
257 image.CollectBintools()
259 if alt_format == 'list':
260 ShowAltFormats(image)
263 # Output an entry to a single file, as a special case
266 raise ValueError('Must specify an entry path to write with -f')
267 if len(entry_paths) != 1:
268 raise ValueError('Must specify exactly one entry path to write with -f')
269 entry = image.FindEntryPath(entry_paths[0])
270 data = entry.ReadData(decomp, alt_format)
271 tools.write_file(output_fname, data)
272 tout.notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
275 # Otherwise we will output to a path given by the entry path of each entry.
276 # This means that entries will appear in subdirectories if they are part of
278 einfos = image.GetListEntries(entry_paths)[0]
279 tout.notice('%d entries match and will be written' % len(einfos))
282 data = entry.ReadData(decomp, alt_format)
283 path = entry.GetPath()[1:]
284 fname = os.path.join(outdir, path)
286 # If this entry has children, create a directory for it and put its
287 # data in a file called 'root' in that directory
288 if entry.GetEntries():
289 if fname and not os.path.exists(fname):
291 fname = os.path.join(fname, 'root')
292 tout.notice("Write entry '%s' size %x to '%s'" %
293 (entry.GetPath(), len(data), fname))
294 tools.write_file(fname, data)
298 def BeforeReplace(image, allow_resize):
299 """Handle getting an image ready for replacing entries in it
302 image: Image to prepare
304 state.PrepareFromLoadedData(image)
306 image.CollectBintools()
308 # If repacking, drop the old offset/size values except for the original
309 # ones, so we are only left with the constraints.
310 if image.allow_repack and allow_resize:
314 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
315 """Handle replacing a single entry an an image
318 image: Image to update
319 entry: Entry to write
320 data: Data to replace with
321 do_compress: True to compress the data if needed, False if data is
322 already compressed so should be used as is
323 allow_resize: True to allow entries to change size (this does a re-pack
324 of the entries), False to raise an exception
326 if not entry.WriteData(data, do_compress):
327 if not image.allow_repack:
328 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
330 entry.Raise('Entry data size does not match, but resize is disabled')
333 def AfterReplace(image, allow_resize, write_map):
334 """Handle write out an image after replacing entries in it
337 image: Image to write
338 allow_resize: True to allow entries to change size (this does a re-pack
339 of the entries), False to raise an exception
340 write_map: True to write a map file
342 tout.info('Processing image')
343 ProcessImage(image, update_fdt=True, write_map=write_map,
344 get_contents=False, allow_resize=allow_resize)
347 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
349 BeforeReplace(image, allow_resize)
350 tout.info('Writing data to %s' % entry.GetPath())
351 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
352 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
355 def WriteEntry(image_fname, entry_path, data, do_compress=True,
356 allow_resize=True, write_map=False):
357 """Replace an entry in an image
359 This replaces the data in a particular entry in an image. This size of the
360 new data must match the size of the old data unless allow_resize is True.
363 image_fname: Image filename to process
364 entry_path: Path to entry to extract
365 data: Data to replace with
366 do_compress: True to compress the data if needed, False if data is
367 already compressed so should be used as is
368 allow_resize: True to allow entries to change size (this does a re-pack
369 of the entries), False to raise an exception
370 write_map: True to write a map file
373 Image object that was updated
375 tout.info("Write entry '%s', file '%s'" % (entry_path, image_fname))
376 image = Image.FromFile(image_fname)
377 image.CollectBintools()
378 entry = image.FindEntryPath(entry_path)
379 WriteEntryToImage(image, entry, data, do_compress=do_compress,
380 allow_resize=allow_resize, write_map=write_map)
385 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
386 do_compress=True, allow_resize=True, write_map=False):
387 """Replace the data from one or more entries from input files
390 image_fname: Image filename to process
391 input_fname: Single input filename to use if replacing one file, None
393 indir: Input directory to use (for any number of files), else None
394 entry_paths: List of entry paths to replace
395 do_compress: True if the input data is uncompressed and may need to be
396 compressed if the entry requires it, False if the data is already
398 write_map: True to write a map file
401 List of EntryInfo records that were written
403 image_fname = os.path.abspath(image_fname)
404 image = Image.FromFile(image_fname)
406 image.mark_build_done()
408 # Replace an entry from a single file, as a special case
411 raise ValueError('Must specify an entry path to read with -f')
412 if len(entry_paths) != 1:
413 raise ValueError('Must specify exactly one entry path to write with -f')
414 entry = image.FindEntryPath(entry_paths[0])
415 data = tools.read_file(input_fname)
416 tout.notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
417 WriteEntryToImage(image, entry, data, do_compress=do_compress,
418 allow_resize=allow_resize, write_map=write_map)
421 # Otherwise we will input from a path given by the entry path of each entry.
422 # This means that files must appear in subdirectories if they are part of
424 einfos = image.GetListEntries(entry_paths)[0]
425 tout.notice("Replacing %d matching entries in image '%s'" %
426 (len(einfos), image_fname))
428 BeforeReplace(image, allow_resize)
432 if entry.GetEntries():
433 tout.info("Skipping section entry '%s'" % entry.GetPath())
436 path = entry.GetPath()[1:]
437 fname = os.path.join(indir, path)
439 if os.path.exists(fname):
440 tout.notice("Write entry '%s' from file '%s'" %
441 (entry.GetPath(), fname))
442 data = tools.read_file(fname)
443 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
445 tout.warning("Skipping entry '%s' from missing file '%s'" %
446 (entry.GetPath(), fname))
448 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
451 def SignEntries(image_fname, input_fname, privatekey_fname, algo, entry_paths,
453 """Sign and replace the data from one or more entries from input files
456 image_fname: Image filename to process
457 input_fname: Single input filename to use if replacing one file, None
459 algo: Hashing algorithm
460 entry_paths: List of entry paths to sign
461 privatekey_fname: Private key filename
462 write_map (bool): True to write the map file
464 image_fname = os.path.abspath(image_fname)
465 image = Image.FromFile(image_fname)
467 image.mark_build_done()
469 BeforeReplace(image, allow_resize=True)
471 for entry_path in entry_paths:
472 entry = image.FindEntryPath(entry_path)
473 entry.UpdateSignatures(privatekey_fname, algo, input_fname)
475 AfterReplace(image, allow_resize=True, write_map=write_map)
477 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded):
478 """Prepare the images to be processed and select the device tree
481 - reads in the device tree
482 - finds and scans the binman node to create all entries
483 - selects which images to build
484 - Updates the device tress with placeholder properties for offset,
488 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
489 selected_images: List of images to output, or None for all
490 update_fdt: True to update the FDT wth entry offsets, etc.
491 use_expanded: True to use expanded versions of entries, if available.
492 So if 'u-boot' is called for, we use 'u-boot-expanded' instead. This
493 is needed if update_fdt is True (although tests may disable it)
496 OrderedDict of images:
497 key: Image name (str)
500 # Import these here in case libfdt.py is not available, in which case
501 # the above help option still works.
503 from dtoc import fdt_util
506 # Get the device tree ready by compiling it and copying the compiled
507 # output into a file in our output directly. Then scan it for use
509 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
510 fname = tools.get_output_filename('u-boot.dtb.out')
511 tools.write_file(fname, tools.read_file(dtb_fname))
512 dtb = fdt.FdtScan(fname)
514 node = _FindBinmanNode(dtb)
516 raise ValueError("Device tree '%s' does not have a 'binman' "
519 images = _ReadImageDesc(node, use_expanded)
523 new_images = OrderedDict()
524 for name, image in images.items():
525 if name in select_images:
526 new_images[name] = image
530 tout.notice('Skipping images: %s' % ', '.join(skip))
532 state.Prepare(images, dtb)
534 # Prepare the device tree by making sure that any missing
535 # properties are added (e.g. 'pos' and 'size'). The values of these
536 # may not be correct yet, but we add placeholders so that the
537 # size of the device tree is correct. Later, in
538 # SetCalculatedProperties() we will insert the correct values
539 # without changing the device-tree size, thus ensuring that our
540 # entry offsets remain the same.
541 for image in images.values():
543 image.CollectBintools()
545 image.AddMissingProperties(True)
546 image.ProcessFdt(dtb)
548 for dtb_item in state.GetAllFdts():
549 dtb_item.Sync(auto_resize=True)
555 def ProcessImage(image, update_fdt, write_map, get_contents=True,
556 allow_resize=True, allow_missing=False,
557 allow_fake_blobs=False):
558 """Perform all steps for this image, including checking and # writing it.
560 This means that errors found with a later image will be reported after
561 earlier images are already completed and written, but that does not seem
565 image: Image to process
566 update_fdt: True to update the FDT wth entry offsets, etc.
567 write_map: True to write a map file
568 get_contents: True to get the image contents from files, etc., False if
569 the contents is already present
570 allow_resize: True to allow entries to change size (this does a re-pack
571 of the entries), False to raise an exception
572 allow_missing: Allow blob_ext objects to be missing
573 allow_fake_blobs: Allow blob_ext objects to be faked with dummy files
576 True if one or more external blobs are missing or faked,
577 False if all are present
580 image.SetAllowMissing(allow_missing)
581 image.SetAllowFakeBlob(allow_fake_blobs)
582 image.GetEntryContents()
584 image.GetEntryOffsets()
586 # We need to pack the entries to figure out where everything
587 # should be placed. This sets the offset/size of each entry.
588 # However, after packing we call ProcessEntryContents() which
589 # may result in an entry changing size. In that case we need to
590 # do another pass. Since the device tree often contains the
591 # final offset/size information we try to make space for this in
592 # AddMissingProperties() above. However, if the device is
593 # compressed we cannot know this compressed size in advance,
594 # since changing an offset from 0x100 to 0x104 (for example) can
595 # alter the compressed size of the device tree. So we need a
596 # third pass for this.
598 for pack_pass in range(passes):
601 except Exception as e:
603 fname = image.WriteMap()
604 print("Wrote map file '%s' to show errors" % fname)
608 image.SetCalculatedProperties()
609 for dtb_item in state.GetAllFdts():
613 sizes_ok = image.ProcessEntryContents()
617 tout.info('Pack completed after %d pass(es)' % (pack_pass + 1))
619 image.Raise('Entries changed size after packing (tried %s passes)' %
627 image.CheckMissing(missing_list)
629 tout.warning("Image '%s' is missing external blobs and is non-functional: %s" %
630 (image.name, ' '.join([e.name for e in missing_list])))
631 _ShowHelpForMissingBlobs(missing_list)
634 image.CheckFakedBlobs(faked_list)
637 "Image '%s' has faked external blobs and is non-functional: %s" %
638 (image.name, ' '.join([os.path.basename(e.GetDefaultFilename())
639 for e in faked_list])))
642 image.CheckOptional(optional_list)
645 "Image '%s' is missing external blobs but is still functional: %s" %
646 (image.name, ' '.join([e.name for e in optional_list])))
647 _ShowHelpForMissingBlobs(optional_list)
649 missing_bintool_list = []
650 image.check_missing_bintools(missing_bintool_list)
651 if missing_bintool_list:
653 "Image '%s' has missing bintools and is non-functional: %s" %
654 (image.name, ' '.join([os.path.basename(bintool.name)
655 for bintool in missing_bintool_list])))
656 return any([missing_list, faked_list, missing_bintool_list])
660 """The main control code for binman
662 This assumes that help and test options have already been dealt with. It
663 deals with the core task of building images.
666 args: Command line arguments Namespace object
672 with importlib.resources.path('binman', 'README.rst') as readme:
673 tools.print_full_help(str(readme))
676 # Put these here so that we can import this module without libfdt
677 from binman.image import Image
678 from binman import state
682 tool_paths += args.toolpath
684 tool_paths.append(args.tooldir)
685 tools.set_tool_paths(tool_paths or None)
686 bintool.Bintool.set_tool_dir(args.tooldir)
688 if args.cmd in ['ls', 'extract', 'replace', 'tool', 'sign']:
690 tout.init(args.verbosity)
691 if args.cmd == 'replace':
692 tools.prepare_output_dir(args.outdir, args.preserve)
694 tools.prepare_output_dir(None)
696 ListEntries(args.image, args.paths)
698 if args.cmd == 'extract':
699 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
700 not args.uncompressed, args.format)
702 if args.cmd == 'replace':
703 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
704 do_compress=not args.compressed,
705 allow_resize=not args.fix_size, write_map=args.map)
707 if args.cmd == 'sign':
708 SignEntries(args.image, args.file, args.key, args.algo, args.paths)
710 if args.cmd == 'tool':
712 bintool.Bintool.list_all()
714 if not args.bintools:
716 "Please specify bintools to fetch or 'all' or 'missing'")
717 bintool.Bintool.fetch_tools(bintool.FETCH_ANY,
720 raise ValueError("Invalid arguments to 'tool' subcommand")
724 tools.finalise_output_dir()
728 if args.update_fdt_in_elf:
729 elf_params = args.update_fdt_in_elf.split(',')
730 if len(elf_params) != 4:
731 raise ValueError('Invalid args %s to --update-fdt-in-elf: expected infile,outfile,begin_sym,end_sym' %
734 # Try to figure out which device tree contains our image description
740 raise ValueError('Must provide a board to process (use -b <board>)')
741 board_pathname = os.path.join(args.build_dir, board)
742 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
745 args.indir.append(board_pathname)
748 tout.init(args.verbosity)
749 elf.debug = args.debug
750 cbfs_util.VERBOSE = args.verbosity > 2
751 state.use_fake_dtb = args.fake_dtb
753 # Normally we replace the 'u-boot' etype with 'u-boot-expanded', etc.
754 # When running tests this can be disabled using this flag. When not
755 # updating the FDT in image, it is not needed by binman, but we use it
756 # for consistency, so that the images look the same to U-Boot at
758 use_expanded = not args.no_expanded
760 tools.set_input_dirs(args.indir)
761 tools.prepare_output_dir(args.outdir, args.preserve)
762 state.SetEntryArgs(args.entry_arg)
763 state.SetThreads(args.threads)
765 images = PrepareImagesAndDtbs(dtb_fname, args.image,
766 args.update_fdt, use_expanded)
768 if args.test_section_timeout:
769 # Set the first image to timeout, used in testThreadTimeout()
770 images[list(images.keys())[0]].test_section_timeout = True
772 bintool.Bintool.set_missing_list(
773 args.force_missing_bintools.split(',') if
774 args.force_missing_bintools else None)
776 # Create the directory here instead of Entry.check_fake_fname()
777 # since that is called from a threaded context so different threads
778 # may race to create the directory
779 if args.fake_ext_blobs:
780 entry.Entry.create_fake_dir()
782 for image in images.values():
783 invalid |= ProcessImage(image, args.update_fdt, args.map,
784 allow_missing=args.allow_missing,
785 allow_fake_blobs=args.fake_ext_blobs)
787 # Write the updated FDTs to our output files
788 for dtb_item in state.GetAllFdts():
789 tools.write_file(dtb_item._fname, dtb_item.GetContents())
792 data = state.GetFdtForEtype('u-boot-dtb').GetContents()
793 elf.UpdateFile(*elf_params, data)
795 # This can only be True if -M is provided, since otherwise binman
796 # would have raised an error already
798 msg = '\nSome images are invalid'
799 if args.ignore_missing:
805 # Use this to debug the time take to pack the image
808 tools.finalise_output_dir()