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
11 import importlib.resources
14 import importlib_resources
21 from binman import bintool
22 from binman import cbfs_util
23 from binman import elf
24 from binman import entry
25 from u_boot_pylib import command
26 from u_boot_pylib import tools
27 from u_boot_pylib import tout
29 # These are imported if needed since they import libfdt
33 # List of images we plan to create
34 # Make this global so that it can be referenced from tests
35 images = OrderedDict()
37 # Help text for each type of missing blob, dict:
38 # key: Value of the entry's 'missing-msg' or entry name
39 # value: Text for the help
40 missing_blob_help = {}
42 def _ReadImageDesc(binman_node, use_expanded):
43 """Read the image descriptions from the /binman node
45 This normally produces a single Image object called 'image'. But if
46 multiple images are present, they will all be returned.
49 binman_node: Node object of the /binman node
50 use_expanded: True if the FDT will be updated with the entry information
52 OrderedDict of Image objects, each of which describes an image
55 # pylint: disable=E1102
56 images = OrderedDict()
57 if 'multiple-images' in binman_node.props:
58 for node in binman_node.subnodes:
59 images[node.name] = Image(node.name, node,
60 use_expanded=use_expanded)
62 images['image'] = Image('image', binman_node, use_expanded=use_expanded)
65 def _FindBinmanNode(dtb):
66 """Find the 'binman' node in the device tree
69 dtb: Fdt object to scan
71 Node object of /binman node, or None if not found
73 for node in dtb.GetRoot().subnodes:
74 if node.name == 'binman':
78 def _ReadMissingBlobHelp():
79 """Read the missing-blob-help file
81 This file containins help messages explaining what to do when external blobs
86 key: Message tag (str)
87 value: Message text (str)
90 def _FinishTag(tag, msg, result):
92 result[tag] = msg.rstrip()
97 my_data = pkg_resources.resource_string(__name__, 'missing-blob-help')
98 re_tag = re.compile('^([-a-z0-9]+):$')
102 for line in my_data.decode('utf-8').splitlines():
103 if not line.startswith('#'):
104 m_tag = re_tag.match(line)
106 _, msg = _FinishTag(tag, msg, result)
110 _FinishTag(tag, msg, result)
113 def _ShowBlobHelp(path, text):
114 tout.warning('\n%s:' % path)
115 for line in text.splitlines():
116 tout.warning(' %s' % line)
118 def _ShowHelpForMissingBlobs(missing_list):
119 """Show help for each missing blob to help the user take action
122 missing_list: List of Entry objects to show help for
124 global missing_blob_help
126 if not missing_blob_help:
127 missing_blob_help = _ReadMissingBlobHelp()
129 for entry in missing_list:
130 tags = entry.GetHelpTags()
132 # Show the first match help message
134 if tag in missing_blob_help:
135 _ShowBlobHelp(entry._node.path, missing_blob_help[tag])
138 def GetEntryModules(include_testing=True):
139 """Get a set of entry class implementations
142 Set of paths to entry class filenames
144 glob_list = pkg_resources.resource_listdir(__name__, 'etype')
145 glob_list = [fname for fname in glob_list if fname.endswith('.py')]
146 return set([os.path.splitext(os.path.basename(item))[0]
147 for item in glob_list
148 if include_testing or '_testing' not in item])
150 def WriteEntryDocs(modules, test_missing=None):
151 """Write out documentation for all entries
154 modules: List of Module objects to get docs for
155 test_missing: Used for testing only, to force an entry's documentation
156 to show as missing even if it is present. Should be set to None in
159 from binman.entry import Entry
160 Entry.WriteDocs(modules, test_missing)
163 def write_bintool_docs(modules, test_missing=None):
164 """Write out documentation for all bintools
167 modules: List of Module objects to get docs for
168 test_missing: Used for testing only, to force an entry's documentation
169 to show as missing even if it is present. Should be set to None in
172 bintool.Bintool.WriteDocs(modules, test_missing)
175 def ListEntries(image_fname, entry_paths):
176 """List the entries in an image
178 This decodes the supplied image and displays a table of entries from that
179 image, preceded by a header.
182 image_fname: Image filename to process
183 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
186 image = Image.FromFile(image_fname)
188 entries, lines, widths = image.GetListEntries(entry_paths)
190 num_columns = len(widths)
191 for linenum, line in enumerate(lines):
194 print('-' * (sum(widths) + num_columns * 2))
196 for i, item in enumerate(line):
198 if item.startswith('>'):
201 txt = '%*s ' % (width, item)
206 def ReadEntry(image_fname, entry_path, decomp=True):
207 """Extract an entry from an image
209 This extracts the data from a particular entry in an image
212 image_fname: Image filename to process
213 entry_path: Path to entry to extract
214 decomp: True to return uncompressed data, if the data is compress
215 False to return the raw data
218 data extracted from the entry
221 from binman.image import Image
223 image = Image.FromFile(image_fname)
224 image.CollectBintools()
225 entry = image.FindEntryPath(entry_path)
226 return entry.ReadData(decomp)
229 def ShowAltFormats(image):
230 """Show alternative formats available for entries in the image
232 This shows a list of formats available.
235 image (Image): Image to check
238 image.CheckAltFormats(alt_formats)
239 print('%-10s %-20s %s' % ('Flag (-F)', 'Entry type', 'Description'))
240 for name, val in alt_formats.items():
241 entry, helptext = val
242 print('%-10s %-20s %s' % (name, entry.etype, helptext))
245 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
246 decomp=True, alt_format=None):
247 """Extract the data from one or more entries and write it to files
250 image_fname: Image filename to process
251 output_fname: Single output filename to use if extracting one file, None
253 outdir: Output directory to use (for any number of files), else None
254 entry_paths: List of entry paths to extract
255 decomp: True to decompress the entry data
258 List of EntryInfo records that were written
260 image = Image.FromFile(image_fname)
261 image.CollectBintools()
263 if alt_format == 'list':
264 ShowAltFormats(image)
267 # Output an entry to a single file, as a special case
270 raise ValueError('Must specify an entry path to write with -f')
271 if len(entry_paths) != 1:
272 raise ValueError('Must specify exactly one entry path to write with -f')
273 entry = image.FindEntryPath(entry_paths[0])
274 data = entry.ReadData(decomp, alt_format)
275 tools.write_file(output_fname, data)
276 tout.notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
279 # Otherwise we will output to a path given by the entry path of each entry.
280 # This means that entries will appear in subdirectories if they are part of
282 einfos = image.GetListEntries(entry_paths)[0]
283 tout.notice('%d entries match and will be written' % len(einfos))
286 data = entry.ReadData(decomp, alt_format)
287 path = entry.GetPath()[1:]
288 fname = os.path.join(outdir, path)
290 # If this entry has children, create a directory for it and put its
291 # data in a file called 'root' in that directory
292 if entry.GetEntries():
293 if fname and not os.path.exists(fname):
295 fname = os.path.join(fname, 'root')
296 tout.notice("Write entry '%s' size %x to '%s'" %
297 (entry.GetPath(), len(data), fname))
298 tools.write_file(fname, data)
302 def BeforeReplace(image, allow_resize):
303 """Handle getting an image ready for replacing entries in it
306 image: Image to prepare
308 state.PrepareFromLoadedData(image)
310 image.CollectBintools()
312 # If repacking, drop the old offset/size values except for the original
313 # ones, so we are only left with the constraints.
314 if image.allow_repack and allow_resize:
318 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
319 """Handle replacing a single entry an an image
322 image: Image to update
323 entry: Entry to write
324 data: Data to replace with
325 do_compress: True to compress the data if needed, False if data is
326 already compressed so should be used as is
327 allow_resize: True to allow entries to change size (this does a re-pack
328 of the entries), False to raise an exception
330 if not entry.WriteData(data, do_compress):
331 if not image.allow_repack:
332 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
334 entry.Raise('Entry data size does not match, but resize is disabled')
337 def AfterReplace(image, allow_resize, write_map):
338 """Handle write out an image after replacing entries in it
341 image: Image to write
342 allow_resize: True to allow entries to change size (this does a re-pack
343 of the entries), False to raise an exception
344 write_map: True to write a map file
346 tout.info('Processing image')
347 ProcessImage(image, update_fdt=True, write_map=write_map,
348 get_contents=False, allow_resize=allow_resize)
351 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
353 BeforeReplace(image, allow_resize)
354 tout.info('Writing data to %s' % entry.GetPath())
355 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
356 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
359 def WriteEntry(image_fname, entry_path, data, do_compress=True,
360 allow_resize=True, write_map=False):
361 """Replace an entry in an image
363 This replaces the data in a particular entry in an image. This size of the
364 new data must match the size of the old data unless allow_resize is True.
367 image_fname: Image filename to process
368 entry_path: Path to entry to extract
369 data: Data to replace with
370 do_compress: True to compress the data if needed, False if data is
371 already compressed so should be used as is
372 allow_resize: True to allow entries to change size (this does a re-pack
373 of the entries), False to raise an exception
374 write_map: True to write a map file
377 Image object that was updated
379 tout.info("Write entry '%s', file '%s'" % (entry_path, image_fname))
380 image = Image.FromFile(image_fname)
381 image.CollectBintools()
382 entry = image.FindEntryPath(entry_path)
383 WriteEntryToImage(image, entry, data, do_compress=do_compress,
384 allow_resize=allow_resize, write_map=write_map)
389 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
390 do_compress=True, allow_resize=True, write_map=False):
391 """Replace the data from one or more entries from input files
394 image_fname: Image filename to process
395 input_fname: Single input filename to use if replacing one file, None
397 indir: Input directory to use (for any number of files), else None
398 entry_paths: List of entry paths to replace
399 do_compress: True if the input data is uncompressed and may need to be
400 compressed if the entry requires it, False if the data is already
402 write_map: True to write a map file
405 List of EntryInfo records that were written
407 image_fname = os.path.abspath(image_fname)
408 image = Image.FromFile(image_fname)
410 image.mark_build_done()
412 # Replace an entry from a single file, as a special case
415 raise ValueError('Must specify an entry path to read with -f')
416 if len(entry_paths) != 1:
417 raise ValueError('Must specify exactly one entry path to write with -f')
418 entry = image.FindEntryPath(entry_paths[0])
419 data = tools.read_file(input_fname)
420 tout.notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
421 WriteEntryToImage(image, entry, data, do_compress=do_compress,
422 allow_resize=allow_resize, write_map=write_map)
425 # Otherwise we will input from a path given by the entry path of each entry.
426 # This means that files must appear in subdirectories if they are part of
428 einfos = image.GetListEntries(entry_paths)[0]
429 tout.notice("Replacing %d matching entries in image '%s'" %
430 (len(einfos), image_fname))
432 BeforeReplace(image, allow_resize)
436 if entry.GetEntries():
437 tout.info("Skipping section entry '%s'" % entry.GetPath())
440 path = entry.GetPath()[1:]
441 fname = os.path.join(indir, path)
443 if os.path.exists(fname):
444 tout.notice("Write entry '%s' from file '%s'" %
445 (entry.GetPath(), fname))
446 data = tools.read_file(fname)
447 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
449 tout.warning("Skipping entry '%s' from missing file '%s'" %
450 (entry.GetPath(), fname))
452 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
455 def SignEntries(image_fname, input_fname, privatekey_fname, algo, entry_paths,
457 """Sign and replace the data from one or more entries from input files
460 image_fname: Image filename to process
461 input_fname: Single input filename to use if replacing one file, None
463 algo: Hashing algorithm
464 entry_paths: List of entry paths to sign
465 privatekey_fname: Private key filename
466 write_map (bool): True to write the map file
468 image_fname = os.path.abspath(image_fname)
469 image = Image.FromFile(image_fname)
471 image.mark_build_done()
473 BeforeReplace(image, allow_resize=True)
475 for entry_path in entry_paths:
476 entry = image.FindEntryPath(entry_path)
477 entry.UpdateSignatures(privatekey_fname, algo, input_fname)
479 AfterReplace(image, allow_resize=True, write_map=write_map)
481 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded):
482 """Prepare the images to be processed and select the device tree
485 - reads in the device tree
486 - finds and scans the binman node to create all entries
487 - selects which images to build
488 - Updates the device tress with placeholder properties for offset,
492 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
493 selected_images: List of images to output, or None for all
494 update_fdt: True to update the FDT wth entry offsets, etc.
495 use_expanded: True to use expanded versions of entries, if available.
496 So if 'u-boot' is called for, we use 'u-boot-expanded' instead. This
497 is needed if update_fdt is True (although tests may disable it)
500 OrderedDict of images:
501 key: Image name (str)
504 # Import these here in case libfdt.py is not available, in which case
505 # the above help option still works.
507 from dtoc import fdt_util
510 # Get the device tree ready by compiling it and copying the compiled
511 # output into a file in our output directly. Then scan it for use
513 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
514 fname = tools.get_output_filename('u-boot.dtb.out')
515 tools.write_file(fname, tools.read_file(dtb_fname))
516 dtb = fdt.FdtScan(fname)
518 node = _FindBinmanNode(dtb)
520 raise ValueError("Device tree '%s' does not have a 'binman' "
523 images = _ReadImageDesc(node, use_expanded)
527 new_images = OrderedDict()
528 for name, image in images.items():
529 if name in select_images:
530 new_images[name] = image
534 tout.notice('Skipping images: %s' % ', '.join(skip))
536 state.Prepare(images, dtb)
538 # Prepare the device tree by making sure that any missing
539 # properties are added (e.g. 'pos' and 'size'). The values of these
540 # may not be correct yet, but we add placeholders so that the
541 # size of the device tree is correct. Later, in
542 # SetCalculatedProperties() we will insert the correct values
543 # without changing the device-tree size, thus ensuring that our
544 # entry offsets remain the same.
545 for image in images.values():
547 image.CollectBintools()
549 image.AddMissingProperties(True)
550 image.ProcessFdt(dtb)
552 for dtb_item in state.GetAllFdts():
553 dtb_item.Sync(auto_resize=True)
559 def ProcessImage(image, update_fdt, write_map, get_contents=True,
560 allow_resize=True, allow_missing=False,
561 allow_fake_blobs=False):
562 """Perform all steps for this image, including checking and # writing it.
564 This means that errors found with a later image will be reported after
565 earlier images are already completed and written, but that does not seem
569 image: Image to process
570 update_fdt: True to update the FDT wth entry offsets, etc.
571 write_map: True to write a map file
572 get_contents: True to get the image contents from files, etc., False if
573 the contents is already present
574 allow_resize: True to allow entries to change size (this does a re-pack
575 of the entries), False to raise an exception
576 allow_missing: Allow blob_ext objects to be missing
577 allow_fake_blobs: Allow blob_ext objects to be faked with dummy files
580 True if one or more external blobs are missing or faked,
581 False if all are present
584 image.SetAllowMissing(allow_missing)
585 image.SetAllowFakeBlob(allow_fake_blobs)
586 image.GetEntryContents()
588 image.GetEntryOffsets()
590 # We need to pack the entries to figure out where everything
591 # should be placed. This sets the offset/size of each entry.
592 # However, after packing we call ProcessEntryContents() which
593 # may result in an entry changing size. In that case we need to
594 # do another pass. Since the device tree often contains the
595 # final offset/size information we try to make space for this in
596 # AddMissingProperties() above. However, if the device is
597 # compressed we cannot know this compressed size in advance,
598 # since changing an offset from 0x100 to 0x104 (for example) can
599 # alter the compressed size of the device tree. So we need a
600 # third pass for this.
602 for pack_pass in range(passes):
605 except Exception as e:
607 fname = image.WriteMap()
608 print("Wrote map file '%s' to show errors" % fname)
612 image.SetCalculatedProperties()
613 for dtb_item in state.GetAllFdts():
617 sizes_ok = image.ProcessEntryContents()
621 tout.info('Pack completed after %d pass(es)' % (pack_pass + 1))
623 image.Raise('Entries changed size after packing (tried %s passes)' %
631 image.CheckMissing(missing_list)
633 tout.warning("Image '%s' is missing external blobs and is non-functional: %s" %
634 (image.name, ' '.join([e.name for e in missing_list])))
635 _ShowHelpForMissingBlobs(missing_list)
638 image.CheckFakedBlobs(faked_list)
641 "Image '%s' has faked external blobs and is non-functional: %s" %
642 (image.name, ' '.join([os.path.basename(e.GetDefaultFilename())
643 for e in faked_list])))
646 image.CheckOptional(optional_list)
649 "Image '%s' is missing external blobs but is still functional: %s" %
650 (image.name, ' '.join([e.name for e in optional_list])))
651 _ShowHelpForMissingBlobs(optional_list)
653 missing_bintool_list = []
654 image.check_missing_bintools(missing_bintool_list)
655 if missing_bintool_list:
657 "Image '%s' has missing bintools and is non-functional: %s" %
658 (image.name, ' '.join([os.path.basename(bintool.name)
659 for bintool in missing_bintool_list])))
660 return any([missing_list, faked_list, missing_bintool_list])
664 """The main control code for binman
666 This assumes that help and test options have already been dealt with. It
667 deals with the core task of building images.
670 args: Command line arguments Namespace object
676 with importlib.resources.path('binman', 'README.rst') as readme:
677 tools.print_full_help(str(readme))
680 # Put these here so that we can import this module without libfdt
681 from binman.image import Image
682 from binman import state
686 tool_paths += args.toolpath
688 tool_paths.append(args.tooldir)
689 tools.set_tool_paths(tool_paths or None)
690 bintool.Bintool.set_tool_dir(args.tooldir)
692 if args.cmd in ['ls', 'extract', 'replace', 'tool', 'sign']:
694 tout.init(args.verbosity)
695 if args.cmd == 'replace':
696 tools.prepare_output_dir(args.outdir, args.preserve)
698 tools.prepare_output_dir(None)
700 ListEntries(args.image, args.paths)
702 if args.cmd == 'extract':
703 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
704 not args.uncompressed, args.format)
706 if args.cmd == 'replace':
707 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
708 do_compress=not args.compressed,
709 allow_resize=not args.fix_size, write_map=args.map)
711 if args.cmd == 'sign':
712 SignEntries(args.image, args.file, args.key, args.algo, args.paths)
714 if args.cmd == 'tool':
716 bintool.Bintool.list_all()
718 if not args.bintools:
720 "Please specify bintools to fetch or 'all' or 'missing'")
721 bintool.Bintool.fetch_tools(bintool.FETCH_ANY,
724 raise ValueError("Invalid arguments to 'tool' subcommand")
728 tools.finalise_output_dir()
732 if args.update_fdt_in_elf:
733 elf_params = args.update_fdt_in_elf.split(',')
734 if len(elf_params) != 4:
735 raise ValueError('Invalid args %s to --update-fdt-in-elf: expected infile,outfile,begin_sym,end_sym' %
738 # Try to figure out which device tree contains our image description
744 raise ValueError('Must provide a board to process (use -b <board>)')
745 board_pathname = os.path.join(args.build_dir, board)
746 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
749 args.indir.append(board_pathname)
752 tout.init(args.verbosity)
753 elf.debug = args.debug
754 cbfs_util.VERBOSE = args.verbosity > 2
755 state.use_fake_dtb = args.fake_dtb
757 # Normally we replace the 'u-boot' etype with 'u-boot-expanded', etc.
758 # When running tests this can be disabled using this flag. When not
759 # updating the FDT in image, it is not needed by binman, but we use it
760 # for consistency, so that the images look the same to U-Boot at
762 use_expanded = not args.no_expanded
764 tools.set_input_dirs(args.indir)
765 tools.prepare_output_dir(args.outdir, args.preserve)
766 state.SetEntryArgs(args.entry_arg)
767 state.SetThreads(args.threads)
769 images = PrepareImagesAndDtbs(dtb_fname, args.image,
770 args.update_fdt, use_expanded)
772 if args.test_section_timeout:
773 # Set the first image to timeout, used in testThreadTimeout()
774 images[list(images.keys())[0]].test_section_timeout = True
776 bintool.Bintool.set_missing_list(
777 args.force_missing_bintools.split(',') if
778 args.force_missing_bintools else None)
780 # Create the directory here instead of Entry.check_fake_fname()
781 # since that is called from a threaded context so different threads
782 # may race to create the directory
783 if args.fake_ext_blobs:
784 entry.Entry.create_fake_dir()
786 for image in images.values():
787 invalid |= ProcessImage(image, args.update_fdt, args.map,
788 allow_missing=args.allow_missing,
789 allow_fake_blobs=args.fake_ext_blobs)
791 # Write the updated FDTs to our output files
792 for dtb_item in state.GetAllFdts():
793 tools.write_file(dtb_item._fname, dtb_item.GetContents())
796 data = state.GetFdtForEtype('u-boot-dtb').GetContents()
797 elf.UpdateFile(*elf_params, data)
799 # This can only be True if -M is provided, since otherwise binman
800 # would have raised an error already
802 msg = '\nSome images are invalid'
803 if args.ignore_missing:
809 # Use this to debug the time take to pack the image
812 tools.finalise_output_dir()