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 from patman import tools
13 from binman import cbfs_util
14 from binman import elf
15 from patman import command
16 from patman import tout
18 # List of images we plan to create
19 # Make this global so that it can be referenced from tests
20 images = OrderedDict()
22 def _ReadImageDesc(binman_node):
23 """Read the image descriptions from the /binman node
25 This normally produces a single Image object called 'image'. But if
26 multiple images are present, they will all be returned.
29 binman_node: Node object of the /binman node
31 OrderedDict of Image objects, each of which describes an image
33 images = OrderedDict()
34 if 'multiple-images' in binman_node.props:
35 for node in binman_node.subnodes:
36 images[node.name] = Image(node.name, node)
38 images['image'] = Image('image', binman_node)
41 def _FindBinmanNode(dtb):
42 """Find the 'binman' node in the device tree
45 dtb: Fdt object to scan
47 Node object of /binman node, or None if not found
49 for node in dtb.GetRoot().subnodes:
50 if node.name == 'binman':
54 def WriteEntryDocs(modules, test_missing=None):
55 """Write out documentation for all entries
58 modules: List of Module objects to get docs for
59 test_missing: Used for testing only, to force an entry's documeentation
60 to show as missing even if it is present. Should be set to None in
63 from binman.entry import Entry
64 Entry.WriteDocs(modules, test_missing)
67 def ListEntries(image_fname, entry_paths):
68 """List the entries in an image
70 This decodes the supplied image and displays a table of entries from that
71 image, preceded by a header.
74 image_fname: Image filename to process
75 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
78 image = Image.FromFile(image_fname)
80 entries, lines, widths = image.GetListEntries(entry_paths)
82 num_columns = len(widths)
83 for linenum, line in enumerate(lines):
86 print('-' * (sum(widths) + num_columns * 2))
88 for i, item in enumerate(line):
90 if item.startswith('>'):
93 txt = '%*s ' % (width, item)
98 def ReadEntry(image_fname, entry_path, decomp=True):
99 """Extract an entry from an image
101 This extracts the data from a particular entry in an image
104 image_fname: Image filename to process
105 entry_path: Path to entry to extract
106 decomp: True to return uncompressed data, if the data is compress
107 False to return the raw data
110 data extracted from the entry
113 from image import Image
115 image = Image.FromFile(image_fname)
116 entry = image.FindEntryPath(entry_path)
117 return entry.ReadData(decomp)
120 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
122 """Extract the data from one or more entries and write it to files
125 image_fname: Image filename to process
126 output_fname: Single output filename to use if extracting one file, None
128 outdir: Output directory to use (for any number of files), else None
129 entry_paths: List of entry paths to extract
130 decomp: True to decompress the entry data
133 List of EntryInfo records that were written
135 image = Image.FromFile(image_fname)
137 # Output an entry to a single file, as a special case
140 raise ValueError('Must specify an entry path to write with -f')
141 if len(entry_paths) != 1:
142 raise ValueError('Must specify exactly one entry path to write with -f')
143 entry = image.FindEntryPath(entry_paths[0])
144 data = entry.ReadData(decomp)
145 tools.WriteFile(output_fname, data)
146 tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
149 # Otherwise we will output to a path given by the entry path of each entry.
150 # This means that entries will appear in subdirectories if they are part of
152 einfos = image.GetListEntries(entry_paths)[0]
153 tout.Notice('%d entries match and will be written' % len(einfos))
156 data = entry.ReadData(decomp)
157 path = entry.GetPath()[1:]
158 fname = os.path.join(outdir, path)
160 # If this entry has children, create a directory for it and put its
161 # data in a file called 'root' in that directory
162 if entry.GetEntries():
163 if not os.path.exists(fname):
165 fname = os.path.join(fname, 'root')
166 tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname))
167 tools.WriteFile(fname, data)
171 def BeforeReplace(image, allow_resize):
172 """Handle getting an image ready for replacing entries in it
175 image: Image to prepare
177 state.PrepareFromLoadedData(image)
180 # If repacking, drop the old offset/size values except for the original
181 # ones, so we are only left with the constraints.
186 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
187 """Handle replacing a single entry an an image
190 image: Image to update
191 entry: Entry to write
192 data: Data to replace with
193 do_compress: True to compress the data if needed, False if data is
194 already compressed so should be used as is
195 allow_resize: True to allow entries to change size (this does a re-pack
196 of the entries), False to raise an exception
198 if not entry.WriteData(data, do_compress):
199 if not image.allow_repack:
200 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
202 entry.Raise('Entry data size does not match, but resize is disabled')
205 def AfterReplace(image, allow_resize, write_map):
206 """Handle write out an image after replacing entries in it
209 image: Image to write
210 allow_resize: True to allow entries to change size (this does a re-pack
211 of the entries), False to raise an exception
212 write_map: True to write a map file
214 tout.Info('Processing image')
215 ProcessImage(image, update_fdt=True, write_map=write_map,
216 get_contents=False, allow_resize=allow_resize)
219 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
221 BeforeReplace(image, allow_resize)
222 tout.Info('Writing data to %s' % entry.GetPath())
223 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
224 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
227 def WriteEntry(image_fname, entry_path, data, do_compress=True,
228 allow_resize=True, write_map=False):
229 """Replace an entry in an image
231 This replaces the data in a particular entry in an image. This size of the
232 new data must match the size of the old data unless allow_resize is True.
235 image_fname: Image filename to process
236 entry_path: Path to entry to extract
237 data: Data to replace with
238 do_compress: True to compress the data if needed, False if data is
239 already compressed so should be used as is
240 allow_resize: True to allow entries to change size (this does a re-pack
241 of the entries), False to raise an exception
242 write_map: True to write a map file
245 Image object that was updated
247 tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
248 image = Image.FromFile(image_fname)
249 entry = image.FindEntryPath(entry_path)
250 WriteEntryToImage(image, entry, data, do_compress=do_compress,
251 allow_resize=allow_resize, write_map=write_map)
256 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
257 do_compress=True, allow_resize=True, write_map=False):
258 """Replace the data from one or more entries from input files
261 image_fname: Image filename to process
262 input_fname: Single input ilename to use if replacing one file, None
264 indir: Input directory to use (for any number of files), else None
265 entry_paths: List of entry paths to extract
266 do_compress: True if the input data is uncompressed and may need to be
267 compressed if the entry requires it, False if the data is already
269 write_map: True to write a map file
272 List of EntryInfo records that were written
274 image = Image.FromFile(image_fname)
276 # Replace an entry from a single file, as a special case
279 raise ValueError('Must specify an entry path to read with -f')
280 if len(entry_paths) != 1:
281 raise ValueError('Must specify exactly one entry path to write with -f')
282 entry = image.FindEntryPath(entry_paths[0])
283 data = tools.ReadFile(input_fname)
284 tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
285 WriteEntryToImage(image, entry, data, do_compress=do_compress,
286 allow_resize=allow_resize, write_map=write_map)
289 # Otherwise we will input from a path given by the entry path of each entry.
290 # This means that files must appear in subdirectories if they are part of
292 einfos = image.GetListEntries(entry_paths)[0]
293 tout.Notice("Replacing %d matching entries in image '%s'" %
294 (len(einfos), image_fname))
296 BeforeReplace(image, allow_resize)
300 if entry.GetEntries():
301 tout.Info("Skipping section entry '%s'" % entry.GetPath())
304 path = entry.GetPath()[1:]
305 fname = os.path.join(indir, path)
307 if os.path.exists(fname):
308 tout.Notice("Write entry '%s' from file '%s'" %
309 (entry.GetPath(), fname))
310 data = tools.ReadFile(fname)
311 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
313 tout.Warning("Skipping entry '%s' from missing file '%s'" %
314 (entry.GetPath(), fname))
316 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
320 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
321 """Prepare the images to be processed and select the device tree
324 - reads in the device tree
325 - finds and scans the binman node to create all entries
326 - selects which images to build
327 - Updates the device tress with placeholder properties for offset,
331 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
332 selected_images: List of images to output, or None for all
333 update_fdt: True to update the FDT wth entry offsets, etc.
335 # Import these here in case libfdt.py is not available, in which case
336 # the above help option still works.
338 from dtoc import fdt_util
341 # Get the device tree ready by compiling it and copying the compiled
342 # output into a file in our output directly. Then scan it for use
344 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
345 fname = tools.GetOutputFilename('u-boot.dtb.out')
346 tools.WriteFile(fname, tools.ReadFile(dtb_fname))
347 dtb = fdt.FdtScan(fname)
349 node = _FindBinmanNode(dtb)
351 raise ValueError("Device tree '%s' does not have a 'binman' "
354 images = _ReadImageDesc(node)
358 new_images = OrderedDict()
359 for name, image in images.items():
360 if name in select_images:
361 new_images[name] = image
365 tout.Notice('Skipping images: %s' % ', '.join(skip))
367 state.Prepare(images, dtb)
369 # Prepare the device tree by making sure that any missing
370 # properties are added (e.g. 'pos' and 'size'). The values of these
371 # may not be correct yet, but we add placeholders so that the
372 # size of the device tree is correct. Later, in
373 # SetCalculatedProperties() we will insert the correct values
374 # without changing the device-tree size, thus ensuring that our
375 # entry offsets remain the same.
376 for image in images.values():
377 image.ExpandEntries()
379 image.AddMissingProperties()
380 image.ProcessFdt(dtb)
382 for dtb_item in state.GetAllFdts():
383 dtb_item.Sync(auto_resize=True)
389 def ProcessImage(image, update_fdt, write_map, get_contents=True,
391 """Perform all steps for this image, including checking and # writing it.
393 This means that errors found with a later image will be reported after
394 earlier images are already completed and written, but that does not seem
398 image: Image to process
399 update_fdt: True to update the FDT wth entry offsets, etc.
400 write_map: True to write a map file
401 get_contents: True to get the image contents from files, etc., False if
402 the contents is already present
403 allow_resize: True to allow entries to change size (this does a re-pack
404 of the entries), False to raise an exception
407 image.GetEntryContents()
408 image.GetEntryOffsets()
410 # We need to pack the entries to figure out where everything
411 # should be placed. This sets the offset/size of each entry.
412 # However, after packing we call ProcessEntryContents() which
413 # may result in an entry changing size. In that case we need to
414 # do another pass. Since the device tree often contains the
415 # final offset/size information we try to make space for this in
416 # AddMissingProperties() above. However, if the device is
417 # compressed we cannot know this compressed size in advance,
418 # since changing an offset from 0x100 to 0x104 (for example) can
419 # alter the compressed size of the device tree. So we need a
420 # third pass for this.
422 for pack_pass in range(passes):
427 except Exception as e:
429 fname = image.WriteMap()
430 print("Wrote map file '%s' to show errors" % fname)
434 image.SetCalculatedProperties()
435 for dtb_item in state.GetAllFdts():
439 sizes_ok = image.ProcessEntryContents()
443 tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
445 image.Raise('Entries changed size after packing (tried %s passes)' %
454 """The main control code for binman
456 This assumes that help and test options have already been dealt with. It
457 deals with the core task of building images.
460 args: Command line arguments Namespace object
466 pager = os.getenv('PAGER')
469 fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
471 command.Run(pager, fname)
474 # Put these here so that we can import this module without libfdt
475 from image import Image
476 from binman import state
478 if args.cmd in ['ls', 'extract', 'replace']:
480 tout.Init(args.verbosity)
481 tools.PrepareOutputDir(None)
483 ListEntries(args.image, args.paths)
485 if args.cmd == 'extract':
486 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
487 not args.uncompressed)
489 if args.cmd == 'replace':
490 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
491 do_compress=not args.compressed,
492 allow_resize=not args.fix_size, write_map=args.map)
496 tools.FinaliseOutputDir()
499 # Try to figure out which device tree contains our image description
505 raise ValueError('Must provide a board to process (use -b <board>)')
506 board_pathname = os.path.join(args.build_dir, board)
507 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
510 args.indir.append(board_pathname)
513 tout.Init(args.verbosity)
514 elf.debug = args.debug
515 cbfs_util.VERBOSE = args.verbosity > 2
516 state.use_fake_dtb = args.fake_dtb
518 tools.SetInputDirs(args.indir)
519 tools.PrepareOutputDir(args.outdir, args.preserve)
520 tools.SetToolPaths(args.toolpath)
521 state.SetEntryArgs(args.entry_arg)
523 images = PrepareImagesAndDtbs(dtb_fname, args.image,
525 for image in images.values():
526 ProcessImage(image, args.update_fdt, args.map)
528 # Write the updated FDTs to our output files
529 for dtb_item in state.GetAllFdts():
530 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
533 tools.FinaliseOutputDir()