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 8from __future__ import print_function 9 10from collections import OrderedDict 11import os 12import sys 13import tools 14 15import cbfs_util 16import command 17import elf 18import tout 19 20# List of images we plan to create 21# Make this global so that it can be referenced from tests 22images = OrderedDict() 23 24def _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 43def _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 56def 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 69def 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 100def 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 122def 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 173def 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 188def 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 207def 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 221def 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 229def 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 258def 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 322def 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 391def 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 455def 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 540