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
15 from patman import tools
17 from binman import cbfs_util
18 from binman import elf
19 from patman import command
20 from patman import tout
22 # List of images we plan to create
23 # Make this global so that it can be referenced from tests
24 images = OrderedDict()
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
29 missing_blob_help = {}
31 def _ReadImageDesc(binman_node, use_expanded):
32 """Read the image descriptions from the /binman node
34 This normally produces a single Image object called 'image'. But if
35 multiple images are present, they will all be returned.
38 binman_node: Node object of the /binman node
39 use_expanded: True if the FDT will be updated with the entry information
41 OrderedDict of Image objects, each of which describes an image
43 images = OrderedDict()
44 if 'multiple-images' in binman_node.props:
45 for node in binman_node.subnodes:
46 images[node.name] = Image(node.name, node,
47 use_expanded=use_expanded)
49 images['image'] = Image('image', binman_node, use_expanded=use_expanded)
52 def _FindBinmanNode(dtb):
53 """Find the 'binman' node in the device tree
56 dtb: Fdt object to scan
58 Node object of /binman node, or None if not found
60 for node in dtb.GetRoot().subnodes:
61 if node.name == 'binman':
65 def _ReadMissingBlobHelp():
66 """Read the missing-blob-help file
68 This file containins help messages explaining what to do when external blobs
73 key: Message tag (str)
74 value: Message text (str)
77 def _FinishTag(tag, msg, result):
79 result[tag] = msg.rstrip()
84 my_data = pkg_resources.resource_string(__name__, 'missing-blob-help')
85 re_tag = re.compile('^([-a-z0-9]+):$')
89 for line in my_data.decode('utf-8').splitlines():
90 if not line.startswith('#'):
91 m_tag = re_tag.match(line)
93 _, msg = _FinishTag(tag, msg, result)
97 _FinishTag(tag, msg, result)
100 def _ShowBlobHelp(path, text):
101 tout.Warning('\n%s:' % path)
102 for line in text.splitlines():
103 tout.Warning(' %s' % line)
105 def _ShowHelpForMissingBlobs(missing_list):
106 """Show help for each missing blob to help the user take action
109 missing_list: List of Entry objects to show help for
111 global missing_blob_help
113 if not missing_blob_help:
114 missing_blob_help = _ReadMissingBlobHelp()
116 for entry in missing_list:
117 tags = entry.GetHelpTags()
119 # Show the first match help message
121 if tag in missing_blob_help:
122 _ShowBlobHelp(entry._node.path, missing_blob_help[tag])
125 def GetEntryModules(include_testing=True):
126 """Get a set of entry class implementations
129 Set of paths to entry class filenames
131 glob_list = pkg_resources.resource_listdir(__name__, 'etype')
132 glob_list = [fname for fname in glob_list if fname.endswith('.py')]
133 return set([os.path.splitext(os.path.basename(item))[0]
134 for item in glob_list
135 if include_testing or '_testing' not in item])
137 def WriteEntryDocs(modules, test_missing=None):
138 """Write out documentation for all entries
141 modules: List of Module objects to get docs for
142 test_missing: Used for testing only, to force an entry's documeentation
143 to show as missing even if it is present. Should be set to None in
146 from binman.entry import Entry
147 Entry.WriteDocs(modules, test_missing)
150 def ListEntries(image_fname, entry_paths):
151 """List the entries in an image
153 This decodes the supplied image and displays a table of entries from that
154 image, preceded by a header.
157 image_fname: Image filename to process
158 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
161 image = Image.FromFile(image_fname)
163 entries, lines, widths = image.GetListEntries(entry_paths)
165 num_columns = len(widths)
166 for linenum, line in enumerate(lines):
169 print('-' * (sum(widths) + num_columns * 2))
171 for i, item in enumerate(line):
173 if item.startswith('>'):
176 txt = '%*s ' % (width, item)
181 def ReadEntry(image_fname, entry_path, decomp=True):
182 """Extract an entry from an image
184 This extracts the data from a particular entry in an image
187 image_fname: Image filename to process
188 entry_path: Path to entry to extract
189 decomp: True to return uncompressed data, if the data is compress
190 False to return the raw data
193 data extracted from the entry
196 from binman.image import Image
198 image = Image.FromFile(image_fname)
199 entry = image.FindEntryPath(entry_path)
200 return entry.ReadData(decomp)
203 def ShowAltFormats(image):
204 """Show alternative formats available for entries in the image
206 This shows a list of formats available.
209 image (Image): Image to check
212 image.CheckAltFormats(alt_formats)
213 print('%-10s %-20s %s' % ('Flag (-F)', 'Entry type', 'Description'))
214 for name, val in alt_formats.items():
215 entry, helptext = val
216 print('%-10s %-20s %s' % (name, entry.etype, helptext))
219 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
220 decomp=True, alt_format=None):
221 """Extract the data from one or more entries and write it to files
224 image_fname: Image filename to process
225 output_fname: Single output filename to use if extracting one file, None
227 outdir: Output directory to use (for any number of files), else None
228 entry_paths: List of entry paths to extract
229 decomp: True to decompress the entry data
232 List of EntryInfo records that were written
234 image = Image.FromFile(image_fname)
236 if alt_format == 'list':
237 ShowAltFormats(image)
240 # Output an entry to a single file, as a special case
243 raise ValueError('Must specify an entry path to write with -f')
244 if len(entry_paths) != 1:
245 raise ValueError('Must specify exactly one entry path to write with -f')
246 entry = image.FindEntryPath(entry_paths[0])
247 data = entry.ReadData(decomp, alt_format)
248 tools.WriteFile(output_fname, data)
249 tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
252 # Otherwise we will output to a path given by the entry path of each entry.
253 # This means that entries will appear in subdirectories if they are part of
255 einfos = image.GetListEntries(entry_paths)[0]
256 tout.Notice('%d entries match and will be written' % len(einfos))
259 data = entry.ReadData(decomp, alt_format)
260 path = entry.GetPath()[1:]
261 fname = os.path.join(outdir, path)
263 # If this entry has children, create a directory for it and put its
264 # data in a file called 'root' in that directory
265 if entry.GetEntries():
266 if fname and not os.path.exists(fname):
268 fname = os.path.join(fname, 'root')
269 tout.Notice("Write entry '%s' size %x to '%s'" %
270 (entry.GetPath(), len(data), fname))
271 tools.WriteFile(fname, data)
275 def BeforeReplace(image, allow_resize):
276 """Handle getting an image ready for replacing entries in it
279 image: Image to prepare
281 state.PrepareFromLoadedData(image)
284 # If repacking, drop the old offset/size values except for the original
285 # ones, so we are only left with the constraints.
290 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
291 """Handle replacing a single entry an an image
294 image: Image to update
295 entry: Entry to write
296 data: Data to replace with
297 do_compress: True to compress the data if needed, False if data is
298 already compressed so should be used as is
299 allow_resize: True to allow entries to change size (this does a re-pack
300 of the entries), False to raise an exception
302 if not entry.WriteData(data, do_compress):
303 if not image.allow_repack:
304 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
306 entry.Raise('Entry data size does not match, but resize is disabled')
309 def AfterReplace(image, allow_resize, write_map):
310 """Handle write out an image after replacing entries in it
313 image: Image to write
314 allow_resize: True to allow entries to change size (this does a re-pack
315 of the entries), False to raise an exception
316 write_map: True to write a map file
318 tout.Info('Processing image')
319 ProcessImage(image, update_fdt=True, write_map=write_map,
320 get_contents=False, allow_resize=allow_resize)
323 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
325 BeforeReplace(image, allow_resize)
326 tout.Info('Writing data to %s' % entry.GetPath())
327 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
328 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
331 def WriteEntry(image_fname, entry_path, data, do_compress=True,
332 allow_resize=True, write_map=False):
333 """Replace an entry in an image
335 This replaces the data in a particular entry in an image. This size of the
336 new data must match the size of the old data unless allow_resize is True.
339 image_fname: Image filename to process
340 entry_path: Path to entry to extract
341 data: Data to replace with
342 do_compress: True to compress the data if needed, False if data is
343 already compressed so should be used as is
344 allow_resize: True to allow entries to change size (this does a re-pack
345 of the entries), False to raise an exception
346 write_map: True to write a map file
349 Image object that was updated
351 tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
352 image = Image.FromFile(image_fname)
353 entry = image.FindEntryPath(entry_path)
354 WriteEntryToImage(image, entry, data, do_compress=do_compress,
355 allow_resize=allow_resize, write_map=write_map)
360 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
361 do_compress=True, allow_resize=True, write_map=False):
362 """Replace the data from one or more entries from input files
365 image_fname: Image filename to process
366 input_fname: Single input filename to use if replacing one file, None
368 indir: Input directory to use (for any number of files), else None
369 entry_paths: List of entry paths to replace
370 do_compress: True if the input data is uncompressed and may need to be
371 compressed if the entry requires it, False if the data is already
373 write_map: True to write a map file
376 List of EntryInfo records that were written
378 image_fname = os.path.abspath(image_fname)
379 image = Image.FromFile(image_fname)
381 # Replace an entry from a single file, as a special case
384 raise ValueError('Must specify an entry path to read with -f')
385 if len(entry_paths) != 1:
386 raise ValueError('Must specify exactly one entry path to write with -f')
387 entry = image.FindEntryPath(entry_paths[0])
388 data = tools.ReadFile(input_fname)
389 tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
390 WriteEntryToImage(image, entry, data, do_compress=do_compress,
391 allow_resize=allow_resize, write_map=write_map)
394 # Otherwise we will input from a path given by the entry path of each entry.
395 # This means that files must appear in subdirectories if they are part of
397 einfos = image.GetListEntries(entry_paths)[0]
398 tout.Notice("Replacing %d matching entries in image '%s'" %
399 (len(einfos), image_fname))
401 BeforeReplace(image, allow_resize)
405 if entry.GetEntries():
406 tout.Info("Skipping section entry '%s'" % entry.GetPath())
409 path = entry.GetPath()[1:]
410 fname = os.path.join(indir, path)
412 if os.path.exists(fname):
413 tout.Notice("Write entry '%s' from file '%s'" %
414 (entry.GetPath(), fname))
415 data = tools.ReadFile(fname)
416 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
418 tout.Warning("Skipping entry '%s' from missing file '%s'" %
419 (entry.GetPath(), fname))
421 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
425 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded):
426 """Prepare the images to be processed and select the device tree
429 - reads in the device tree
430 - finds and scans the binman node to create all entries
431 - selects which images to build
432 - Updates the device tress with placeholder properties for offset,
436 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
437 selected_images: List of images to output, or None for all
438 update_fdt: True to update the FDT wth entry offsets, etc.
439 use_expanded: True to use expanded versions of entries, if available.
440 So if 'u-boot' is called for, we use 'u-boot-expanded' instead. This
441 is needed if update_fdt is True (although tests may disable it)
444 OrderedDict of images:
445 key: Image name (str)
448 # Import these here in case libfdt.py is not available, in which case
449 # the above help option still works.
451 from dtoc import fdt_util
454 # Get the device tree ready by compiling it and copying the compiled
455 # output into a file in our output directly. Then scan it for use
457 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
458 fname = tools.GetOutputFilename('u-boot.dtb.out')
459 tools.WriteFile(fname, tools.ReadFile(dtb_fname))
460 dtb = fdt.FdtScan(fname)
462 node = _FindBinmanNode(dtb)
464 raise ValueError("Device tree '%s' does not have a 'binman' "
467 images = _ReadImageDesc(node, use_expanded)
471 new_images = OrderedDict()
472 for name, image in images.items():
473 if name in select_images:
474 new_images[name] = image
478 tout.Notice('Skipping images: %s' % ', '.join(skip))
480 state.Prepare(images, dtb)
482 # Prepare the device tree by making sure that any missing
483 # properties are added (e.g. 'pos' and 'size'). The values of these
484 # may not be correct yet, but we add placeholders so that the
485 # size of the device tree is correct. Later, in
486 # SetCalculatedProperties() we will insert the correct values
487 # without changing the device-tree size, thus ensuring that our
488 # entry offsets remain the same.
489 for image in images.values():
490 image.ExpandEntries()
492 image.AddMissingProperties(True)
493 image.ProcessFdt(dtb)
495 for dtb_item in state.GetAllFdts():
496 dtb_item.Sync(auto_resize=True)
502 def ProcessImage(image, update_fdt, write_map, get_contents=True,
503 allow_resize=True, allow_missing=False):
504 """Perform all steps for this image, including checking and # writing it.
506 This means that errors found with a later image will be reported after
507 earlier images are already completed and written, but that does not seem
511 image: Image to process
512 update_fdt: True to update the FDT wth entry offsets, etc.
513 write_map: True to write a map file
514 get_contents: True to get the image contents from files, etc., False if
515 the contents is already present
516 allow_resize: True to allow entries to change size (this does a re-pack
517 of the entries), False to raise an exception
518 allow_missing: Allow blob_ext objects to be missing
521 True if one or more external blobs are missing, False if all are present
524 image.SetAllowMissing(allow_missing)
525 image.GetEntryContents()
526 image.GetEntryOffsets()
528 # We need to pack the entries to figure out where everything
529 # should be placed. This sets the offset/size of each entry.
530 # However, after packing we call ProcessEntryContents() which
531 # may result in an entry changing size. In that case we need to
532 # do another pass. Since the device tree often contains the
533 # final offset/size information we try to make space for this in
534 # AddMissingProperties() above. However, if the device is
535 # compressed we cannot know this compressed size in advance,
536 # since changing an offset from 0x100 to 0x104 (for example) can
537 # alter the compressed size of the device tree. So we need a
538 # third pass for this.
540 for pack_pass in range(passes):
543 except Exception as e:
545 fname = image.WriteMap()
546 print("Wrote map file '%s' to show errors" % fname)
550 image.SetCalculatedProperties()
551 for dtb_item in state.GetAllFdts():
555 sizes_ok = image.ProcessEntryContents()
559 tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
561 image.Raise('Entries changed size after packing (tried %s passes)' %
568 image.CheckMissing(missing_list)
570 tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" %
571 (image.name, ' '.join([e.name for e in missing_list])))
572 _ShowHelpForMissingBlobs(missing_list)
573 return bool(missing_list)
577 """The main control code for binman
579 This assumes that help and test options have already been dealt with. It
580 deals with the core task of building images.
583 args: Command line arguments Namespace object
590 os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'README.rst')
594 # Put these here so that we can import this module without libfdt
595 from binman.image import Image
596 from binman import state
598 if args.cmd in ['ls', 'extract', 'replace']:
600 tout.Init(args.verbosity)
601 tools.PrepareOutputDir(None)
603 ListEntries(args.image, args.paths)
605 if args.cmd == 'extract':
606 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
607 not args.uncompressed, args.format)
609 if args.cmd == 'replace':
610 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
611 do_compress=not args.compressed,
612 allow_resize=not args.fix_size, write_map=args.map)
616 tools.FinaliseOutputDir()
620 if args.update_fdt_in_elf:
621 elf_params = args.update_fdt_in_elf.split(',')
622 if len(elf_params) != 4:
623 raise ValueError('Invalid args %s to --update-fdt-in-elf: expected infile,outfile,begin_sym,end_sym' %
626 # Try to figure out which device tree contains our image description
632 raise ValueError('Must provide a board to process (use -b <board>)')
633 board_pathname = os.path.join(args.build_dir, board)
634 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
637 args.indir.append(board_pathname)
640 tout.Init(args.verbosity)
641 elf.debug = args.debug
642 cbfs_util.VERBOSE = args.verbosity > 2
643 state.use_fake_dtb = args.fake_dtb
645 # Normally we replace the 'u-boot' etype with 'u-boot-expanded', etc.
646 # When running tests this can be disabled using this flag. When not
647 # updating the FDT in image, it is not needed by binman, but we use it
648 # for consistency, so that the images look the same to U-Boot at
650 use_expanded = not args.no_expanded
652 tools.SetInputDirs(args.indir)
653 tools.PrepareOutputDir(args.outdir, args.preserve)
654 tools.SetToolPaths(args.toolpath)
655 state.SetEntryArgs(args.entry_arg)
656 state.SetThreads(args.threads)
658 images = PrepareImagesAndDtbs(dtb_fname, args.image,
659 args.update_fdt, use_expanded)
660 if args.test_section_timeout:
661 # Set the first image to timeout, used in testThreadTimeout()
662 images[list(images.keys())[0]].test_section_timeout = True
664 for image in images.values():
665 missing |= ProcessImage(image, args.update_fdt, args.map,
666 allow_missing=args.allow_missing)
668 # Write the updated FDTs to our output files
669 for dtb_item in state.GetAllFdts():
670 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
673 data = state.GetFdtForEtype('u-boot-dtb').GetContents()
674 elf.UpdateFile(*elf_params, data)
677 tout.Warning("\nSome images are invalid")
679 # Use this to debug the time take to pack the image
682 tools.FinaliseOutputDir()