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
12 from patman import tools
14 from binman import cbfs_util
15 from binman import elf
16 from patman import command
17 from patman import tout
19 # List of images we plan to create
20 # Make this global so that it can be referenced from tests
21 images = OrderedDict()
23 def _ReadImageDesc(binman_node):
24 """Read the image descriptions from the /binman node
26 This normally produces a single Image object called 'image'. But if
27 multiple images are present, they will all be returned.
30 binman_node: Node object of the /binman node
32 OrderedDict of Image objects, each of which describes an image
34 images = OrderedDict()
35 if 'multiple-images' in binman_node.props:
36 for node in binman_node.subnodes:
37 images[node.name] = Image(node.name, node)
39 images['image'] = Image('image', binman_node)
42 def _FindBinmanNode(dtb):
43 """Find the 'binman' node in the device tree
46 dtb: Fdt object to scan
48 Node object of /binman node, or None if not found
50 for node in dtb.GetRoot().subnodes:
51 if node.name == 'binman':
55 def GetEntryModules(include_testing=True):
56 """Get a set of entry class implementations
59 Set of paths to entry class filenames
61 our_path = os.path.dirname(os.path.realpath(__file__))
62 glob_list = glob.glob(os.path.join(our_path, 'etype/*.py'))
63 return set([os.path.splitext(os.path.basename(item))[0]
65 if include_testing or '_testing' not in item])
67 def WriteEntryDocs(modules, test_missing=None):
68 """Write out documentation for all entries
71 modules: List of Module objects to get docs for
72 test_missing: Used for testing only, to force an entry's documeentation
73 to show as missing even if it is present. Should be set to None in
76 from binman.entry import Entry
77 Entry.WriteDocs(modules, test_missing)
80 def ListEntries(image_fname, entry_paths):
81 """List the entries in an image
83 This decodes the supplied image and displays a table of entries from that
84 image, preceded by a header.
87 image_fname: Image filename to process
88 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
91 image = Image.FromFile(image_fname)
93 entries, lines, widths = image.GetListEntries(entry_paths)
95 num_columns = len(widths)
96 for linenum, line in enumerate(lines):
99 print('-' * (sum(widths) + num_columns * 2))
101 for i, item in enumerate(line):
103 if item.startswith('>'):
106 txt = '%*s ' % (width, item)
111 def ReadEntry(image_fname, entry_path, decomp=True):
112 """Extract an entry from an image
114 This extracts the data from a particular entry in an image
117 image_fname: Image filename to process
118 entry_path: Path to entry to extract
119 decomp: True to return uncompressed data, if the data is compress
120 False to return the raw data
123 data extracted from the entry
126 from binman.image import Image
128 image = Image.FromFile(image_fname)
129 entry = image.FindEntryPath(entry_path)
130 return entry.ReadData(decomp)
133 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
135 """Extract the data from one or more entries and write it to files
138 image_fname: Image filename to process
139 output_fname: Single output filename to use if extracting one file, None
141 outdir: Output directory to use (for any number of files), else None
142 entry_paths: List of entry paths to extract
143 decomp: True to decompress the entry data
146 List of EntryInfo records that were written
148 image = Image.FromFile(image_fname)
150 # Output an entry to a single file, as a special case
153 raise ValueError('Must specify an entry path to write with -f')
154 if len(entry_paths) != 1:
155 raise ValueError('Must specify exactly one entry path to write with -f')
156 entry = image.FindEntryPath(entry_paths[0])
157 data = entry.ReadData(decomp)
158 tools.WriteFile(output_fname, data)
159 tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
162 # Otherwise we will output to a path given by the entry path of each entry.
163 # This means that entries will appear in subdirectories if they are part of
165 einfos = image.GetListEntries(entry_paths)[0]
166 tout.Notice('%d entries match and will be written' % len(einfos))
169 data = entry.ReadData(decomp)
170 path = entry.GetPath()[1:]
171 fname = os.path.join(outdir, path)
173 # If this entry has children, create a directory for it and put its
174 # data in a file called 'root' in that directory
175 if entry.GetEntries():
176 if not os.path.exists(fname):
178 fname = os.path.join(fname, 'root')
179 tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname))
180 tools.WriteFile(fname, data)
184 def BeforeReplace(image, allow_resize):
185 """Handle getting an image ready for replacing entries in it
188 image: Image to prepare
190 state.PrepareFromLoadedData(image)
193 # If repacking, drop the old offset/size values except for the original
194 # ones, so we are only left with the constraints.
199 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
200 """Handle replacing a single entry an an image
203 image: Image to update
204 entry: Entry to write
205 data: Data to replace with
206 do_compress: True to compress the data if needed, False if data is
207 already compressed so should be used as is
208 allow_resize: True to allow entries to change size (this does a re-pack
209 of the entries), False to raise an exception
211 if not entry.WriteData(data, do_compress):
212 if not image.allow_repack:
213 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
215 entry.Raise('Entry data size does not match, but resize is disabled')
218 def AfterReplace(image, allow_resize, write_map):
219 """Handle write out an image after replacing entries in it
222 image: Image to write
223 allow_resize: True to allow entries to change size (this does a re-pack
224 of the entries), False to raise an exception
225 write_map: True to write a map file
227 tout.Info('Processing image')
228 ProcessImage(image, update_fdt=True, write_map=write_map,
229 get_contents=False, allow_resize=allow_resize)
232 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
234 BeforeReplace(image, allow_resize)
235 tout.Info('Writing data to %s' % entry.GetPath())
236 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
237 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
240 def WriteEntry(image_fname, entry_path, data, do_compress=True,
241 allow_resize=True, write_map=False):
242 """Replace an entry in an image
244 This replaces the data in a particular entry in an image. This size of the
245 new data must match the size of the old data unless allow_resize is True.
248 image_fname: Image filename to process
249 entry_path: Path to entry to extract
250 data: Data to replace with
251 do_compress: True to compress the data if needed, False if data is
252 already compressed so should be used as is
253 allow_resize: True to allow entries to change size (this does a re-pack
254 of the entries), False to raise an exception
255 write_map: True to write a map file
258 Image object that was updated
260 tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
261 image = Image.FromFile(image_fname)
262 entry = image.FindEntryPath(entry_path)
263 WriteEntryToImage(image, entry, data, do_compress=do_compress,
264 allow_resize=allow_resize, write_map=write_map)
269 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
270 do_compress=True, allow_resize=True, write_map=False):
271 """Replace the data from one or more entries from input files
274 image_fname: Image filename to process
275 input_fname: Single input ilename to use if replacing one file, None
277 indir: Input directory to use (for any number of files), else None
278 entry_paths: List of entry paths to extract
279 do_compress: True if the input data is uncompressed and may need to be
280 compressed if the entry requires it, False if the data is already
282 write_map: True to write a map file
285 List of EntryInfo records that were written
287 image = Image.FromFile(image_fname)
289 # Replace an entry from a single file, as a special case
292 raise ValueError('Must specify an entry path to read with -f')
293 if len(entry_paths) != 1:
294 raise ValueError('Must specify exactly one entry path to write with -f')
295 entry = image.FindEntryPath(entry_paths[0])
296 data = tools.ReadFile(input_fname)
297 tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
298 WriteEntryToImage(image, entry, data, do_compress=do_compress,
299 allow_resize=allow_resize, write_map=write_map)
302 # Otherwise we will input from a path given by the entry path of each entry.
303 # This means that files must appear in subdirectories if they are part of
305 einfos = image.GetListEntries(entry_paths)[0]
306 tout.Notice("Replacing %d matching entries in image '%s'" %
307 (len(einfos), image_fname))
309 BeforeReplace(image, allow_resize)
313 if entry.GetEntries():
314 tout.Info("Skipping section entry '%s'" % entry.GetPath())
317 path = entry.GetPath()[1:]
318 fname = os.path.join(indir, path)
320 if os.path.exists(fname):
321 tout.Notice("Write entry '%s' from file '%s'" %
322 (entry.GetPath(), fname))
323 data = tools.ReadFile(fname)
324 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
326 tout.Warning("Skipping entry '%s' from missing file '%s'" %
327 (entry.GetPath(), fname))
329 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
333 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
334 """Prepare the images to be processed and select the device tree
337 - reads in the device tree
338 - finds and scans the binman node to create all entries
339 - selects which images to build
340 - Updates the device tress with placeholder properties for offset,
344 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
345 selected_images: List of images to output, or None for all
346 update_fdt: True to update the FDT wth entry offsets, etc.
348 # Import these here in case libfdt.py is not available, in which case
349 # the above help option still works.
351 from dtoc import fdt_util
354 # Get the device tree ready by compiling it and copying the compiled
355 # output into a file in our output directly. Then scan it for use
357 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
358 fname = tools.GetOutputFilename('u-boot.dtb.out')
359 tools.WriteFile(fname, tools.ReadFile(dtb_fname))
360 dtb = fdt.FdtScan(fname)
362 node = _FindBinmanNode(dtb)
364 raise ValueError("Device tree '%s' does not have a 'binman' "
367 images = _ReadImageDesc(node)
371 new_images = OrderedDict()
372 for name, image in images.items():
373 if name in select_images:
374 new_images[name] = image
378 tout.Notice('Skipping images: %s' % ', '.join(skip))
380 state.Prepare(images, dtb)
382 # Prepare the device tree by making sure that any missing
383 # properties are added (e.g. 'pos' and 'size'). The values of these
384 # may not be correct yet, but we add placeholders so that the
385 # size of the device tree is correct. Later, in
386 # SetCalculatedProperties() we will insert the correct values
387 # without changing the device-tree size, thus ensuring that our
388 # entry offsets remain the same.
389 for image in images.values():
390 image.ExpandEntries()
392 image.AddMissingProperties()
393 image.ProcessFdt(dtb)
395 for dtb_item in state.GetAllFdts():
396 dtb_item.Sync(auto_resize=True)
402 def ProcessImage(image, update_fdt, write_map, get_contents=True,
403 allow_resize=True, allow_missing=False):
404 """Perform all steps for this image, including checking and # writing it.
406 This means that errors found with a later image will be reported after
407 earlier images are already completed and written, but that does not seem
411 image: Image to process
412 update_fdt: True to update the FDT wth entry offsets, etc.
413 write_map: True to write a map file
414 get_contents: True to get the image contents from files, etc., False if
415 the contents is already present
416 allow_resize: True to allow entries to change size (this does a re-pack
417 of the entries), False to raise an exception
418 allow_missing: Allow blob_ext objects to be missing
421 True if one or more external blobs are missing, False if all are present
424 image.SetAllowMissing(allow_missing)
425 image.GetEntryContents()
426 image.GetEntryOffsets()
428 # We need to pack the entries to figure out where everything
429 # should be placed. This sets the offset/size of each entry.
430 # However, after packing we call ProcessEntryContents() which
431 # may result in an entry changing size. In that case we need to
432 # do another pass. Since the device tree often contains the
433 # final offset/size information we try to make space for this in
434 # AddMissingProperties() above. However, if the device is
435 # compressed we cannot know this compressed size in advance,
436 # since changing an offset from 0x100 to 0x104 (for example) can
437 # alter the compressed size of the device tree. So we need a
438 # third pass for this.
440 for pack_pass in range(passes):
445 except Exception as e:
447 fname = image.WriteMap()
448 print("Wrote map file '%s' to show errors" % fname)
452 image.SetCalculatedProperties()
453 for dtb_item in state.GetAllFdts():
457 sizes_ok = image.ProcessEntryContents()
461 tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
463 image.Raise('Entries changed size after packing (tried %s passes)' %
470 image.CheckMissing(missing_list)
472 tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" %
473 (image.name, ' '.join([e.name for e in missing_list])))
474 return bool(missing_list)
478 """The main control code for binman
480 This assumes that help and test options have already been dealt with. It
481 deals with the core task of building images.
484 args: Command line arguments Namespace object
490 pager = os.getenv('PAGER')
493 fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
495 command.Run(pager, fname)
498 # Put these here so that we can import this module without libfdt
499 from binman.image import Image
500 from binman import state
502 if args.cmd in ['ls', 'extract', 'replace']:
504 tout.Init(args.verbosity)
505 tools.PrepareOutputDir(None)
507 ListEntries(args.image, args.paths)
509 if args.cmd == 'extract':
510 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
511 not args.uncompressed)
513 if args.cmd == 'replace':
514 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
515 do_compress=not args.compressed,
516 allow_resize=not args.fix_size, write_map=args.map)
520 tools.FinaliseOutputDir()
523 # Try to figure out which device tree contains our image description
529 raise ValueError('Must provide a board to process (use -b <board>)')
530 board_pathname = os.path.join(args.build_dir, board)
531 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
534 args.indir.append(board_pathname)
537 tout.Init(args.verbosity)
538 elf.debug = args.debug
539 cbfs_util.VERBOSE = args.verbosity > 2
540 state.use_fake_dtb = args.fake_dtb
542 tools.SetInputDirs(args.indir)
543 tools.PrepareOutputDir(args.outdir, args.preserve)
544 tools.SetToolPaths(args.toolpath)
545 state.SetEntryArgs(args.entry_arg)
547 images = PrepareImagesAndDtbs(dtb_fname, args.image,
550 for image in images.values():
551 missing |= ProcessImage(image, args.update_fdt, args.map,
552 allow_missing=args.allow_missing)
554 # Write the updated FDTs to our output files
555 for dtb_item in state.GetAllFdts():
556 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
559 tout.Warning("Some images are invalid")
561 tools.FinaliseOutputDir()