binman: Allow use of help and entry-docs without libfdt
[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         sizes_ok = image.ProcessEntryContents()
441         if sizes_ok:
442             break
443         image.ResetForPack()
444     if not sizes_ok:
445         image.Raise('Entries changed size after packing (tried %s passes)' %
446                     passes)
447
448     image.WriteSymbols()
449     image.BuildImage()
450     if write_map:
451         image.WriteMap()
452
453
454 def Binman(args):
455     """The main control code for binman
456
457     This assumes that help and test options have already been dealt with. It
458     deals with the core task of building images.
459
460     Args:
461         args: Command line arguments Namespace object
462     """
463     global Image
464     global state
465
466     if args.full_help:
467         pager = os.getenv('PAGER')
468         if not pager:
469             pager = 'more'
470         fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
471                             'README')
472         command.Run(pager, fname)
473         return 0
474
475     # Put these here so that we can import this module without libfdt
476     from image import Image
477     import state
478
479     if args.cmd in ['ls', 'extract', 'replace']:
480         try:
481             tout.Init(args.verbosity)
482             tools.PrepareOutputDir(None)
483             if args.cmd == 'ls':
484                 ListEntries(args.image, args.paths)
485
486             if args.cmd == 'extract':
487                 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
488                                not args.uncompressed)
489
490             if args.cmd == 'replace':
491                 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
492                                do_compress=not args.compressed,
493                                allow_resize=not args.fix_size, write_map=args.map)
494         except:
495             raise
496         finally:
497             tools.FinaliseOutputDir()
498         return 0
499
500     # Try to figure out which device tree contains our image description
501     if args.dt:
502         dtb_fname = args.dt
503     else:
504         board = args.board
505         if not board:
506             raise ValueError('Must provide a board to process (use -b <board>)')
507         board_pathname = os.path.join(args.build_dir, board)
508         dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
509         if not args.indir:
510             args.indir = ['.']
511         args.indir.append(board_pathname)
512
513     try:
514         tout.Init(args.verbosity)
515         elf.debug = args.debug
516         cbfs_util.VERBOSE = args.verbosity > 2
517         state.use_fake_dtb = args.fake_dtb
518         try:
519             tools.SetInputDirs(args.indir)
520             tools.PrepareOutputDir(args.outdir, args.preserve)
521             tools.SetToolPaths(args.toolpath)
522             state.SetEntryArgs(args.entry_arg)
523
524             images = PrepareImagesAndDtbs(dtb_fname, args.image,
525                                           args.update_fdt)
526             for image in images.values():
527                 ProcessImage(image, args.update_fdt, args.map)
528
529             # Write the updated FDTs to our output files
530             for dtb_item in state.GetAllFdts():
531                 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
532
533         finally:
534             tools.FinaliseOutputDir()
535     finally:
536         tout.Uninit()
537
538     return 0