patman: Drop references to __future__
[oweals/u-boot.git] / tools / binman / control.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
3 # Written by Simon Glass <sjg@chromium.org>
4 #
5 # Creates binary images from input files controlled by a description
6 #
7
8 from collections import OrderedDict
9 import os
10 import sys
11 import tools
12
13 import cbfs_util
14 import command
15 import elf
16 import tout
17
18 # List of images we plan to create
19 # Make this global so that it can be referenced from tests
20 images = OrderedDict()
21
22 def _ReadImageDesc(binman_node):
23     """Read the image descriptions from the /binman node
24
25     This normally produces a single Image object called 'image'. But if
26     multiple images are present, they will all be returned.
27
28     Args:
29         binman_node: Node object of the /binman node
30     Returns:
31         OrderedDict of Image objects, each of which describes an image
32     """
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)
37     else:
38         images['image'] = Image('image', binman_node)
39     return images
40
41 def _FindBinmanNode(dtb):
42     """Find the 'binman' node in the device tree
43
44     Args:
45         dtb: Fdt object to scan
46     Returns:
47         Node object of /binman node, or None if not found
48     """
49     for node in dtb.GetRoot().subnodes:
50         if node.name == 'binman':
51             return node
52     return None
53
54 def WriteEntryDocs(modules, test_missing=None):
55     """Write out documentation for all entries
56
57     Args:
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
61             normal use.
62     """
63     from entry import Entry
64     Entry.WriteDocs(modules, test_missing)
65
66
67 def ListEntries(image_fname, entry_paths):
68     """List the entries in an image
69
70     This decodes the supplied image and displays a table of entries from that
71     image, preceded by a header.
72
73     Args:
74         image_fname: Image filename to process
75         entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
76                                                      'section/u-boot'])
77     """
78     image = Image.FromFile(image_fname)
79
80     entries, lines, widths = image.GetListEntries(entry_paths)
81
82     num_columns = len(widths)
83     for linenum, line in enumerate(lines):
84         if linenum == 1:
85             # Print header line
86             print('-' * (sum(widths) + num_columns * 2))
87         out = ''
88         for i, item in enumerate(line):
89             width = -widths[i]
90             if item.startswith('>'):
91                 width = -width
92                 item = item[1:]
93             txt = '%*s  ' % (width, item)
94             out += txt
95         print(out.rstrip())
96
97
98 def ReadEntry(image_fname, entry_path, decomp=True):
99     """Extract an entry from an image
100
101     This extracts the data from a particular entry in an image
102
103     Args:
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
108
109     Returns:
110         data extracted from the entry
111     """
112     global Image
113     from image import Image
114
115     image = Image.FromFile(image_fname)
116     entry = image.FindEntryPath(entry_path)
117     return entry.ReadData(decomp)
118
119
120 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
121                    decomp=True):
122     """Extract the data from one or more entries and write it to files
123
124     Args:
125         image_fname: Image filename to process
126         output_fname: Single output filename to use if extracting one file, None
127             otherwise
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
131
132     Returns:
133         List of EntryInfo records that were written
134     """
135     image = Image.FromFile(image_fname)
136
137     # Output an entry to a single file, as a special case
138     if output_fname:
139         if not entry_paths:
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))
147         return
148
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
151     # a sub-section.
152     einfos = image.GetListEntries(entry_paths)[0]
153     tout.Notice('%d entries match and will be written' % len(einfos))
154     for einfo in einfos:
155         entry = einfo.entry
156         data = entry.ReadData(decomp)
157         path = entry.GetPath()[1:]
158         fname = os.path.join(outdir, path)
159
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):
164                 os.makedirs(fname)
165             fname = os.path.join(fname, 'root')
166         tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname))
167         tools.WriteFile(fname, data)
168     return einfos
169
170
171 def BeforeReplace(image, allow_resize):
172     """Handle getting an image ready for replacing entries in it
173
174     Args:
175         image: Image to prepare
176     """
177     state.PrepareFromLoadedData(image)
178     image.LoadData()
179
180     # If repacking, drop the old offset/size values except for the original
181     # ones, so we are only left with the constraints.
182     if allow_resize:
183         image.ResetForPack()
184
185
186 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
187     """Handle replacing a single entry an an image
188
189     Args:
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
197     """
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')
201         if not allow_resize:
202             entry.Raise('Entry data size does not match, but resize is disabled')
203
204
205 def AfterReplace(image, allow_resize, write_map):
206     """Handle write out an image after replacing entries in it
207
208     Args:
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
213     """
214     tout.Info('Processing image')
215     ProcessImage(image, update_fdt=True, write_map=write_map,
216                  get_contents=False, allow_resize=allow_resize)
217
218
219 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
220                       write_map=False):
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)
225
226
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
230
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.
233
234     Args:
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
243
244     Returns:
245         Image object that was updated
246     """
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)
252
253     return image
254
255
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
259
260     Args:
261         image_fname: Image filename to process
262         input_fname: Single input ilename to use if replacing one file, None
263             otherwise
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
268             compressed.
269         write_map: True to write a map file
270
271     Returns:
272         List of EntryInfo records that were written
273     """
274     image = Image.FromFile(image_fname)
275
276     # Replace an entry from a single file, as a special case
277     if input_fname:
278         if not entry_paths:
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)
287         return
288
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
291     # a sub-section.
292     einfos = image.GetListEntries(entry_paths)[0]
293     tout.Notice("Replacing %d matching entries in image '%s'" %
294                 (len(einfos), image_fname))
295
296     BeforeReplace(image, allow_resize)
297
298     for einfo in einfos:
299         entry = einfo.entry
300         if entry.GetEntries():
301             tout.Info("Skipping section entry '%s'" % entry.GetPath())
302             continue
303
304         path = entry.GetPath()[1:]
305         fname = os.path.join(indir, path)
306
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)
312         else:
313             tout.Warning("Skipping entry '%s' from missing file '%s'" %
314                          (entry.GetPath(), fname))
315
316     AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
317     return image
318
319
320 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
321     """Prepare the images to be processed and select the device tree
322
323     This function:
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,
328         image-pos, etc.
329
330     Args:
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.
334     """
335     # Import these here in case libfdt.py is not available, in which case
336     # the above help option still works.
337     import fdt
338     import fdt_util
339     global images
340
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
343     # in binman.
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)
348
349     node = _FindBinmanNode(dtb)
350     if not node:
351         raise ValueError("Device tree '%s' does not have a 'binman' "
352                             "node" % dtb_fname)
353
354     images = _ReadImageDesc(node)
355
356     if select_images:
357         skip = []
358         new_images = OrderedDict()
359         for name, image in images.items():
360             if name in select_images:
361                 new_images[name] = image
362             else:
363                 skip.append(name)
364         images = new_images
365         tout.Notice('Skipping images: %s' % ', '.join(skip))
366
367     state.Prepare(images, dtb)
368
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()
378         if update_fdt:
379             image.AddMissingProperties()
380         image.ProcessFdt(dtb)
381
382     for dtb_item in state.GetAllFdts():
383         dtb_item.Sync(auto_resize=True)
384         dtb_item.Pack()
385         dtb_item.Flush()
386     return images
387
388
389 def ProcessImage(image, update_fdt, write_map, get_contents=True,
390                  allow_resize=True):
391     """Perform all steps for this image, including checking and # writing it.
392
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
395     important.
396
397     Args:
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
405     """
406     if get_contents:
407         image.GetEntryContents()
408     image.GetEntryOffsets()
409
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.
421     passes = 5
422     for pack_pass in range(passes):
423         try:
424             image.PackEntries()
425             image.CheckSize()
426             image.CheckEntries()
427         except Exception as e:
428             if write_map:
429                 fname = image.WriteMap()
430                 print("Wrote map file '%s' to show errors"  % fname)
431             raise
432         image.SetImagePos()
433         if update_fdt:
434             image.SetCalculatedProperties()
435             for dtb_item in state.GetAllFdts():
436                 dtb_item.Sync()
437                 dtb_item.Flush()
438         image.WriteSymbols()
439         sizes_ok = image.ProcessEntryContents()
440         if sizes_ok:
441             break
442         image.ResetForPack()
443     tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
444     if not sizes_ok:
445         image.Raise('Entries changed size after packing (tried %s passes)' %
446                     passes)
447
448     image.BuildImage()
449     if write_map:
450         image.WriteMap()
451
452
453 def Binman(args):
454     """The main control code for binman
455
456     This assumes that help and test options have already been dealt with. It
457     deals with the core task of building images.
458
459     Args:
460         args: Command line arguments Namespace object
461     """
462     global Image
463     global state
464
465     if args.full_help:
466         pager = os.getenv('PAGER')
467         if not pager:
468             pager = 'more'
469         fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
470                             'README')
471         command.Run(pager, fname)
472         return 0
473
474     # Put these here so that we can import this module without libfdt
475     from image import Image
476     import state
477
478     if args.cmd in ['ls', 'extract', 'replace']:
479         try:
480             tout.Init(args.verbosity)
481             tools.PrepareOutputDir(None)
482             if args.cmd == 'ls':
483                 ListEntries(args.image, args.paths)
484
485             if args.cmd == 'extract':
486                 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
487                                not args.uncompressed)
488
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)
493         except:
494             raise
495         finally:
496             tools.FinaliseOutputDir()
497         return 0
498
499     # Try to figure out which device tree contains our image description
500     if args.dt:
501         dtb_fname = args.dt
502     else:
503         board = args.board
504         if not board:
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')
508         if not args.indir:
509             args.indir = ['.']
510         args.indir.append(board_pathname)
511
512     try:
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
517         try:
518             tools.SetInputDirs(args.indir)
519             tools.PrepareOutputDir(args.outdir, args.preserve)
520             tools.SetToolPaths(args.toolpath)
521             state.SetEntryArgs(args.entry_arg)
522
523             images = PrepareImagesAndDtbs(dtb_fname, args.image,
524                                           args.update_fdt)
525             for image in images.values():
526                 ProcessImage(image, args.update_fdt, args.map)
527
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())
531
532         finally:
533             tools.FinaliseOutputDir()
534     finally:
535         tout.Uninit()
536
537     return 0