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 collections import OrderedDict 9import glob 10import os 11import pkg_resources 12import re 13 14import sys 15from patman import tools 16 17from binman import cbfs_util 18from binman import elf 19from patman import command 20from patman import tout 21 22# List of images we plan to create 23# Make this global so that it can be referenced from tests 24images = 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 29missing_blob_help = {} 30 31def _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 52def _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 65def _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 100def _ShowBlobHelp(path, text): 101 tout.Warning('\n%s:' % path) 102 for line in text.splitlines(): 103 tout.Warning(' %s' % line) 104 105def _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 125def 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 137def 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 150def 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 181def 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 203def ExtractEntries(image_fname, output_fname, outdir, entry_paths, 204 decomp=True): 205 """Extract the data from one or more entries and write it to files 206 207 Args: 208 image_fname: Image filename to process 209 output_fname: Single output filename to use if extracting one file, None 210 otherwise 211 outdir: Output directory to use (for any number of files), else None 212 entry_paths: List of entry paths to extract 213 decomp: True to decompress the entry data 214 215 Returns: 216 List of EntryInfo records that were written 217 """ 218 image = Image.FromFile(image_fname) 219 220 # Output an entry to a single file, as a special case 221 if output_fname: 222 if not entry_paths: 223 raise ValueError('Must specify an entry path to write with -f') 224 if len(entry_paths) != 1: 225 raise ValueError('Must specify exactly one entry path to write with -f') 226 entry = image.FindEntryPath(entry_paths[0]) 227 data = entry.ReadData(decomp) 228 tools.WriteFile(output_fname, data) 229 tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname)) 230 return 231 232 # Otherwise we will output to a path given by the entry path of each entry. 233 # This means that entries will appear in subdirectories if they are part of 234 # a sub-section. 235 einfos = image.GetListEntries(entry_paths)[0] 236 tout.Notice('%d entries match and will be written' % len(einfos)) 237 for einfo in einfos: 238 entry = einfo.entry 239 data = entry.ReadData(decomp) 240 path = entry.GetPath()[1:] 241 fname = os.path.join(outdir, path) 242 243 # If this entry has children, create a directory for it and put its 244 # data in a file called 'root' in that directory 245 if entry.GetEntries(): 246 if fname and not os.path.exists(fname): 247 os.makedirs(fname) 248 fname = os.path.join(fname, 'root') 249 tout.Notice("Write entry '%s' size %x to '%s'" % 250 (entry.GetPath(), len(data), fname)) 251 tools.WriteFile(fname, data) 252 return einfos 253 254 255def BeforeReplace(image, allow_resize): 256 """Handle getting an image ready for replacing entries in it 257 258 Args: 259 image: Image to prepare 260 """ 261 state.PrepareFromLoadedData(image) 262 image.LoadData() 263 264 # If repacking, drop the old offset/size values except for the original 265 # ones, so we are only left with the constraints. 266 if allow_resize: 267 image.ResetForPack() 268 269 270def ReplaceOneEntry(image, entry, data, do_compress, allow_resize): 271 """Handle replacing a single entry an an image 272 273 Args: 274 image: Image to update 275 entry: Entry to write 276 data: Data to replace with 277 do_compress: True to compress the data if needed, False if data is 278 already compressed so should be used as is 279 allow_resize: True to allow entries to change size (this does a re-pack 280 of the entries), False to raise an exception 281 """ 282 if not entry.WriteData(data, do_compress): 283 if not image.allow_repack: 284 entry.Raise('Entry data size does not match, but allow-repack is not present for this image') 285 if not allow_resize: 286 entry.Raise('Entry data size does not match, but resize is disabled') 287 288 289def AfterReplace(image, allow_resize, write_map): 290 """Handle write out an image after replacing entries in it 291 292 Args: 293 image: Image to write 294 allow_resize: True to allow entries to change size (this does a re-pack 295 of the entries), False to raise an exception 296 write_map: True to write a map file 297 """ 298 tout.Info('Processing image') 299 ProcessImage(image, update_fdt=True, write_map=write_map, 300 get_contents=False, allow_resize=allow_resize) 301 302 303def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True, 304 write_map=False): 305 BeforeReplace(image, allow_resize) 306 tout.Info('Writing data to %s' % entry.GetPath()) 307 ReplaceOneEntry(image, entry, data, do_compress, allow_resize) 308 AfterReplace(image, allow_resize=allow_resize, write_map=write_map) 309 310 311def WriteEntry(image_fname, entry_path, data, do_compress=True, 312 allow_resize=True, write_map=False): 313 """Replace an entry in an image 314 315 This replaces the data in a particular entry in an image. This size of the 316 new data must match the size of the old data unless allow_resize is True. 317 318 Args: 319 image_fname: Image filename to process 320 entry_path: Path to entry to extract 321 data: Data to replace with 322 do_compress: True to compress the data if needed, False if data is 323 already compressed so should be used as is 324 allow_resize: True to allow entries to change size (this does a re-pack 325 of the entries), False to raise an exception 326 write_map: True to write a map file 327 328 Returns: 329 Image object that was updated 330 """ 331 tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname)) 332 image = Image.FromFile(image_fname) 333 entry = image.FindEntryPath(entry_path) 334 WriteEntryToImage(image, entry, data, do_compress=do_compress, 335 allow_resize=allow_resize, write_map=write_map) 336 337 return image 338 339 340def ReplaceEntries(image_fname, input_fname, indir, entry_paths, 341 do_compress=True, allow_resize=True, write_map=False): 342 """Replace the data from one or more entries from input files 343 344 Args: 345 image_fname: Image filename to process 346 input_fname: Single input ilename to use if replacing one file, None 347 otherwise 348 indir: Input directory to use (for any number of files), else None 349 entry_paths: List of entry paths to extract 350 do_compress: True if the input data is uncompressed and may need to be 351 compressed if the entry requires it, False if the data is already 352 compressed. 353 write_map: True to write a map file 354 355 Returns: 356 List of EntryInfo records that were written 357 """ 358 image = Image.FromFile(image_fname) 359 360 # Replace an entry from a single file, as a special case 361 if input_fname: 362 if not entry_paths: 363 raise ValueError('Must specify an entry path to read with -f') 364 if len(entry_paths) != 1: 365 raise ValueError('Must specify exactly one entry path to write with -f') 366 entry = image.FindEntryPath(entry_paths[0]) 367 data = tools.ReadFile(input_fname) 368 tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname)) 369 WriteEntryToImage(image, entry, data, do_compress=do_compress, 370 allow_resize=allow_resize, write_map=write_map) 371 return 372 373 # Otherwise we will input from a path given by the entry path of each entry. 374 # This means that files must appear in subdirectories if they are part of 375 # a sub-section. 376 einfos = image.GetListEntries(entry_paths)[0] 377 tout.Notice("Replacing %d matching entries in image '%s'" % 378 (len(einfos), image_fname)) 379 380 BeforeReplace(image, allow_resize) 381 382 for einfo in einfos: 383 entry = einfo.entry 384 if entry.GetEntries(): 385 tout.Info("Skipping section entry '%s'" % entry.GetPath()) 386 continue 387 388 path = entry.GetPath()[1:] 389 fname = os.path.join(indir, path) 390 391 if os.path.exists(fname): 392 tout.Notice("Write entry '%s' from file '%s'" % 393 (entry.GetPath(), fname)) 394 data = tools.ReadFile(fname) 395 ReplaceOneEntry(image, entry, data, do_compress, allow_resize) 396 else: 397 tout.Warning("Skipping entry '%s' from missing file '%s'" % 398 (entry.GetPath(), fname)) 399 400 AfterReplace(image, allow_resize=allow_resize, write_map=write_map) 401 return image 402 403 404def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded): 405 """Prepare the images to be processed and select the device tree 406 407 This function: 408 - reads in the device tree 409 - finds and scans the binman node to create all entries 410 - selects which images to build 411 - Updates the device tress with placeholder properties for offset, 412 image-pos, etc. 413 414 Args: 415 dtb_fname: Filename of the device tree file to use (.dts or .dtb) 416 selected_images: List of images to output, or None for all 417 update_fdt: True to update the FDT wth entry offsets, etc. 418 use_expanded: True to use expanded versions of entries, if available. 419 So if 'u-boot' is called for, we use 'u-boot-expanded' instead. This 420 is needed if update_fdt is True (although tests may disable it) 421 422 Returns: 423 OrderedDict of images: 424 key: Image name (str) 425 value: Image object 426 """ 427 # Import these here in case libfdt.py is not available, in which case 428 # the above help option still works. 429 from dtoc import fdt 430 from dtoc import fdt_util 431 global images 432 433 # Get the device tree ready by compiling it and copying the compiled 434 # output into a file in our output directly. Then scan it for use 435 # in binman. 436 dtb_fname = fdt_util.EnsureCompiled(dtb_fname) 437 fname = tools.GetOutputFilename('u-boot.dtb.out') 438 tools.WriteFile(fname, tools.ReadFile(dtb_fname)) 439 dtb = fdt.FdtScan(fname) 440 441 node = _FindBinmanNode(dtb) 442 if not node: 443 raise ValueError("Device tree '%s' does not have a 'binman' " 444 "node" % dtb_fname) 445 446 images = _ReadImageDesc(node, use_expanded) 447 448 if select_images: 449 skip = [] 450 new_images = OrderedDict() 451 for name, image in images.items(): 452 if name in select_images: 453 new_images[name] = image 454 else: 455 skip.append(name) 456 images = new_images 457 tout.Notice('Skipping images: %s' % ', '.join(skip)) 458 459 state.Prepare(images, dtb) 460 461 # Prepare the device tree by making sure that any missing 462 # properties are added (e.g. 'pos' and 'size'). The values of these 463 # may not be correct yet, but we add placeholders so that the 464 # size of the device tree is correct. Later, in 465 # SetCalculatedProperties() we will insert the correct values 466 # without changing the device-tree size, thus ensuring that our 467 # entry offsets remain the same. 468 for image in images.values(): 469 image.ExpandEntries() 470 if update_fdt: 471 image.AddMissingProperties(True) 472 image.ProcessFdt(dtb) 473 474 for dtb_item in state.GetAllFdts(): 475 dtb_item.Sync(auto_resize=True) 476 dtb_item.Pack() 477 dtb_item.Flush() 478 return images 479 480 481def ProcessImage(image, update_fdt, write_map, get_contents=True, 482 allow_resize=True, allow_missing=False): 483 """Perform all steps for this image, including checking and # writing it. 484 485 This means that errors found with a later image will be reported after 486 earlier images are already completed and written, but that does not seem 487 important. 488 489 Args: 490 image: Image to process 491 update_fdt: True to update the FDT wth entry offsets, etc. 492 write_map: True to write a map file 493 get_contents: True to get the image contents from files, etc., False if 494 the contents is already present 495 allow_resize: True to allow entries to change size (this does a re-pack 496 of the entries), False to raise an exception 497 allow_missing: Allow blob_ext objects to be missing 498 499 Returns: 500 True if one or more external blobs are missing, False if all are present 501 """ 502 if get_contents: 503 image.SetAllowMissing(allow_missing) 504 image.GetEntryContents() 505 image.GetEntryOffsets() 506 507 # We need to pack the entries to figure out where everything 508 # should be placed. This sets the offset/size of each entry. 509 # However, after packing we call ProcessEntryContents() which 510 # may result in an entry changing size. In that case we need to 511 # do another pass. Since the device tree often contains the 512 # final offset/size information we try to make space for this in 513 # AddMissingProperties() above. However, if the device is 514 # compressed we cannot know this compressed size in advance, 515 # since changing an offset from 0x100 to 0x104 (for example) can 516 # alter the compressed size of the device tree. So we need a 517 # third pass for this. 518 passes = 5 519 for pack_pass in range(passes): 520 try: 521 image.PackEntries() 522 except Exception as e: 523 if write_map: 524 fname = image.WriteMap() 525 print("Wrote map file '%s' to show errors" % fname) 526 raise 527 image.SetImagePos() 528 if update_fdt: 529 image.SetCalculatedProperties() 530 for dtb_item in state.GetAllFdts(): 531 dtb_item.Sync() 532 dtb_item.Flush() 533 image.WriteSymbols() 534 sizes_ok = image.ProcessEntryContents() 535 if sizes_ok: 536 break 537 image.ResetForPack() 538 tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1)) 539 if not sizes_ok: 540 image.Raise('Entries changed size after packing (tried %s passes)' % 541 passes) 542 543 image.BuildImage() 544 if write_map: 545 image.WriteMap() 546 missing_list = [] 547 image.CheckMissing(missing_list) 548 if missing_list: 549 tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" % 550 (image.name, ' '.join([e.name for e in missing_list]))) 551 _ShowHelpForMissingBlobs(missing_list) 552 return bool(missing_list) 553 554 555def Binman(args): 556 """The main control code for binman 557 558 This assumes that help and test options have already been dealt with. It 559 deals with the core task of building images. 560 561 Args: 562 args: Command line arguments Namespace object 563 """ 564 global Image 565 global state 566 567 if args.full_help: 568 tools.PrintFullHelp( 569 os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'README.rst') 570 ) 571 return 0 572 573 # Put these here so that we can import this module without libfdt 574 from binman.image import Image 575 from binman import state 576 577 if args.cmd in ['ls', 'extract', 'replace']: 578 try: 579 tout.Init(args.verbosity) 580 tools.PrepareOutputDir(None) 581 if args.cmd == 'ls': 582 ListEntries(args.image, args.paths) 583 584 if args.cmd == 'extract': 585 ExtractEntries(args.image, args.filename, args.outdir, args.paths, 586 not args.uncompressed) 587 588 if args.cmd == 'replace': 589 ReplaceEntries(args.image, args.filename, args.indir, args.paths, 590 do_compress=not args.compressed, 591 allow_resize=not args.fix_size, write_map=args.map) 592 except: 593 raise 594 finally: 595 tools.FinaliseOutputDir() 596 return 0 597 598 # Try to figure out which device tree contains our image description 599 if args.dt: 600 dtb_fname = args.dt 601 else: 602 board = args.board 603 if not board: 604 raise ValueError('Must provide a board to process (use -b <board>)') 605 board_pathname = os.path.join(args.build_dir, board) 606 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb') 607 if not args.indir: 608 args.indir = ['.'] 609 args.indir.append(board_pathname) 610 611 try: 612 tout.Init(args.verbosity) 613 elf.debug = args.debug 614 cbfs_util.VERBOSE = args.verbosity > 2 615 state.use_fake_dtb = args.fake_dtb 616 617 # Normally we replace the 'u-boot' etype with 'u-boot-expanded', etc. 618 # When running tests this can be disabled using this flag. When not 619 # updating the FDT in image, it is not needed by binman, but we use it 620 # for consistency, so that the images look the same to U-Boot at 621 # runtime. 622 use_expanded = not args.no_expanded 623 try: 624 tools.SetInputDirs(args.indir) 625 tools.PrepareOutputDir(args.outdir, args.preserve) 626 tools.SetToolPaths(args.toolpath) 627 state.SetEntryArgs(args.entry_arg) 628 state.SetThreads(args.threads) 629 630 images = PrepareImagesAndDtbs(dtb_fname, args.image, 631 args.update_fdt, use_expanded) 632 if args.test_section_timeout: 633 # Set the first image to timeout, used in testThreadTimeout() 634 images[list(images.keys())[0]].test_section_timeout = True 635 missing = False 636 for image in images.values(): 637 missing |= ProcessImage(image, args.update_fdt, args.map, 638 allow_missing=args.allow_missing) 639 640 # Write the updated FDTs to our output files 641 for dtb_item in state.GetAllFdts(): 642 tools.WriteFile(dtb_item._fname, dtb_item.GetContents()) 643 644 if missing: 645 tout.Warning("\nSome images are invalid") 646 647 # Use this to debug the time take to pack the image 648 #state.TimingShow() 649 finally: 650 tools.FinaliseOutputDir() 651 finally: 652 tout.Uninit() 653 654 return 0 655