Merge tag 'u-boot-imx-20200825' of https://gitlab.denx.de/u-boot/custodians/u-boot-imx
[platform/kernel/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 glob
10 import os
11 import sys
12 from patman import tools
13
14 from binman import cbfs_util
15 from binman import elf
16 from patman import command
17 from patman import tout
18
19 # List of images we plan to create
20 # Make this global so that it can be referenced from tests
21 images = OrderedDict()
22
23 def _ReadImageDesc(binman_node):
24     """Read the image descriptions from the /binman node
25
26     This normally produces a single Image object called 'image'. But if
27     multiple images are present, they will all be returned.
28
29     Args:
30         binman_node: Node object of the /binman node
31     Returns:
32         OrderedDict of Image objects, each of which describes an image
33     """
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)
38     else:
39         images['image'] = Image('image', binman_node)
40     return images
41
42 def _FindBinmanNode(dtb):
43     """Find the 'binman' node in the device tree
44
45     Args:
46         dtb: Fdt object to scan
47     Returns:
48         Node object of /binman node, or None if not found
49     """
50     for node in dtb.GetRoot().subnodes:
51         if node.name == 'binman':
52             return node
53     return None
54
55 def GetEntryModules(include_testing=True):
56     """Get a set of entry class implementations
57
58     Returns:
59         Set of paths to entry class filenames
60     """
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]
64                 for item in glob_list
65                 if include_testing or '_testing' not in item])
66
67 def WriteEntryDocs(modules, test_missing=None):
68     """Write out documentation for all entries
69
70     Args:
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
74             normal use.
75     """
76     from binman.entry import Entry
77     Entry.WriteDocs(modules, test_missing)
78
79
80 def ListEntries(image_fname, entry_paths):
81     """List the entries in an image
82
83     This decodes the supplied image and displays a table of entries from that
84     image, preceded by a header.
85
86     Args:
87         image_fname: Image filename to process
88         entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
89                                                      'section/u-boot'])
90     """
91     image = Image.FromFile(image_fname)
92
93     entries, lines, widths = image.GetListEntries(entry_paths)
94
95     num_columns = len(widths)
96     for linenum, line in enumerate(lines):
97         if linenum == 1:
98             # Print header line
99             print('-' * (sum(widths) + num_columns * 2))
100         out = ''
101         for i, item in enumerate(line):
102             width = -widths[i]
103             if item.startswith('>'):
104                 width = -width
105                 item = item[1:]
106             txt = '%*s  ' % (width, item)
107             out += txt
108         print(out.rstrip())
109
110
111 def ReadEntry(image_fname, entry_path, decomp=True):
112     """Extract an entry from an image
113
114     This extracts the data from a particular entry in an image
115
116     Args:
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
121
122     Returns:
123         data extracted from the entry
124     """
125     global Image
126     from binman.image import Image
127
128     image = Image.FromFile(image_fname)
129     entry = image.FindEntryPath(entry_path)
130     return entry.ReadData(decomp)
131
132
133 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
134                    decomp=True):
135     """Extract the data from one or more entries and write it to files
136
137     Args:
138         image_fname: Image filename to process
139         output_fname: Single output filename to use if extracting one file, None
140             otherwise
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
144
145     Returns:
146         List of EntryInfo records that were written
147     """
148     image = Image.FromFile(image_fname)
149
150     # Output an entry to a single file, as a special case
151     if output_fname:
152         if not entry_paths:
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))
160         return
161
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
164     # a sub-section.
165     einfos = image.GetListEntries(entry_paths)[0]
166     tout.Notice('%d entries match and will be written' % len(einfos))
167     for einfo in einfos:
168         entry = einfo.entry
169         data = entry.ReadData(decomp)
170         path = entry.GetPath()[1:]
171         fname = os.path.join(outdir, path)
172
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):
177                 os.makedirs(fname)
178             fname = os.path.join(fname, 'root')
179         tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname))
180         tools.WriteFile(fname, data)
181     return einfos
182
183
184 def BeforeReplace(image, allow_resize):
185     """Handle getting an image ready for replacing entries in it
186
187     Args:
188         image: Image to prepare
189     """
190     state.PrepareFromLoadedData(image)
191     image.LoadData()
192
193     # If repacking, drop the old offset/size values except for the original
194     # ones, so we are only left with the constraints.
195     if allow_resize:
196         image.ResetForPack()
197
198
199 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
200     """Handle replacing a single entry an an image
201
202     Args:
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
210     """
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')
214         if not allow_resize:
215             entry.Raise('Entry data size does not match, but resize is disabled')
216
217
218 def AfterReplace(image, allow_resize, write_map):
219     """Handle write out an image after replacing entries in it
220
221     Args:
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
226     """
227     tout.Info('Processing image')
228     ProcessImage(image, update_fdt=True, write_map=write_map,
229                  get_contents=False, allow_resize=allow_resize)
230
231
232 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
233                       write_map=False):
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)
238
239
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
243
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.
246
247     Args:
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
256
257     Returns:
258         Image object that was updated
259     """
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)
265
266     return image
267
268
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
272
273     Args:
274         image_fname: Image filename to process
275         input_fname: Single input ilename to use if replacing one file, None
276             otherwise
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
281             compressed.
282         write_map: True to write a map file
283
284     Returns:
285         List of EntryInfo records that were written
286     """
287     image = Image.FromFile(image_fname)
288
289     # Replace an entry from a single file, as a special case
290     if input_fname:
291         if not entry_paths:
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)
300         return
301
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
304     # a sub-section.
305     einfos = image.GetListEntries(entry_paths)[0]
306     tout.Notice("Replacing %d matching entries in image '%s'" %
307                 (len(einfos), image_fname))
308
309     BeforeReplace(image, allow_resize)
310
311     for einfo in einfos:
312         entry = einfo.entry
313         if entry.GetEntries():
314             tout.Info("Skipping section entry '%s'" % entry.GetPath())
315             continue
316
317         path = entry.GetPath()[1:]
318         fname = os.path.join(indir, path)
319
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)
325         else:
326             tout.Warning("Skipping entry '%s' from missing file '%s'" %
327                          (entry.GetPath(), fname))
328
329     AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
330     return image
331
332
333 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
334     """Prepare the images to be processed and select the device tree
335
336     This function:
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,
341         image-pos, etc.
342
343     Args:
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.
347     """
348     # Import these here in case libfdt.py is not available, in which case
349     # the above help option still works.
350     from dtoc import fdt
351     from dtoc import fdt_util
352     global images
353
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
356     # in binman.
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)
361
362     node = _FindBinmanNode(dtb)
363     if not node:
364         raise ValueError("Device tree '%s' does not have a 'binman' "
365                             "node" % dtb_fname)
366
367     images = _ReadImageDesc(node)
368
369     if select_images:
370         skip = []
371         new_images = OrderedDict()
372         for name, image in images.items():
373             if name in select_images:
374                 new_images[name] = image
375             else:
376                 skip.append(name)
377         images = new_images
378         tout.Notice('Skipping images: %s' % ', '.join(skip))
379
380     state.Prepare(images, dtb)
381
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()
391         if update_fdt:
392             image.AddMissingProperties()
393         image.ProcessFdt(dtb)
394
395     for dtb_item in state.GetAllFdts():
396         dtb_item.Sync(auto_resize=True)
397         dtb_item.Pack()
398         dtb_item.Flush()
399     return images
400
401
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.
405
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
408     important.
409
410     Args:
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
419
420     Returns:
421         True if one or more external blobs are missing, False if all are present
422     """
423     if get_contents:
424         image.SetAllowMissing(allow_missing)
425         image.GetEntryContents()
426     image.GetEntryOffsets()
427
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.
439     passes = 5
440     for pack_pass in range(passes):
441         try:
442             image.PackEntries()
443             image.CheckSize()
444             image.CheckEntries()
445         except Exception as e:
446             if write_map:
447                 fname = image.WriteMap()
448                 print("Wrote map file '%s' to show errors"  % fname)
449             raise
450         image.SetImagePos()
451         if update_fdt:
452             image.SetCalculatedProperties()
453             for dtb_item in state.GetAllFdts():
454                 dtb_item.Sync()
455                 dtb_item.Flush()
456         image.WriteSymbols()
457         sizes_ok = image.ProcessEntryContents()
458         if sizes_ok:
459             break
460         image.ResetForPack()
461     tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
462     if not sizes_ok:
463         image.Raise('Entries changed size after packing (tried %s passes)' %
464                     passes)
465
466     image.BuildImage()
467     if write_map:
468         image.WriteMap()
469     missing_list = []
470     image.CheckMissing(missing_list)
471     if 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)
475
476
477 def Binman(args):
478     """The main control code for binman
479
480     This assumes that help and test options have already been dealt with. It
481     deals with the core task of building images.
482
483     Args:
484         args: Command line arguments Namespace object
485     """
486     global Image
487     global state
488
489     if args.full_help:
490         pager = os.getenv('PAGER')
491         if not pager:
492             pager = 'more'
493         fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
494                             'README')
495         command.Run(pager, fname)
496         return 0
497
498     # Put these here so that we can import this module without libfdt
499     from binman.image import Image
500     from binman import state
501
502     if args.cmd in ['ls', 'extract', 'replace']:
503         try:
504             tout.Init(args.verbosity)
505             tools.PrepareOutputDir(None)
506             if args.cmd == 'ls':
507                 ListEntries(args.image, args.paths)
508
509             if args.cmd == 'extract':
510                 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
511                                not args.uncompressed)
512
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)
517         except:
518             raise
519         finally:
520             tools.FinaliseOutputDir()
521         return 0
522
523     # Try to figure out which device tree contains our image description
524     if args.dt:
525         dtb_fname = args.dt
526     else:
527         board = args.board
528         if not board:
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')
532         if not args.indir:
533             args.indir = ['.']
534         args.indir.append(board_pathname)
535
536     try:
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
541         try:
542             tools.SetInputDirs(args.indir)
543             tools.PrepareOutputDir(args.outdir, args.preserve)
544             tools.SetToolPaths(args.toolpath)
545             state.SetEntryArgs(args.entry_arg)
546
547             images = PrepareImagesAndDtbs(dtb_fname, args.image,
548                                           args.update_fdt)
549             missing = False
550             for image in images.values():
551                 missing |= ProcessImage(image, args.update_fdt, args.map,
552                                         allow_missing=args.allow_missing)
553
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())
557
558             if missing:
559                 tout.Warning("Some images are invalid")
560         finally:
561             tools.FinaliseOutputDir()
562     finally:
563         tout.Uninit()
564
565     return 0