Merge tag 'v2022.01-rc4' into next
[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 pkg_resources
12 import re
13
14 import sys
15 from patman import tools
16
17 from binman import cbfs_util
18 from binman import elf
19 from patman import command
20 from patman import tout
21
22 # List of images we plan to create
23 # Make this global so that it can be referenced from tests
24 images = OrderedDict()
25
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 = {}
30
31 def _ReadImageDesc(binman_node, use_expanded):
32     """Read the image descriptions from the /binman node
33
34     This normally produces a single Image object called 'image'. But if
35     multiple images are present, they will all be returned.
36
37     Args:
38         binman_node: Node object of the /binman node
39         use_expanded: True if the FDT will be updated with the entry information
40     Returns:
41         OrderedDict of Image objects, each of which describes an image
42     """
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)
48     else:
49         images['image'] = Image('image', binman_node, use_expanded=use_expanded)
50     return images
51
52 def _FindBinmanNode(dtb):
53     """Find the 'binman' node in the device tree
54
55     Args:
56         dtb: Fdt object to scan
57     Returns:
58         Node object of /binman node, or None if not found
59     """
60     for node in dtb.GetRoot().subnodes:
61         if node.name == 'binman':
62             return node
63     return None
64
65 def _ReadMissingBlobHelp():
66     """Read the missing-blob-help file
67
68     This file containins help messages explaining what to do when external blobs
69     are missing.
70
71     Returns:
72         Dict:
73             key: Message tag (str)
74             value: Message text (str)
75     """
76
77     def _FinishTag(tag, msg, result):
78         if tag:
79             result[tag] = msg.rstrip()
80             tag = None
81             msg = ''
82         return tag, msg
83
84     my_data = pkg_resources.resource_string(__name__, 'missing-blob-help')
85     re_tag = re.compile('^([-a-z0-9]+):$')
86     result = {}
87     tag = None
88     msg = ''
89     for line in my_data.decode('utf-8').splitlines():
90         if not line.startswith('#'):
91             m_tag = re_tag.match(line)
92             if m_tag:
93                 _, msg = _FinishTag(tag, msg, result)
94                 tag = m_tag.group(1)
95             elif tag:
96                 msg += line + '\n'
97     _FinishTag(tag, msg, result)
98     return result
99
100 def _ShowBlobHelp(path, text):
101     tout.Warning('\n%s:' % path)
102     for line in text.splitlines():
103         tout.Warning('   %s' % line)
104
105 def _ShowHelpForMissingBlobs(missing_list):
106     """Show help for each missing blob to help the user take action
107
108     Args:
109         missing_list: List of Entry objects to show help for
110     """
111     global missing_blob_help
112
113     if not missing_blob_help:
114         missing_blob_help = _ReadMissingBlobHelp()
115
116     for entry in missing_list:
117         tags = entry.GetHelpTags()
118
119         # Show the first match help message
120         for tag in tags:
121             if tag in missing_blob_help:
122                 _ShowBlobHelp(entry._node.path, missing_blob_help[tag])
123                 break
124
125 def GetEntryModules(include_testing=True):
126     """Get a set of entry class implementations
127
128     Returns:
129         Set of paths to entry class filenames
130     """
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])
136
137 def WriteEntryDocs(modules, test_missing=None):
138     """Write out documentation for all entries
139
140     Args:
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
144             normal use.
145     """
146     from binman.entry import Entry
147     Entry.WriteDocs(modules, test_missing)
148
149
150 def ListEntries(image_fname, entry_paths):
151     """List the entries in an image
152
153     This decodes the supplied image and displays a table of entries from that
154     image, preceded by a header.
155
156     Args:
157         image_fname: Image filename to process
158         entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
159                                                      'section/u-boot'])
160     """
161     image = Image.FromFile(image_fname)
162
163     entries, lines, widths = image.GetListEntries(entry_paths)
164
165     num_columns = len(widths)
166     for linenum, line in enumerate(lines):
167         if linenum == 1:
168             # Print header line
169             print('-' * (sum(widths) + num_columns * 2))
170         out = ''
171         for i, item in enumerate(line):
172             width = -widths[i]
173             if item.startswith('>'):
174                 width = -width
175                 item = item[1:]
176             txt = '%*s  ' % (width, item)
177             out += txt
178         print(out.rstrip())
179
180
181 def ReadEntry(image_fname, entry_path, decomp=True):
182     """Extract an entry from an image
183
184     This extracts the data from a particular entry in an image
185
186     Args:
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
191
192     Returns:
193         data extracted from the entry
194     """
195     global Image
196     from binman.image import Image
197
198     image = Image.FromFile(image_fname)
199     entry = image.FindEntryPath(entry_path)
200     return entry.ReadData(decomp)
201
202
203 def ShowAltFormats(image):
204     """Show alternative formats available for entries in the image
205
206     This shows a list of formats available.
207
208     Args:
209         image (Image): Image to check
210     """
211     alt_formats = {}
212     image.CheckAltFormats(alt_formats)
213     print('%-10s  %-20s  %s' % ('Flag (-F)', 'Entry type', 'Description'))
214     for name, val in alt_formats.items():
215         entry, helptext = val
216         print('%-10s  %-20s  %s' % (name, entry.etype, helptext))
217
218
219 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
220                    decomp=True, alt_format=None):
221     """Extract the data from one or more entries and write it to files
222
223     Args:
224         image_fname: Image filename to process
225         output_fname: Single output filename to use if extracting one file, None
226             otherwise
227         outdir: Output directory to use (for any number of files), else None
228         entry_paths: List of entry paths to extract
229         decomp: True to decompress the entry data
230
231     Returns:
232         List of EntryInfo records that were written
233     """
234     image = Image.FromFile(image_fname)
235
236     if alt_format == 'list':
237         ShowAltFormats(image)
238         return
239
240     # Output an entry to a single file, as a special case
241     if output_fname:
242         if not entry_paths:
243             raise ValueError('Must specify an entry path to write with -f')
244         if len(entry_paths) != 1:
245             raise ValueError('Must specify exactly one entry path to write with -f')
246         entry = image.FindEntryPath(entry_paths[0])
247         data = entry.ReadData(decomp, alt_format)
248         tools.WriteFile(output_fname, data)
249         tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
250         return
251
252     # Otherwise we will output to a path given by the entry path of each entry.
253     # This means that entries will appear in subdirectories if they are part of
254     # a sub-section.
255     einfos = image.GetListEntries(entry_paths)[0]
256     tout.Notice('%d entries match and will be written' % len(einfos))
257     for einfo in einfos:
258         entry = einfo.entry
259         data = entry.ReadData(decomp, alt_format)
260         path = entry.GetPath()[1:]
261         fname = os.path.join(outdir, path)
262
263         # If this entry has children, create a directory for it and put its
264         # data in a file called 'root' in that directory
265         if entry.GetEntries():
266             if fname and not os.path.exists(fname):
267                 os.makedirs(fname)
268             fname = os.path.join(fname, 'root')
269         tout.Notice("Write entry '%s' size %x to '%s'" %
270                     (entry.GetPath(), len(data), fname))
271         tools.WriteFile(fname, data)
272     return einfos
273
274
275 def BeforeReplace(image, allow_resize):
276     """Handle getting an image ready for replacing entries in it
277
278     Args:
279         image: Image to prepare
280     """
281     state.PrepareFromLoadedData(image)
282     image.LoadData()
283
284     # If repacking, drop the old offset/size values except for the original
285     # ones, so we are only left with the constraints.
286     if allow_resize:
287         image.ResetForPack()
288
289
290 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
291     """Handle replacing a single entry an an image
292
293     Args:
294         image: Image to update
295         entry: Entry to write
296         data: Data to replace with
297         do_compress: True to compress the data if needed, False if data is
298             already compressed so should be used as is
299         allow_resize: True to allow entries to change size (this does a re-pack
300             of the entries), False to raise an exception
301     """
302     if not entry.WriteData(data, do_compress):
303         if not image.allow_repack:
304             entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
305         if not allow_resize:
306             entry.Raise('Entry data size does not match, but resize is disabled')
307
308
309 def AfterReplace(image, allow_resize, write_map):
310     """Handle write out an image after replacing entries in it
311
312     Args:
313         image: Image to write
314         allow_resize: True to allow entries to change size (this does a re-pack
315             of the entries), False to raise an exception
316         write_map: True to write a map file
317     """
318     tout.Info('Processing image')
319     ProcessImage(image, update_fdt=True, write_map=write_map,
320                  get_contents=False, allow_resize=allow_resize)
321
322
323 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
324                       write_map=False):
325     BeforeReplace(image, allow_resize)
326     tout.Info('Writing data to %s' % entry.GetPath())
327     ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
328     AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
329
330
331 def WriteEntry(image_fname, entry_path, data, do_compress=True,
332                allow_resize=True, write_map=False):
333     """Replace an entry in an image
334
335     This replaces the data in a particular entry in an image. This size of the
336     new data must match the size of the old data unless allow_resize is True.
337
338     Args:
339         image_fname: Image filename to process
340         entry_path: Path to entry to extract
341         data: Data to replace with
342         do_compress: True to compress the data if needed, False if data is
343             already compressed so should be used as is
344         allow_resize: True to allow entries to change size (this does a re-pack
345             of the entries), False to raise an exception
346         write_map: True to write a map file
347
348     Returns:
349         Image object that was updated
350     """
351     tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
352     image = Image.FromFile(image_fname)
353     entry = image.FindEntryPath(entry_path)
354     WriteEntryToImage(image, entry, data, do_compress=do_compress,
355                       allow_resize=allow_resize, write_map=write_map)
356
357     return image
358
359
360 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
361                    do_compress=True, allow_resize=True, write_map=False):
362     """Replace the data from one or more entries from input files
363
364     Args:
365         image_fname: Image filename to process
366         input_fname: Single input filename to use if replacing one file, None
367             otherwise
368         indir: Input directory to use (for any number of files), else None
369         entry_paths: List of entry paths to replace
370         do_compress: True if the input data is uncompressed and may need to be
371             compressed if the entry requires it, False if the data is already
372             compressed.
373         write_map: True to write a map file
374
375     Returns:
376         List of EntryInfo records that were written
377     """
378     image_fname = os.path.abspath(image_fname)
379     image = Image.FromFile(image_fname)
380
381     # Replace an entry from a single file, as a special case
382     if input_fname:
383         if not entry_paths:
384             raise ValueError('Must specify an entry path to read with -f')
385         if len(entry_paths) != 1:
386             raise ValueError('Must specify exactly one entry path to write with -f')
387         entry = image.FindEntryPath(entry_paths[0])
388         data = tools.ReadFile(input_fname)
389         tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
390         WriteEntryToImage(image, entry, data, do_compress=do_compress,
391                           allow_resize=allow_resize, write_map=write_map)
392         return
393
394     # Otherwise we will input from a path given by the entry path of each entry.
395     # This means that files must appear in subdirectories if they are part of
396     # a sub-section.
397     einfos = image.GetListEntries(entry_paths)[0]
398     tout.Notice("Replacing %d matching entries in image '%s'" %
399                 (len(einfos), image_fname))
400
401     BeforeReplace(image, allow_resize)
402
403     for einfo in einfos:
404         entry = einfo.entry
405         if entry.GetEntries():
406             tout.Info("Skipping section entry '%s'" % entry.GetPath())
407             continue
408
409         path = entry.GetPath()[1:]
410         fname = os.path.join(indir, path)
411
412         if os.path.exists(fname):
413             tout.Notice("Write entry '%s' from file '%s'" %
414                         (entry.GetPath(), fname))
415             data = tools.ReadFile(fname)
416             ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
417         else:
418             tout.Warning("Skipping entry '%s' from missing file '%s'" %
419                          (entry.GetPath(), fname))
420
421     AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
422     return image
423
424
425 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded):
426     """Prepare the images to be processed and select the device tree
427
428     This function:
429     - reads in the device tree
430     - finds and scans the binman node to create all entries
431     - selects which images to build
432     - Updates the device tress with placeholder properties for offset,
433         image-pos, etc.
434
435     Args:
436         dtb_fname: Filename of the device tree file to use (.dts or .dtb)
437         selected_images: List of images to output, or None for all
438         update_fdt: True to update the FDT wth entry offsets, etc.
439         use_expanded: True to use expanded versions of entries, if available.
440             So if 'u-boot' is called for, we use 'u-boot-expanded' instead. This
441             is needed if update_fdt is True (although tests may disable it)
442
443     Returns:
444         OrderedDict of images:
445             key: Image name (str)
446             value: Image object
447     """
448     # Import these here in case libfdt.py is not available, in which case
449     # the above help option still works.
450     from dtoc import fdt
451     from dtoc import fdt_util
452     global images
453
454     # Get the device tree ready by compiling it and copying the compiled
455     # output into a file in our output directly. Then scan it for use
456     # in binman.
457     dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
458     fname = tools.GetOutputFilename('u-boot.dtb.out')
459     tools.WriteFile(fname, tools.ReadFile(dtb_fname))
460     dtb = fdt.FdtScan(fname)
461
462     node = _FindBinmanNode(dtb)
463     if not node:
464         raise ValueError("Device tree '%s' does not have a 'binman' "
465                             "node" % dtb_fname)
466
467     images = _ReadImageDesc(node, use_expanded)
468
469     if select_images:
470         skip = []
471         new_images = OrderedDict()
472         for name, image in images.items():
473             if name in select_images:
474                 new_images[name] = image
475             else:
476                 skip.append(name)
477         images = new_images
478         tout.Notice('Skipping images: %s' % ', '.join(skip))
479
480     state.Prepare(images, dtb)
481
482     # Prepare the device tree by making sure that any missing
483     # properties are added (e.g. 'pos' and 'size'). The values of these
484     # may not be correct yet, but we add placeholders so that the
485     # size of the device tree is correct. Later, in
486     # SetCalculatedProperties() we will insert the correct values
487     # without changing the device-tree size, thus ensuring that our
488     # entry offsets remain the same.
489     for image in images.values():
490         image.ExpandEntries()
491         if update_fdt:
492             image.AddMissingProperties(True)
493         image.ProcessFdt(dtb)
494
495     for dtb_item in state.GetAllFdts():
496         dtb_item.Sync(auto_resize=True)
497         dtb_item.Pack()
498         dtb_item.Flush()
499     return images
500
501
502 def ProcessImage(image, update_fdt, write_map, get_contents=True,
503                  allow_resize=True, allow_missing=False):
504     """Perform all steps for this image, including checking and # writing it.
505
506     This means that errors found with a later image will be reported after
507     earlier images are already completed and written, but that does not seem
508     important.
509
510     Args:
511         image: Image to process
512         update_fdt: True to update the FDT wth entry offsets, etc.
513         write_map: True to write a map file
514         get_contents: True to get the image contents from files, etc., False if
515             the contents is already present
516         allow_resize: True to allow entries to change size (this does a re-pack
517             of the entries), False to raise an exception
518         allow_missing: Allow blob_ext objects to be missing
519
520     Returns:
521         True if one or more external blobs are missing, False if all are present
522     """
523     if get_contents:
524         image.SetAllowMissing(allow_missing)
525         image.GetEntryContents()
526     image.GetEntryOffsets()
527
528     # We need to pack the entries to figure out where everything
529     # should be placed. This sets the offset/size of each entry.
530     # However, after packing we call ProcessEntryContents() which
531     # may result in an entry changing size. In that case we need to
532     # do another pass. Since the device tree often contains the
533     # final offset/size information we try to make space for this in
534     # AddMissingProperties() above. However, if the device is
535     # compressed we cannot know this compressed size in advance,
536     # since changing an offset from 0x100 to 0x104 (for example) can
537     # alter the compressed size of the device tree. So we need a
538     # third pass for this.
539     passes = 5
540     for pack_pass in range(passes):
541         try:
542             image.PackEntries()
543         except Exception as e:
544             if write_map:
545                 fname = image.WriteMap()
546                 print("Wrote map file '%s' to show errors"  % fname)
547             raise
548         image.SetImagePos()
549         if update_fdt:
550             image.SetCalculatedProperties()
551             for dtb_item in state.GetAllFdts():
552                 dtb_item.Sync()
553                 dtb_item.Flush()
554         image.WriteSymbols()
555         sizes_ok = image.ProcessEntryContents()
556         if sizes_ok:
557             break
558         image.ResetForPack()
559     tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
560     if not sizes_ok:
561         image.Raise('Entries changed size after packing (tried %s passes)' %
562                     passes)
563
564     image.BuildImage()
565     if write_map:
566         image.WriteMap()
567     missing_list = []
568     image.CheckMissing(missing_list)
569     if missing_list:
570         tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" %
571                      (image.name, ' '.join([e.name for e in missing_list])))
572         _ShowHelpForMissingBlobs(missing_list)
573     return bool(missing_list)
574
575
576 def Binman(args):
577     """The main control code for binman
578
579     This assumes that help and test options have already been dealt with. It
580     deals with the core task of building images.
581
582     Args:
583         args: Command line arguments Namespace object
584     """
585     global Image
586     global state
587
588     if args.full_help:
589         tools.PrintFullHelp(
590             os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'README.rst')
591         )
592         return 0
593
594     # Put these here so that we can import this module without libfdt
595     from binman.image import Image
596     from binman import state
597
598     if args.cmd in ['ls', 'extract', 'replace']:
599         try:
600             tout.Init(args.verbosity)
601             tools.PrepareOutputDir(None)
602             if args.cmd == 'ls':
603                 ListEntries(args.image, args.paths)
604
605             if args.cmd == 'extract':
606                 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
607                                not args.uncompressed, args.format)
608
609             if args.cmd == 'replace':
610                 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
611                                do_compress=not args.compressed,
612                                allow_resize=not args.fix_size, write_map=args.map)
613         except:
614             raise
615         finally:
616             tools.FinaliseOutputDir()
617         return 0
618
619     elf_params = None
620     if args.update_fdt_in_elf:
621         elf_params = args.update_fdt_in_elf.split(',')
622         if len(elf_params) != 4:
623             raise ValueError('Invalid args %s to --update-fdt-in-elf: expected infile,outfile,begin_sym,end_sym' %
624                              elf_params)
625
626     # Try to figure out which device tree contains our image description
627     if args.dt:
628         dtb_fname = args.dt
629     else:
630         board = args.board
631         if not board:
632             raise ValueError('Must provide a board to process (use -b <board>)')
633         board_pathname = os.path.join(args.build_dir, board)
634         dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
635         if not args.indir:
636             args.indir = ['.']
637         args.indir.append(board_pathname)
638
639     try:
640         tout.Init(args.verbosity)
641         elf.debug = args.debug
642         cbfs_util.VERBOSE = args.verbosity > 2
643         state.use_fake_dtb = args.fake_dtb
644
645         # Normally we replace the 'u-boot' etype with 'u-boot-expanded', etc.
646         # When running tests this can be disabled using this flag. When not
647         # updating the FDT in image, it is not needed by binman, but we use it
648         # for consistency, so that the images look the same to U-Boot at
649         # runtime.
650         use_expanded = not args.no_expanded
651         try:
652             tools.SetInputDirs(args.indir)
653             tools.PrepareOutputDir(args.outdir, args.preserve)
654             tools.SetToolPaths(args.toolpath)
655             state.SetEntryArgs(args.entry_arg)
656             state.SetThreads(args.threads)
657
658             images = PrepareImagesAndDtbs(dtb_fname, args.image,
659                                           args.update_fdt, use_expanded)
660             if args.test_section_timeout:
661                 # Set the first image to timeout, used in testThreadTimeout()
662                 images[list(images.keys())[0]].test_section_timeout = True
663             missing = False
664             for image in images.values():
665                 missing |= ProcessImage(image, args.update_fdt, args.map,
666                                         allow_missing=args.allow_missing)
667
668             # Write the updated FDTs to our output files
669             for dtb_item in state.GetAllFdts():
670                 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
671
672             if elf_params:
673                 data = state.GetFdtForEtype('u-boot-dtb').GetContents()
674                 elf.UpdateFile(*elf_params, data)
675
676             if missing:
677                 tout.Warning("\nSome images are invalid")
678
679             # Use this to debug the time take to pack the image
680             #state.TimingShow()
681         finally:
682             tools.FinaliseOutputDir()
683     finally:
684         tout.Uninit()
685
686     return 0