68ad5fc2c0c4d098ddcf430d77be8b21dbe23668
[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 __future__ import print_function
9
10 from collections import OrderedDict
11 import os
12 import sys
13 import tools
14
15 import cbfs_util
16 import command
17 import elf
18 import tout
19
20 # List of images we plan to create
21 # Make this global so that it can be referenced from tests
22 images = OrderedDict()
23
24 def _ReadImageDesc(binman_node):
25     """Read the image descriptions from the /binman node
26
27     This normally produces a single Image object called 'image'. But if
28     multiple images are present, they will all be returned.
29
30     Args:
31         binman_node: Node object of the /binman node
32     Returns:
33         OrderedDict of Image objects, each of which describes an image
34     """
35     images = OrderedDict()
36     if 'multiple-images' in binman_node.props:
37         for node in binman_node.subnodes:
38             images[node.name] = Image(node.name, node)
39     else:
40         images['image'] = Image('image', binman_node)
41     return images
42
43 def _FindBinmanNode(dtb):
44     """Find the 'binman' node in the device tree
45
46     Args:
47         dtb: Fdt object to scan
48     Returns:
49         Node object of /binman node, or None if not found
50     """
51     for node in dtb.GetRoot().subnodes:
52         if node.name == 'binman':
53             return node
54     return None
55
56 def WriteEntryDocs(modules, test_missing=None):
57     """Write out documentation for all entries
58
59     Args:
60         modules: List of Module objects to get docs for
61         test_missing: Used for testing only, to force an entry's documeentation
62             to show as missing even if it is present. Should be set to None in
63             normal use.
64     """
65     from entry import Entry
66     Entry.WriteDocs(modules, test_missing)
67
68
69 def ListEntries(image_fname, entry_paths):
70     """List the entries in an image
71
72     This decodes the supplied image and displays a table of entries from that
73     image, preceded by a header.
74
75     Args:
76         image_fname: Image filename to process
77         entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
78                                                      'section/u-boot'])
79     """
80     image = Image.FromFile(image_fname)
81
82     entries, lines, widths = image.GetListEntries(entry_paths)
83
84     num_columns = len(widths)
85     for linenum, line in enumerate(lines):
86         if linenum == 1:
87             # Print header line
88             print('-' * (sum(widths) + num_columns * 2))
89         out = ''
90         for i, item in enumerate(line):
91             width = -widths[i]
92             if item.startswith('>'):
93                 width = -width
94                 item = item[1:]
95             txt = '%*s  ' % (width, item)
96             out += txt
97         print(out.rstrip())
98
99
100 def ReadEntry(image_fname, entry_path, decomp=True):
101     """Extract an entry from an image
102
103     This extracts the data from a particular entry in an image
104
105     Args:
106         image_fname: Image filename to process
107         entry_path: Path to entry to extract
108         decomp: True to return uncompressed data, if the data is compress
109             False to return the raw data
110
111     Returns:
112         data extracted from the entry
113     """
114     global Image
115     from image import Image
116
117     image = Image.FromFile(image_fname)
118     entry = image.FindEntryPath(entry_path)
119     return entry.ReadData(decomp)
120
121
122 def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
123                    decomp=True):
124     """Extract the data from one or more entries and write it to files
125
126     Args:
127         image_fname: Image filename to process
128         output_fname: Single output filename to use if extracting one file, None
129             otherwise
130         outdir: Output directory to use (for any number of files), else None
131         entry_paths: List of entry paths to extract
132         decomp: True to decompress the entry data
133
134     Returns:
135         List of EntryInfo records that were written
136     """
137     image = Image.FromFile(image_fname)
138
139     # Output an entry to a single file, as a special case
140     if output_fname:
141         if not entry_paths:
142             raise ValueError('Must specify an entry path to write with -f')
143         if len(entry_paths) != 1:
144             raise ValueError('Must specify exactly one entry path to write with -f')
145         entry = image.FindEntryPath(entry_paths[0])
146         data = entry.ReadData(decomp)
147         tools.WriteFile(output_fname, data)
148         tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
149         return
150
151     # Otherwise we will output to a path given by the entry path of each entry.
152     # This means that entries will appear in subdirectories if they are part of
153     # a sub-section.
154     einfos = image.GetListEntries(entry_paths)[0]
155     tout.Notice('%d entries match and will be written' % len(einfos))
156     for einfo in einfos:
157         entry = einfo.entry
158         data = entry.ReadData(decomp)
159         path = entry.GetPath()[1:]
160         fname = os.path.join(outdir, path)
161
162         # If this entry has children, create a directory for it and put its
163         # data in a file called 'root' in that directory
164         if entry.GetEntries():
165             if not os.path.exists(fname):
166                 os.makedirs(fname)
167             fname = os.path.join(fname, 'root')
168         tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname))
169         tools.WriteFile(fname, data)
170     return einfos
171
172
173 def BeforeReplace(image, allow_resize):
174     """Handle getting an image ready for replacing entries in it
175
176     Args:
177         image: Image to prepare
178     """
179     state.PrepareFromLoadedData(image)
180     image.LoadData()
181
182     # If repacking, drop the old offset/size values except for the original
183     # ones, so we are only left with the constraints.
184     if allow_resize:
185         image.ResetForPack()
186
187
188 def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
189     """Handle replacing a single entry an an image
190
191     Args:
192         image: Image to update
193         entry: Entry to write
194         data: Data to replace with
195         do_compress: True to compress the data if needed, False if data is
196             already compressed so should be used as is
197         allow_resize: True to allow entries to change size (this does a re-pack
198             of the entries), False to raise an exception
199     """
200     if not entry.WriteData(data, do_compress):
201         if not image.allow_repack:
202             entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
203         if not allow_resize:
204             entry.Raise('Entry data size does not match, but resize is disabled')
205
206
207 def AfterReplace(image, allow_resize, write_map):
208     """Handle write out an image after replacing entries in it
209
210     Args:
211         image: Image to write
212         allow_resize: True to allow entries to change size (this does a re-pack
213             of the entries), False to raise an exception
214         write_map: True to write a map file
215     """
216     tout.Info('Processing image')
217     ProcessImage(image, update_fdt=True, write_map=write_map,
218                  get_contents=False, allow_resize=allow_resize)
219
220
221 def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
222                       write_map=False):
223     BeforeReplace(image, allow_resize)
224     tout.Info('Writing data to %s' % entry.GetPath())
225     ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
226     AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
227
228
229 def WriteEntry(image_fname, entry_path, data, do_compress=True,
230                allow_resize=True, write_map=False):
231     """Replace an entry in an image
232
233     This replaces the data in a particular entry in an image. This size of the
234     new data must match the size of the old data unless allow_resize is True.
235
236     Args:
237         image_fname: Image filename to process
238         entry_path: Path to entry to extract
239         data: Data to replace with
240         do_compress: True to compress the data if needed, False if data is
241             already compressed so should be used as is
242         allow_resize: True to allow entries to change size (this does a re-pack
243             of the entries), False to raise an exception
244         write_map: True to write a map file
245
246     Returns:
247         Image object that was updated
248     """
249     tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
250     image = Image.FromFile(image_fname)
251     entry = image.FindEntryPath(entry_path)
252     WriteEntryToImage(image, entry, data, do_compress=do_compress,
253                       allow_resize=allow_resize, write_map=write_map)
254
255     return image
256
257
258 def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
259                    do_compress=True, allow_resize=True, write_map=False):
260     """Replace the data from one or more entries from input files
261
262     Args:
263         image_fname: Image filename to process
264         input_fname: Single input ilename to use if replacing one file, None
265             otherwise
266         indir: Input directory to use (for any number of files), else None
267         entry_paths: List of entry paths to extract
268         do_compress: True if the input data is uncompressed and may need to be
269             compressed if the entry requires it, False if the data is already
270             compressed.
271         write_map: True to write a map file
272
273     Returns:
274         List of EntryInfo records that were written
275     """
276     image = Image.FromFile(image_fname)
277
278     # Replace an entry from a single file, as a special case
279     if input_fname:
280         if not entry_paths:
281             raise ValueError('Must specify an entry path to read with -f')
282         if len(entry_paths) != 1:
283             raise ValueError('Must specify exactly one entry path to write with -f')
284         entry = image.FindEntryPath(entry_paths[0])
285         data = tools.ReadFile(input_fname)
286         tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
287         WriteEntryToImage(image, entry, data, do_compress=do_compress,
288                           allow_resize=allow_resize, write_map=write_map)
289         return
290
291     # Otherwise we will input from a path given by the entry path of each entry.
292     # This means that files must appear in subdirectories if they are part of
293     # a sub-section.
294     einfos = image.GetListEntries(entry_paths)[0]
295     tout.Notice("Replacing %d matching entries in image '%s'" %
296                 (len(einfos), image_fname))
297
298     BeforeReplace(image, allow_resize)
299
300     for einfo in einfos:
301         entry = einfo.entry
302         if entry.GetEntries():
303             tout.Info("Skipping section entry '%s'" % entry.GetPath())
304             continue
305
306         path = entry.GetPath()[1:]
307         fname = os.path.join(indir, path)
308
309         if os.path.exists(fname):
310             tout.Notice("Write entry '%s' from file '%s'" %
311                         (entry.GetPath(), fname))
312             data = tools.ReadFile(fname)
313             ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
314         else:
315             tout.Warning("Skipping entry '%s' from missing file '%s'" %
316                          (entry.GetPath(), fname))
317
318     AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
319     return image
320
321
322 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
323     """Prepare the images to be processed and select the device tree
324
325     This function:
326     - reads in the device tree
327     - finds and scans the binman node to create all entries
328     - selects which images to build
329     - Updates the device tress with placeholder properties for offset,
330         image-pos, etc.
331
332     Args:
333         dtb_fname: Filename of the device tree file to use (.dts or .dtb)
334         selected_images: List of images to output, or None for all
335         update_fdt: True to update the FDT wth entry offsets, etc.
336     """
337     # Import these here in case libfdt.py is not available, in which case
338     # the above help option still works.
339     import fdt
340     import fdt_util
341     global images
342
343     # Get the device tree ready by compiling it and copying the compiled
344     # output into a file in our output directly. Then scan it for use
345     # in binman.
346     dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
347     fname = tools.GetOutputFilename('u-boot.dtb.out')
348     tools.WriteFile(fname, tools.ReadFile(dtb_fname))
349     dtb = fdt.FdtScan(fname)
350
351     node = _FindBinmanNode(dtb)
352     if not node:
353         raise ValueError("Device tree '%s' does not have a 'binman' "
354                             "node" % dtb_fname)
355
356     images = _ReadImageDesc(node)
357
358     if select_images:
359         skip = []
360         new_images = OrderedDict()
361         for name, image in images.items():
362             if name in select_images:
363                 new_images[name] = image
364             else:
365                 skip.append(name)
366         images = new_images
367         tout.Notice('Skipping images: %s' % ', '.join(skip))
368
369     state.Prepare(images, dtb)
370
371     # Prepare the device tree by making sure that any missing
372     # properties are added (e.g. 'pos' and 'size'). The values of these
373     # may not be correct yet, but we add placeholders so that the
374     # size of the device tree is correct. Later, in
375     # SetCalculatedProperties() we will insert the correct values
376     # without changing the device-tree size, thus ensuring that our
377     # entry offsets remain the same.
378     for image in images.values():
379         image.ExpandEntries()
380         if update_fdt:
381             image.AddMissingProperties()
382         image.ProcessFdt(dtb)
383
384     for dtb_item in state.GetAllFdts():
385         dtb_item.Sync(auto_resize=True)
386         dtb_item.Pack()
387         dtb_item.Flush()
388     return images
389
390
391 def ProcessImage(image, update_fdt, write_map, get_contents=True,
392                  allow_resize=True):
393     """Perform all steps for this image, including checking and # writing it.
394
395     This means that errors found with a later image will be reported after
396     earlier images are already completed and written, but that does not seem
397     important.
398
399     Args:
400         image: Image to process
401         update_fdt: True to update the FDT wth entry offsets, etc.
402         write_map: True to write a map file
403         get_contents: True to get the image contents from files, etc., False if
404             the contents is already present
405         allow_resize: True to allow entries to change size (this does a re-pack
406             of the entries), False to raise an exception
407     """
408     if get_contents:
409         image.GetEntryContents()
410     image.GetEntryOffsets()
411
412     # We need to pack the entries to figure out where everything
413     # should be placed. This sets the offset/size of each entry.
414     # However, after packing we call ProcessEntryContents() which
415     # may result in an entry changing size. In that case we need to
416     # do another pass. Since the device tree often contains the
417     # final offset/size information we try to make space for this in
418     # AddMissingProperties() above. However, if the device is
419     # compressed we cannot know this compressed size in advance,
420     # since changing an offset from 0x100 to 0x104 (for example) can
421     # alter the compressed size of the device tree. So we need a
422     # third pass for this.
423     passes = 5
424     for pack_pass in range(passes):
425         try:
426             image.PackEntries()
427             image.CheckSize()
428             image.CheckEntries()
429         except Exception as e:
430             if write_map:
431                 fname = image.WriteMap()
432                 print("Wrote map file '%s' to show errors"  % fname)
433             raise
434         image.SetImagePos()
435         if update_fdt:
436             image.SetCalculatedProperties()
437             for dtb_item in state.GetAllFdts():
438                 dtb_item.Sync()
439                 dtb_item.Flush()
440         image.WriteSymbols()
441         sizes_ok = image.ProcessEntryContents()
442         if sizes_ok:
443             break
444         image.ResetForPack()
445     tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
446     if not sizes_ok:
447         image.Raise('Entries changed size after packing (tried %s passes)' %
448                     passes)
449
450     image.BuildImage()
451     if write_map:
452         image.WriteMap()
453
454
455 def Binman(args):
456     """The main control code for binman
457
458     This assumes that help and test options have already been dealt with. It
459     deals with the core task of building images.
460
461     Args:
462         args: Command line arguments Namespace object
463     """
464     global Image
465     global state
466
467     if args.full_help:
468         pager = os.getenv('PAGER')
469         if not pager:
470             pager = 'more'
471         fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
472                             'README')
473         command.Run(pager, fname)
474         return 0
475
476     # Put these here so that we can import this module without libfdt
477     from image import Image
478     import state
479
480     if args.cmd in ['ls', 'extract', 'replace']:
481         try:
482             tout.Init(args.verbosity)
483             tools.PrepareOutputDir(None)
484             if args.cmd == 'ls':
485                 ListEntries(args.image, args.paths)
486
487             if args.cmd == 'extract':
488                 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
489                                not args.uncompressed)
490
491             if args.cmd == 'replace':
492                 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
493                                do_compress=not args.compressed,
494                                allow_resize=not args.fix_size, write_map=args.map)
495         except:
496             raise
497         finally:
498             tools.FinaliseOutputDir()
499         return 0
500
501     # Try to figure out which device tree contains our image description
502     if args.dt:
503         dtb_fname = args.dt
504     else:
505         board = args.board
506         if not board:
507             raise ValueError('Must provide a board to process (use -b <board>)')
508         board_pathname = os.path.join(args.build_dir, board)
509         dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
510         if not args.indir:
511             args.indir = ['.']
512         args.indir.append(board_pathname)
513
514     try:
515         tout.Init(args.verbosity)
516         elf.debug = args.debug
517         cbfs_util.VERBOSE = args.verbosity > 2
518         state.use_fake_dtb = args.fake_dtb
519         try:
520             tools.SetInputDirs(args.indir)
521             tools.PrepareOutputDir(args.outdir, args.preserve)
522             tools.SetToolPaths(args.toolpath)
523             state.SetEntryArgs(args.entry_arg)
524
525             images = PrepareImagesAndDtbs(dtb_fname, args.image,
526                                           args.update_fdt)
527             for image in images.values():
528                 ProcessImage(image, args.update_fdt, args.map)
529
530             # Write the updated FDTs to our output files
531             for dtb_item in state.GetAllFdts():
532                 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
533
534         finally:
535             tools.FinaliseOutputDir()
536     finally:
537         tout.Uninit()
538
539     return 0