1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
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 ExtractEntries(image_fname, output_fname, outdir, entry_paths,
205 """Extract the data from one or more entries and write it to files
208 image_fname: Image filename to process
209 output_fname: Single output filename to use if extracting one file, None
211 outdir: Output directory to use (for any number of files), else None
212 entry_paths: List of entry paths to extract
213 decomp: True to decompress the entry data
216 List of EntryInfo records that were written
218 image = Image.FromFile(image_fname)
220 # Output an entry to a single file, as a special case
223 raise ValueError('Must specify an entry path to write with -f')
224 if len(entry_paths) != 1:
225 raise ValueError('Must specify exactly one entry path to write with -f')
226 entry = image.FindEntryPath(entry_paths[0])
227 data = entry.ReadData(decomp)
228 tools.WriteFile(output_fname, data)
229 tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
232 # Otherwise we will output to a path given by the entry path of each entry.
233 # This means that entries will appear in subdirectories if they are part of
235 einfos = image.GetListEntries(entry_paths)[0]
236 tout.Notice('%d entries match and will be written' % len(einfos))
239 data = entry.ReadData(decomp)
240 path = entry.GetPath()[1:]
241 fname = os.path.join(outdir, path)
243 # If this entry has children, create a directory for it and put its
244 # data in a file called 'root' in that directory
245 if entry.GetEntries():
246 if fname and not os.path.exists(fname):
248 fname = os.path.join(fname, 'root')
249 tout.Notice("Write entry '%s' size %x to '%s'" %
250 (entry.GetPath(), len(data), fname))
251 tools.WriteFile(fname, data)
255 def BeforeReplace(image, allow_resize):
256 """Handle getting an image ready for replacing entries in it
259 image: Image to prepare
261 state.PrepareFromLoadedData(image)
264 # If repacking, drop the old offset/size values except for the original
265 # ones, so we are only left with the constraints.
270 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
271 """Handle replacing a single entry an an image
274 image: Image to update
275 entry: Entry to write
276 data: Data to replace with
277 do_compress: True to compress the data if needed, False if data is
278 already compressed so should be used as is
279 allow_resize: True to allow entries to change size (this does a re-pack
280 of the entries), False to raise an exception
282 if not entry.WriteData(data, do_compress):
283 if not image.allow_repack:
284 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
286 entry.Raise('Entry data size does not match, but resize is disabled')
289 def AfterReplace(image, allow_resize, write_map):
290 """Handle write out an image after replacing entries in it
293 image: Image to write
294 allow_resize: True to allow entries to change size (this does a re-pack
295 of the entries), False to raise an exception
296 write_map: True to write a map file
298 tout.Info('Processing image')
299 ProcessImage(image, update_fdt=True, write_map=write_map,
300 get_contents=False, allow_resize=allow_resize)
303 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
305 BeforeReplace(image, allow_resize)
306 tout.Info('Writing data to %s' % entry.GetPath())
307 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
308 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
311 def WriteEntry(image_fname, entry_path, data, do_compress=True,
312 allow_resize=True, write_map=False):
313 """Replace an entry in an image
315 This replaces the data in a particular entry in an image. This size of the
316 new data must match the size of the old data unless allow_resize is True.
319 image_fname: Image filename to process
320 entry_path: Path to entry to extract
321 data: Data to replace with
322 do_compress: True to compress the data if needed, False if data is
323 already compressed so should be used as is
324 allow_resize: True to allow entries to change size (this does a re-pack
325 of the entries), False to raise an exception
326 write_map: True to write a map file
329 Image object that was updated
331 tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
332 image = Image.FromFile(image_fname)
333 entry = image.FindEntryPath(entry_path)
334 WriteEntryToImage(image, entry, data, do_compress=do_compress,
335 allow_resize=allow_resize, write_map=write_map)
340 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
341 do_compress=True, allow_resize=True, write_map=False):
342 """Replace the data from one or more entries from input files
345 image_fname: Image filename to process
346 input_fname: Single input ilename to use if replacing one file, None
348 indir: Input directory to use (for any number of files), else None
349 entry_paths: List of entry paths to extract
350 do_compress: True if the input data is uncompressed and may need to be
351 compressed if the entry requires it, False if the data is already
353 write_map: True to write a map file
356 List of EntryInfo records that were written
358 image = Image.FromFile(image_fname)
360 # Replace an entry from a single file, as a special case
363 raise ValueError('Must specify an entry path to read with -f')
364 if len(entry_paths) != 1:
365 raise ValueError('Must specify exactly one entry path to write with -f')
366 entry = image.FindEntryPath(entry_paths[0])
367 data = tools.ReadFile(input_fname)
368 tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
369 WriteEntryToImage(image, entry, data, do_compress=do_compress,
370 allow_resize=allow_resize, write_map=write_map)
373 # Otherwise we will input from a path given by the entry path of each entry.
374 # This means that files must appear in subdirectories if they are part of
376 einfos = image.GetListEntries(entry_paths)[0]
377 tout.Notice("Replacing %d matching entries in image '%s'" %
378 (len(einfos), image_fname))
380 BeforeReplace(image, allow_resize)
384 if entry.GetEntries():
385 tout.Info("Skipping section entry '%s'" % entry.GetPath())
388 path = entry.GetPath()[1:]
389 fname = os.path.join(indir, path)
391 if os.path.exists(fname):
392 tout.Notice("Write entry '%s' from file '%s'" %
393 (entry.GetPath(), fname))
394 data = tools.ReadFile(fname)
395 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
397 tout.Warning("Skipping entry '%s' from missing file '%s'" %
398 (entry.GetPath(), fname))
400 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
404 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded):
405 """Prepare the images to be processed and select the device tree
408 - reads in the device tree
409 - finds and scans the binman node to create all entries
410 - selects which images to build
411 - Updates the device tress with placeholder properties for offset,
415 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
416 selected_images: List of images to output, or None for all
417 update_fdt: True to update the FDT wth entry offsets, etc.
418 use_expanded: True to use expanded versions of entries, if available.
419 So if 'u-boot' is called for, we use 'u-boot-expanded' instead. This
420 is needed if update_fdt is True (although tests may disable it)
423 OrderedDict of images:
424 key: Image name (str)
427 # Import these here in case libfdt.py is not available, in which case
428 # the above help option still works.
430 from dtoc import fdt_util
433 # Get the device tree ready by compiling it and copying the compiled
434 # output into a file in our output directly. Then scan it for use
436 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
437 fname = tools.GetOutputFilename('u-boot.dtb.out')
438 tools.WriteFile(fname, tools.ReadFile(dtb_fname))
439 dtb = fdt.FdtScan(fname)
441 node = _FindBinmanNode(dtb)
443 raise ValueError("Device tree '%s' does not have a 'binman' "
446 images = _ReadImageDesc(node, use_expanded)
450 new_images = OrderedDict()
451 for name, image in images.items():
452 if name in select_images:
453 new_images[name] = image
457 tout.Notice('Skipping images: %s' % ', '.join(skip))
459 state.Prepare(images, dtb)
461 # Prepare the device tree by making sure that any missing
462 # properties are added (e.g. 'pos' and 'size'). The values of these
463 # may not be correct yet, but we add placeholders so that the
464 # size of the device tree is correct. Later, in
465 # SetCalculatedProperties() we will insert the correct values
466 # without changing the device-tree size, thus ensuring that our
467 # entry offsets remain the same.
468 for image in images.values():
469 image.ExpandEntries()
471 image.AddMissingProperties(True)
472 image.ProcessFdt(dtb)
474 for dtb_item in state.GetAllFdts():
475 dtb_item.Sync(auto_resize=True)
481 def ProcessImage(image, update_fdt, write_map, get_contents=True,
482 allow_resize=True, allow_missing=False):
483 """Perform all steps for this image, including checking and # writing it.
485 This means that errors found with a later image will be reported after
486 earlier images are already completed and written, but that does not seem
490 image: Image to process
491 update_fdt: True to update the FDT wth entry offsets, etc.
492 write_map: True to write a map file
493 get_contents: True to get the image contents from files, etc., False if
494 the contents is already present
495 allow_resize: True to allow entries to change size (this does a re-pack
496 of the entries), False to raise an exception
497 allow_missing: Allow blob_ext objects to be missing
500 True if one or more external blobs are missing, False if all are present
503 image.SetAllowMissing(allow_missing)
504 image.GetEntryContents()
505 image.GetEntryOffsets()
507 # We need to pack the entries to figure out where everything
508 # should be placed. This sets the offset/size of each entry.
509 # However, after packing we call ProcessEntryContents() which
510 # may result in an entry changing size. In that case we need to
511 # do another pass. Since the device tree often contains the
512 # final offset/size information we try to make space for this in
513 # AddMissingProperties() above. However, if the device is
514 # compressed we cannot know this compressed size in advance,
515 # since changing an offset from 0x100 to 0x104 (for example) can
516 # alter the compressed size of the device tree. So we need a
517 # third pass for this.
519 for pack_pass in range(passes):
522 except Exception as e:
524 fname = image.WriteMap()
525 print("Wrote map file '%s' to show errors" % fname)
529 image.SetCalculatedProperties()
530 for dtb_item in state.GetAllFdts():
534 sizes_ok = image.ProcessEntryContents()
538 tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
540 image.Raise('Entries changed size after packing (tried %s passes)' %
547 image.CheckMissing(missing_list)
549 tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" %
550 (image.name, ' '.join([e.name for e in missing_list])))
551 _ShowHelpForMissingBlobs(missing_list)
552 return bool(missing_list)
556 """The main control code for binman
558 This assumes that help and test options have already been dealt with. It
559 deals with the core task of building images.
562 args: Command line arguments Namespace object
569 os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'README.rst')
573 # Put these here so that we can import this module without libfdt
574 from binman.image import Image
575 from binman import state
577 if args.cmd in ['ls', 'extract', 'replace']:
579 tout.Init(args.verbosity)
580 tools.PrepareOutputDir(None)
582 ListEntries(args.image, args.paths)
584 if args.cmd == 'extract':
585 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
586 not args.uncompressed)
588 if args.cmd == 'replace':
589 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
590 do_compress=not args.compressed,
591 allow_resize=not args.fix_size, write_map=args.map)
595 tools.FinaliseOutputDir()
598 # Try to figure out which device tree contains our image description
604 raise ValueError('Must provide a board to process (use -b <board>)')
605 board_pathname = os.path.join(args.build_dir, board)
606 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
609 args.indir.append(board_pathname)
612 tout.Init(args.verbosity)
613 elf.debug = args.debug
614 cbfs_util.VERBOSE = args.verbosity > 2
615 state.use_fake_dtb = args.fake_dtb
617 # Normally we replace the 'u-boot' etype with 'u-boot-expanded', etc.
618 # When running tests this can be disabled using this flag. When not
619 # updating the FDT in image, it is not needed by binman, but we use it
620 # for consistency, so that the images look the same to U-Boot at
622 use_expanded = not args.no_expanded
624 tools.SetInputDirs(args.indir)
625 tools.PrepareOutputDir(args.outdir, args.preserve)
626 tools.SetToolPaths(args.toolpath)
627 state.SetEntryArgs(args.entry_arg)
628 state.SetThreads(args.threads)
630 images = PrepareImagesAndDtbs(dtb_fname, args.image,
631 args.update_fdt, use_expanded)
632 if args.test_section_timeout:
633 # Set the first image to timeout, used in testThreadTimeout()
634 images[list(images.keys())[0]].test_section_timeout = True
636 for image in images.values():
637 missing |= ProcessImage(image, args.update_fdt, args.map,
638 allow_missing=args.allow_missing)
640 # Write the updated FDTs to our output files
641 for dtb_item in state.GetAllFdts():
642 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
645 tout.Warning("\nSome images are invalid")
647 # Use this to debug the time take to pack the image
650 tools.FinaliseOutputDir()