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