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