1#!/usr/bin/python2.6 2# 3# CDDL HEADER START 4# 5# The contents of this file are subject to the terms of the 6# Common Development and Distribution License (the "License"). 7# You may not use this file except in compliance with the License. 8# 9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10# or http://www.opensolaris.org/os/licensing. 11# See the License for the specific language governing permissions 12# and limitations under the License. 13# 14# When distributing Covered Code, include this CDDL HEADER in each 15# file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16# If applicable, add the following below this CDDL HEADER, with the 17# fields enclosed by brackets "[]" replaced with your own identifying 18# information: Portions Copyright [yyyy] [name of copyright owner] 19# 20# CDDL HEADER END 21# 22# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. 23# 24 25# 26# wsdiff(1) is a tool that can be used to determine which compiled objects 27# have changed as a result of a given source change. Developers backporting 28# new features, RFEs and bug fixes need to be able to identify the set of 29# patch deliverables necessary for feature/fix realization on a patched system. 30# 31# The tool works by comparing objects in two trees/proto areas (one build with, 32# and without the source changes. 33# 34# Using wsdiff(1) is fairly simple: 35# - Bringover to a fresh workspace 36# - Perform a full non-debug build (clobber if workspace isn't fresh) 37# - Move the proto area aside, call it proto.old, or something. 38# - Integrate your changes to the workspace 39# - Perform another full non-debug clobber build. 40# - Use wsdiff(1) to see what changed: 41# $ wsdiff proto.old proto 42# 43# By default, wsdiff will print the list of changed objects / deliverables to 44# stdout. If a results file is specified via -r, the list of differing objects, 45# and details about why wsdiff(1) thinks they are different will be logged to 46# the results file. 47# 48# By invoking nightly(1) with the -w option to NIGHTLY_FLAGS, nightly(1) will use 49# wsdiff(1) to report on what objects changed since the last build. 50# 51# For patch deliverable purposes, it's advised to have nightly do a clobber, 52# non-debug build. 53# 54# Think about the results. Was something flagged that you don't expect? Go look 55# at the results file to see details about the differences. 56# 57# Use the -i option in conjunction with -v and -V to dive deeper and have wsdiff(1) 58# report with more verbosity. 59# 60# Usage: wsdiff [-vVt] [-r results ] [-i filelist ] old new 61# 62# Where "old" is the path to the proto area build without the changes, and 63# "new" is the path to the proto area built with the changes. The following 64# options are supported: 65# 66# -v Do not truncate observed diffs in results 67# -V Log *all* ELF sect diffs vs. logging the first diff found 68# -t Use onbld tools in $SRC/tools 69# -r Log results and observed differences 70# -i Tell wsdiff which objects to compare via an input file list 71 72import datetime, fnmatch, getopt, os, profile, commands 73import re, resource, select, shutil, signal, string, struct, sys, tempfile 74import time, threading 75from stat import * 76 77# Human readable diffs truncated by default if longer than this 78# Specifying -v on the command line will override 79diffs_sz_thresh = 4096 80 81# Lock name Provides exclusive access to 82# --------------+------------------------------------------------ 83# output_lock standard output or temporary file (difference()) 84# log_lock the results file (log_difference()) 85# wset_lock changedFiles list (workerThread()) 86output_lock = threading.Lock() 87log_lock = threading.Lock() 88wset_lock = threading.Lock() 89 90# Variable for thread control 91keep_processing = True 92 93# Default search path for wsdiff 94wsdiff_path = [ "/usr/bin", 95 "/usr/ccs/bin", 96 "/lib/svc/bin", 97 "/opt/onbld/bin" ] 98 99# These are objects that wsdiff will notice look different, but will not report. 100# Existence of an exceptions list, and adding things here is *dangerous*, 101# and therefore the *only* reasons why anything would be listed here is because 102# the objects do not build deterministically, yet we *cannot* fix this. 103# 104# These perl libraries use __DATE__ and therefore always look different. 105# Ideally, we would purge use the use of __DATE__ from the source, but because 106# this is source we wish to distribute with Solaris "unchanged", we cannot modify. 107# 108wsdiff_exceptions = [ "usr/perl5/5.8.4/lib/sun4-solaris-64int/CORE/libperl.so.1", 109 "usr/perl5/5.6.1/lib/sun4-solaris-64int/CORE/libperl.so.1", 110 "usr/perl5/5.8.4/lib/i86pc-solaris-64int/CORE/libperl.so.1", 111 "usr/perl5/5.6.1/lib/i86pc-solaris-64int/CORE/libperl.so.1" 112 ] 113 114##### 115# Logging routines 116# 117 118# Debug message to be printed to the screen, and the log file 119def debug(msg) : 120 121 # Add prefix to highlight debugging message 122 msg = "## " + msg 123 if debugon : 124 output_lock.acquire() 125 print >> sys.stdout, msg 126 sys.stdout.flush() 127 output_lock.release() 128 if logging : 129 log_lock.acquire() 130 print >> log, msg 131 log.flush() 132 log_lock.release() 133 134# Informational message to be printed to the screen, and the log file 135def info(msg) : 136 137 output_lock.acquire() 138 print >> sys.stdout, msg 139 sys.stdout.flush() 140 output_lock.release() 141 if logging : 142 log_lock.acquire() 143 print >> log, msg 144 log.flush() 145 log_lock.release() 146 147# Error message to be printed to the screen, and the log file 148def error(msg) : 149 150 output_lock.acquire() 151 print >> sys.stderr, "ERROR:", msg 152 sys.stderr.flush() 153 output_lock.release() 154 if logging : 155 log_lock.acquire() 156 print >> log, "ERROR:", msg 157 log.flush() 158 log_lock.release() 159 160# Informational message to be printed only to the log, if there is one. 161def v_info(msg) : 162 163 if logging : 164 log_lock.acquire() 165 print >> log, msg 166 log.flush() 167 log_lock.release() 168 169# 170# Flag a detected file difference 171# Display the fileName to stdout, and log the difference 172# 173def difference(f, dtype, diffs) : 174 175 if f in wsdiff_exceptions : 176 return 177 178 output_lock.acquire() 179 if sorted : 180 differentFiles.append(f) 181 else: 182 print >> sys.stdout, f 183 sys.stdout.flush() 184 output_lock.release() 185 186 log_difference(f, dtype, diffs) 187 188# 189# Do the actual logging of the difference to the results file 190# 191def log_difference(f, dtype, diffs) : 192 193 if logging : 194 log_lock.acquire() 195 print >> log, f 196 print >> log, "NOTE:", dtype, "difference detected." 197 198 difflen = len(diffs) 199 if difflen > 0 : 200 print >> log 201 202 if not vdiffs and difflen > diffs_sz_thresh : 203 print >> log, diffs[:diffs_sz_thresh] 204 print >> log, \ 205 "... truncated due to length: " \ 206 "use -v to override ..." 207 else : 208 print >> log, diffs 209 print >> log, "\n" 210 log.flush() 211 log_lock.release() 212 213 214##### 215# diff generating routines 216# 217 218# 219# Return human readable diffs from two temporary files 220# 221def diffFileData(tmpf1, tmpf2) : 222 223 binaries = False 224 225 # Filter the data through od(1) if the data is detected 226 # as being binary 227 if isBinary(tmpf1) or isBinary(tmpf2) : 228 binaries = True 229 tmp_od1 = tmpf1 + ".od" 230 tmp_od2 = tmpf2 + ".od" 231 232 cmd = od_cmd + " -c -t x4" + " " + tmpf1 + " > " + tmp_od1 233 os.system(cmd) 234 cmd = od_cmd + " -c -t x4" + " " + tmpf2 + " > " + tmp_od2 235 os.system(cmd) 236 237 tmpf1 = tmp_od1 238 tmpf2 = tmp_od2 239 240 try: 241 data = commands.getoutput(diff_cmd + " " + tmpf1 + " " + tmpf2) 242 # Remove the temp files as we no longer need them. 243 if binaries : 244 try: 245 os.unlink(tmp_od1) 246 except OSError, e: 247 error("diffFileData: unlink failed %s" % e) 248 try: 249 os.unlink(tmp_od2) 250 except OSError, e: 251 error("diffFileData: unlink failed %s" % e) 252 except: 253 error("failed to get output of command: " + diff_cmd + " " \ 254 + tmpf1 + " " + tmpf2) 255 256 # Send exception for the failed command up 257 raise 258 return 259 260 return data 261 262# 263# Return human readable diffs betweeen two datasets 264# 265def diffData(base, ptch, d1, d2) : 266 267 t = threading.currentThread() 268 tmpFile1 = tmpDir1 + os.path.basename(base) + t.getName() 269 tmpFile2 = tmpDir2 + os.path.basename(ptch) + t.getName() 270 271 try: 272 fd1 = open(tmpFile1, "w") 273 except: 274 error("failed to open: " + tmpFile1) 275 cleanup(1) 276 277 try: 278 fd2 = open(tmpFile2, "w") 279 except: 280 error("failed to open: " + tmpFile2) 281 cleanup(1) 282 283 fd1.write(d1) 284 fd2.write(d2) 285 fd1.close() 286 fd2.close() 287 288 return diffFileData(tmpFile1, tmpFile2) 289 290##### 291# Misc utility functions 292# 293 294# Prune off the leading prefix from string s 295def str_prefix_trunc(s, prefix) : 296 snipLen = len(prefix) 297 return s[snipLen:] 298 299# 300# Prune off leading proto path goo (if there is one) to yield 301# the deliverable's eventual path relative to root 302# e.g. proto.base/root_sparc/usr/src/cmd/prstat => usr/src/cmd/prstat 303# 304def fnFormat(fn) : 305 root_arch_str = "root_" + arch 306 307 pos = fn.find(root_arch_str) 308 if pos == -1 : 309 return fn 310 311 pos = fn.find("/", pos) 312 if pos == -1 : 313 return fn 314 315 return fn[pos + 1:] 316 317##### 318# Usage / argument processing 319# 320 321# 322# Display usage message 323# 324def usage() : 325 sys.stdout.flush() 326 print >> sys.stderr, """Usage: wsdiff [-dvVst] [-r results ] [-i filelist ] old new 327 -d Print debug messages about the progress 328 -v Do not truncate observed diffs in results 329 -V Log *all* ELF sect diffs vs. logging the first diff found 330 -t Use onbld tools in $SRC/tools 331 -r Log results and observed differences 332 -s Produce sorted list of differences 333 -i Tell wsdiff which objects to compare via an input file list""" 334 sys.exit(1) 335 336# 337# Process command line options 338# 339def args() : 340 341 global debugon 342 global logging 343 global vdiffs 344 global reportAllSects 345 global sorted 346 347 validOpts = 'di:r:vVst?' 348 349 baseRoot = "" 350 ptchRoot = "" 351 fileNamesFile = "" 352 results = "" 353 localTools = False 354 355 # getopt.getopt() returns: 356 # an option/value tuple 357 # a list of remaining non-option arguments 358 # 359 # A correct wsdiff invocation will have exactly two non option 360 # arguments, the paths to the base (old), ptch (new) proto areas 361 try: 362 optlist, args = getopt.getopt(sys.argv[1:], validOpts) 363 except getopt.error, val: 364 usage() 365 366 if len(args) != 2 : 367 usage(); 368 369 for opt,val in optlist : 370 if opt == '-d' : 371 debugon = True 372 elif opt == '-i' : 373 fileNamesFile = val 374 elif opt == '-r' : 375 results = val 376 logging = True 377 elif opt == '-s' : 378 sorted = True 379 elif opt == '-v' : 380 vdiffs = True 381 elif opt == '-V' : 382 reportAllSects = True 383 elif opt == '-t': 384 localTools = True 385 else: 386 usage() 387 388 baseRoot = args[0] 389 ptchRoot = args[1] 390 391 if len(baseRoot) == 0 or len(ptchRoot) == 0 : 392 usage() 393 394 if logging and len(results) == 0 : 395 usage() 396 397 if vdiffs and not logging : 398 error("The -v option requires a results file (-r)") 399 sys.exit(1) 400 401 if reportAllSects and not logging : 402 error("The -V option requires a results file (-r)") 403 sys.exit(1) 404 405 # alphabetical order 406 return baseRoot, fileNamesFile, localTools, ptchRoot, results 407 408##### 409# File identification 410# 411 412# 413# Identify the file type. 414# If it's not ELF, use the file extension to identify 415# certain file types that require special handling to 416# compare. Otherwise just return a basic "ASCII" type. 417# 418def getTheFileType(f) : 419 420 extensions = { 'a' : 'ELF Object Archive', 421 'jar' : 'Java Archive', 422 'html' : 'HTML', 423 'ln' : 'Lint Library', 424 'db' : 'Sqlite Database' } 425 426 try: 427 if os.stat(f)[ST_SIZE] == 0 : 428 return 'ASCII' 429 except: 430 error("failed to stat " + f) 431 return 'Error' 432 433 if isELF(f) == 1 : 434 return 'ELF' 435 436 fnamelist = f.split('.') 437 if len(fnamelist) > 1 : # Test the file extension 438 extension = fnamelist[-1] 439 if extension in extensions.keys(): 440 return extensions[extension] 441 442 return 'ASCII' 443 444# 445# Return non-zero if "f" is an ELF file 446# 447elfmagic = '\177ELF' 448def isELF(f) : 449 try: 450 fd = open(f) 451 except: 452 error("failed to open: " + f) 453 return 0 454 magic = fd.read(len(elfmagic)) 455 fd.close() 456 457 if magic == elfmagic : 458 return 1 459 return 0 460 461# 462# Return non-zero is "f" is binary. 463# Consider the file to be binary if it contains any null characters 464# 465def isBinary(f) : 466 try: 467 fd = open(f) 468 except: 469 error("failed to open: " + f) 470 return 0 471 s = fd.read() 472 fd.close() 473 474 if s.find('\0') == -1 : 475 return 0 476 else : 477 return 1 478 479##### 480# Directory traversal and file finding 481# 482 483# 484# Return a sorted list of files found under the specified directory 485# 486def findFiles(d) : 487 for path, subdirs, files in os.walk(d) : 488 files.sort() 489 for name in files : 490 yield os.path.join(path, name) 491 492# 493# Examine all files in base, ptch 494# 495# Return a list of files appearing in both proto areas, 496# a list of new files (files found only in ptch) and 497# a list of deleted files (files found only in base) 498# 499def protoCatalog(base, ptch) : 500 501 compFiles = [] # List of files in both proto areas 502 ptchList = [] # List of file in patch proto area 503 504 newFiles = [] # New files detected 505 deletedFiles = [] # Deleted files 506 507 debug("Getting the list of files in the base area"); 508 baseFilesList = list(findFiles(base)) 509 baseStringLength = len(base) 510 debug("Found " + str(len(baseFilesList)) + " files") 511 512 debug("Getting the list of files in the patch area"); 513 ptchFilesList = list(findFiles(ptch)) 514 ptchStringLength = len(ptch) 515 debug("Found " + str(len(ptchFilesList)) + " files") 516 517 # Inventory files in the base proto area 518 debug("Determining the list of regular files in the base area"); 519 for fn in baseFilesList : 520 if os.path.islink(fn) : 521 continue 522 523 fileName = fn[baseStringLength:] 524 compFiles.append(fileName) 525 debug("Found " + str(len(compFiles)) + " files") 526 527 # Inventory files in the patch proto area 528 debug("Determining the list of regular files in the patch area"); 529 for fn in ptchFilesList : 530 if os.path.islink(fn) : 531 continue 532 533 fileName = fn[ptchStringLength:] 534 ptchList.append(fileName) 535 debug("Found " + str(len(ptchList)) + " files") 536 537 # Deleted files appear in the base area, but not the patch area 538 debug("Searching for deleted files by comparing the lists") 539 for fileName in compFiles : 540 if not fileName in ptchList : 541 deletedFiles.append(fileName) 542 debug("Found " + str(len(deletedFiles)) + " deleted files") 543 544 # Eliminate "deleted" files from the list of objects appearing 545 # in both the base and patch proto areas 546 debug("Eliminating deleted files from the list of objects") 547 for fileName in deletedFiles : 548 try: 549 compFiles.remove(fileName) 550 except: 551 error("filelist.remove() failed") 552 debug("List for comparison reduced to " + str(len(compFiles)) \ 553 + " files") 554 555 # New files appear in the patch area, but not the base 556 debug("Getting the list of newly added files") 557 for fileName in ptchList : 558 if not fileName in compFiles : 559 newFiles.append(fileName) 560 debug("Found " + str(len(newFiles)) + " new files") 561 562 return compFiles, newFiles, deletedFiles 563 564# 565# Examine the files listed in the input file list 566# 567# Return a list of files appearing in both proto areas, 568# a list of new files (files found only in ptch) and 569# a list of deleted files (files found only in base) 570# 571def flistCatalog(base, ptch, flist) : 572 compFiles = [] # List of files in both proto areas 573 newFiles = [] # New files detected 574 deletedFiles = [] # Deleted files 575 576 try: 577 fd = open(flist, "r") 578 except: 579 error("could not open: " + flist) 580 cleanup(1) 581 582 files = [] 583 files = fd.readlines() 584 fd.close() 585 586 for f in files : 587 ptch_present = True 588 base_present = True 589 590 if f == '\n' : 591 continue 592 593 # the fileNames have a trailing '\n' 594 f = f.rstrip() 595 596 # The objects in the file list have paths relative 597 # to $ROOT or to the base/ptch directory specified on 598 # the command line. 599 # If it's relative to $ROOT, we'll need to add back the 600 # root_`uname -p` goo we stripped off in fnFormat() 601 if os.path.exists(base + f) : 602 fn = f; 603 elif os.path.exists(base + "root_" + arch + "/" + f) : 604 fn = "root_" + arch + "/" + f 605 else : 606 base_present = False 607 608 if base_present : 609 if not os.path.exists(ptch + fn) : 610 ptch_present = False 611 else : 612 if os.path.exists(ptch + f) : 613 fn = f 614 elif os.path.exists(ptch + "root_" + arch + "/" + f) : 615 fn = "root_" + arch + "/" + f 616 else : 617 ptch_present = False 618 619 if os.path.islink(base + fn) : # ignore links 620 base_present = False 621 if os.path.islink(ptch + fn) : 622 ptch_present = False 623 624 if base_present and ptch_present : 625 compFiles.append(fn) 626 elif base_present : 627 deletedFiles.append(fn) 628 elif ptch_present : 629 newFiles.append(fn) 630 else : 631 if os.path.islink(base + fn) and \ 632 os.path.islink(ptch + fn) : 633 continue 634 error(f + " in file list, but not in either tree. " + \ 635 "Skipping...") 636 637 return compFiles, newFiles, deletedFiles 638 639 640# 641# Build a fully qualified path to an external tool/utility. 642# Consider the default system locations. For onbld tools, if 643# the -t option was specified, we'll try to use built tools in $SRC tools, 644# and otherwise, we'll fall back on /opt/onbld/ 645# 646def find_tool(tool) : 647 648 # First, check what was passed 649 if os.path.exists(tool) : 650 return tool 651 652 # Next try in wsdiff path 653 for pdir in wsdiff_path : 654 location = pdir + "/" + tool 655 if os.path.exists(location) : 656 return location + " " 657 658 location = pdir + "/" + arch + "/" + tool 659 if os.path.exists(location) : 660 return location + " " 661 662 error("Could not find path to: " + tool); 663 sys.exit(1); 664 665 666##### 667# ELF file comparison helper routines 668# 669 670# 671# Return a dictionary of ELF section types keyed by section name 672# 673def get_elfheader(f) : 674 675 header = {} 676 677 hstring = commands.getoutput(elfdump_cmd + " -c " + f) 678 679 if len(hstring) == 0 : 680 error("Failed to dump ELF header for " + f) 681 raise 682 return 683 684 # elfdump(1) dumps the section headers with the section name 685 # following "sh_name:", and the section type following "sh_type:" 686 sections = hstring.split("Section Header") 687 for sect in sections : 688 datap = sect.find("sh_name:"); 689 if datap == -1 : 690 continue 691 section = sect[datap:].split()[1] 692 datap = sect.find("sh_type:"); 693 if datap == -1 : 694 error("Could not get type for sect: " + section + \ 695 " in " + f) 696 sh_type = sect[datap:].split()[2] 697 header[section] = sh_type 698 699 return header 700 701# 702# Extract data in the specified ELF section from the given file 703# 704def extract_elf_section(f, section) : 705 706 data = commands.getoutput(dump_cmd + " -sn " + section + " " + f) 707 708 if len(data) == 0 : 709 error(dump_cmd + "yielded no data on section " + section + \ 710 " of " + f) 711 raise 712 return 713 714 # dump(1) displays the file name to start... 715 # get past it to the data itself 716 dbegin = data.find(":") + 1 717 data = data[dbegin:]; 718 719 return (data) 720 721# 722# Return a (hopefully meaningful) human readable set of diffs 723# for the specified ELF section between f1 and f2 724# 725# Depending on the section, various means for dumping and diffing 726# the data may be employed. 727# 728text_sections = [ '.text', '.init', '.fini' ] 729def diff_elf_section(f1, f2, section, sh_type) : 730 731 t = threading.currentThread() 732 tmpFile1 = tmpDir1 + os.path.basename(f1) + t.getName() 733 tmpFile2 = tmpDir2 + os.path.basename(f2) + t.getName() 734 735 if (sh_type == "SHT_RELA") : # sh_type == SHT_RELA 736 cmd1 = elfdump_cmd + " -r " + f1 + " > " + tmpFile1 737 cmd2 = elfdump_cmd + " -r " + f2 + " > " + tmpFile2 738 elif (section == ".group") : 739 cmd1 = elfdump_cmd + " -g " + f1 + " > " + tmpFile1 740 cmd2 = elfdump_cmd + " -g " + f2 + " > " + tmpFile2 741 elif (section == ".hash") : 742 cmd1 = elfdump_cmd + " -h " + f1 + " > " + tmpFile1 743 cmd2 = elfdump_cmd + " -h " + f2 + " > " + tmpFile2 744 elif (section == ".dynamic") : 745 cmd1 = elfdump_cmd + " -d " + f1 + " > " + tmpFile1 746 cmd2 = elfdump_cmd + " -d " + f2 + " > " + tmpFile2 747 elif (section == ".got") : 748 cmd1 = elfdump_cmd + " -G " + f1 + " > " + tmpFile1 749 cmd2 = elfdump_cmd + " -G " + f2 + " > " + tmpFile2 750 elif (section == ".SUNW_cap") : 751 cmd1 = elfdump_cmd + " -H " + f1 + " > " + tmpFile1 752 cmd2 = elfdump_cmd + " -H " + f2 + " > " + tmpFile2 753 elif (section == ".interp") : 754 cmd1 = elfdump_cmd + " -i " + f1 + " > " + tmpFile1 755 cmd2 = elfdump_cmd + " -i " + f2 + " > " + tmpFile2 756 elif (section == ".symtab" or section == ".dynsym") : 757 cmd1 = elfdump_cmd + " -s -N " + section + " " + f1 + \ 758 " > " + tmpFile1 759 cmd2 = elfdump_cmd + " -s -N " + section + " " + f2 + \ 760 " > " + tmpFile2 761 elif (section in text_sections) : 762 # dis sometimes complains when it hits something it doesn't 763 # know how to disassemble. Just ignore it, as the output 764 # being generated here is human readable, and we've already 765 # correctly flagged the difference. 766 cmd1 = dis_cmd + " -t " + section + " " + f1 + \ 767 " 2>/dev/null | grep -v disassembly > " + tmpFile1 768 cmd2 = dis_cmd + " -t " + section + " " + f2 + \ 769 " 2>/dev/null | grep -v disassembly > " + tmpFile2 770 else : 771 cmd1 = elfdump_cmd + " -w " + tmpFile1 + " -N " + \ 772 section + " " + f1 773 cmd2 = elfdump_cmd + " -w " + tmpFile2 + " -N " + \ 774 section + " " + f2 775 776 os.system(cmd1) 777 os.system(cmd2) 778 779 data = diffFileData(tmpFile1, tmpFile2) 780 781 # remove temp files as we no longer need them 782 try: 783 os.unlink(tmpFile1) 784 except OSError, e: 785 error("diff_elf_section: unlink failed %s" % e) 786 try: 787 os.unlink(tmpFile2) 788 except OSError, e: 789 error("diff_elf_section: unlink failed %s" % e) 790 791 return (data) 792 793# 794# compare the relevant sections of two ELF binaries 795# and report any differences 796# 797# Returns: 1 if any differenes found 798# 0 if no differences found 799# -1 on error 800# 801 802# Sections deliberately not considered when comparing two ELF 803# binaries. Differences observed in these sections are not considered 804# significant where patch deliverable identification is concerned. 805sections_to_skip = [ ".SUNW_signature", 806 ".comment", 807 ".SUNW_ctf", 808 ".debug", 809 ".plt", 810 ".rela.bss", 811 ".rela.plt", 812 ".line", 813 ".note", 814 ".compcom", 815 ] 816 817sections_preferred = [ ".rodata.str1.8", 818 ".rodata.str1.1", 819 ".rodata", 820 ".data1", 821 ".data", 822 ".text", 823 ] 824 825def compareElfs(base, ptch, quiet) : 826 827 global logging 828 829 try: 830 base_header = get_elfheader(base) 831 except: 832 return 833 sections = base_header.keys() 834 835 try: 836 ptch_header = get_elfheader(ptch) 837 except: 838 return 839 e2_only_sections = ptch_header.keys() 840 841 e1_only_sections = [] 842 843 fileName = fnFormat(base) 844 845 # Derive the list of ELF sections found only in 846 # either e1 or e2. 847 for sect in sections : 848 if not sect in e2_only_sections : 849 e1_only_sections.append(sect) 850 else : 851 e2_only_sections.remove(sect) 852 853 if len(e1_only_sections) > 0 : 854 if quiet : 855 return 1 856 857 data = "" 858 if logging : 859 slist = "" 860 for sect in e1_only_sections : 861 slist = slist + sect + "\t" 862 data = "ELF sections found in " + \ 863 base + " but not in " + ptch + \ 864 "\n\n" + slist 865 866 difference(fileName, "ELF", data) 867 return 1 868 869 if len(e2_only_sections) > 0 : 870 if quiet : 871 return 1 872 873 data = "" 874 if logging : 875 slist = "" 876 for sect in e2_only_sections : 877 slist = slist + sect + "\t" 878 data = "ELF sections found in " + \ 879 ptch + " but not in " + base + \ 880 "\n\n" + slist 881 882 difference(fileName, "ELF", data) 883 return 1 884 885 # Look for preferred sections, and put those at the 886 # top of the list of sections to compare 887 for psect in sections_preferred : 888 if psect in sections : 889 sections.remove(psect) 890 sections.insert(0, psect) 891 892 # Compare ELF sections 893 first_section = True 894 for sect in sections : 895 896 if sect in sections_to_skip : 897 continue 898 899 try: 900 s1 = extract_elf_section(base, sect); 901 except: 902 return 903 904 try: 905 s2 = extract_elf_section(ptch, sect); 906 except: 907 return 908 909 if len(s1) != len (s2) or s1 != s2: 910 if not quiet: 911 sh_type = base_header[sect] 912 data = diff_elf_section(base, ptch, \ 913 sect, sh_type) 914 915 # If all ELF sections are being reported, then 916 # invoke difference() to flag the file name to 917 # stdout only once. Any other section differences 918 # should be logged to the results file directly 919 if not first_section : 920 log_difference(fileName, \ 921 "ELF " + sect, data) 922 else : 923 difference(fileName, "ELF " + sect, \ 924 data) 925 926 if not reportAllSects : 927 return 1 928 first_section = False 929 930 return 0 931 932##### 933# recursively remove 2 directories 934# 935# Used for removal of temporary directory strucures (ignores any errors). 936# 937def clearTmpDirs(dir1, dir2) : 938 939 if os.path.isdir(dir1) > 0 : 940 shutil.rmtree(dir1, True) 941 942 if os.path.isdir(dir2) > 0 : 943 shutil.rmtree(dir2, True) 944 945 946##### 947# Archive object comparison 948# 949# Returns 1 if difference detected 950# 0 if no difference detected 951# -1 on error 952# 953def compareArchives(base, ptch, fileType) : 954 955 fileName = fnFormat(base) 956 t = threading.currentThread() 957 ArchTmpDir1 = tmpDir1 + os.path.basename(base) + t.getName() 958 ArchTmpDir2 = tmpDir2 + os.path.basename(base) + t.getName() 959 960 # 961 # Be optimistic and first try a straight file compare 962 # as it will allow us to finish up quickly. 963 # 964 if compareBasic(base, ptch, True, fileType) == 0 : 965 return 0 966 967 try: 968 os.makedirs(ArchTmpDir1) 969 except OSError, e: 970 error("compareArchives: makedir failed %s" % e) 971 return -1 972 try: 973 os.makedirs(ArchTmpDir2) 974 except OSError, e: 975 error("compareArchives: makedir failed %s" % e) 976 return -1 977 978 # copy over the objects to the temp areas, and 979 # unpack them 980 baseCmd = "cp -fp " + base + " " + ArchTmpDir1 981 status, output = commands.getstatusoutput(baseCmd) 982 if status != 0 : 983 error(baseCmd + " failed: " + output) 984 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 985 return -1 986 987 ptchCmd = "cp -fp " + ptch + " " + ArchTmpDir2 988 status, output = commands.getstatusoutput(ptchCmd) 989 if status != 0 : 990 error(ptchCmd + " failed: " + output) 991 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 992 return -1 993 994 bname = string.split(fileName, '/')[-1] 995 if fileType == "Java Archive" : 996 baseCmd = "cd " + ArchTmpDir1 + "; " + "jar xf " + bname + \ 997 "; rm -f " + bname + " META-INF/MANIFEST.MF" 998 ptchCmd = "cd " + ArchTmpDir2 + "; " + "jar xf " + bname + \ 999 "; rm -f " + bname + " META-INF/MANIFEST.MF" 1000 elif fileType == "ELF Object Archive" : 1001 baseCmd = "cd " + ArchTmpDir1 + "; " + "/usr/ccs/bin/ar x " + \ 1002 bname + "; rm -f " + bname 1003 ptchCmd = "cd " + ArchTmpDir2 + "; " + "/usr/ccs/bin/ar x " + \ 1004 bname + "; rm -f " + bname 1005 else : 1006 error("unexpected file type: " + fileType) 1007 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1008 return -1 1009 1010 os.system(baseCmd) 1011 os.system(ptchCmd) 1012 1013 baseFlist = list(findFiles(ArchTmpDir1)) 1014 ptchFlist = list(findFiles(ArchTmpDir2)) 1015 1016 # Trim leading path off base/ptch file lists 1017 flist = [] 1018 for fn in baseFlist : 1019 flist.append(str_prefix_trunc(fn, ArchTmpDir1)) 1020 baseFlist = flist 1021 1022 flist = [] 1023 for fn in ptchFlist : 1024 flist.append(str_prefix_trunc(fn, ArchTmpDir2)) 1025 ptchFlist = flist 1026 1027 for fn in ptchFlist : 1028 if not fn in baseFlist : 1029 difference(fileName, fileType, \ 1030 fn + " added to " + fileName) 1031 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1032 return 1 1033 1034 for fn in baseFlist : 1035 if not fn in ptchFlist : 1036 difference(fileName, fileType, \ 1037 fn + " removed from " + fileName) 1038 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1039 return 1 1040 1041 differs = compareOneFile((ArchTmpDir1 + fn), \ 1042 (ArchTmpDir2 + fn), True) 1043 if differs : 1044 difference(fileName, fileType, \ 1045 fn + " in " + fileName + " differs") 1046 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1047 return 1 1048 1049 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1050 return 0 1051 1052##### 1053# (Basic) file comparison 1054# 1055# There's some special case code here for Javadoc HTML files 1056# 1057# Returns 1 if difference detected 1058# 0 if no difference detected 1059# -1 on error 1060# 1061def compareBasic(base, ptch, quiet, fileType) : 1062 1063 fileName = fnFormat(base); 1064 1065 if quiet and os.stat(base)[ST_SIZE] != os.stat(ptch)[ST_SIZE] : 1066 return 1 1067 1068 try: 1069 baseFile = open(base) 1070 except: 1071 error("could not open " + base) 1072 return -1 1073 try: 1074 ptchFile = open(ptch) 1075 except: 1076 error("could not open " + ptch) 1077 return -1 1078 1079 baseData = baseFile.read() 1080 ptchData = ptchFile.read() 1081 1082 baseFile.close() 1083 ptchFile.close() 1084 1085 needToSnip = False 1086 if fileType == "HTML" : 1087 needToSnip = True 1088 toSnipBeginStr = "<!-- Generated by javadoc" 1089 toSnipEndStr = "-->\n" 1090 1091 if needToSnip : 1092 toSnipBegin = string.find(baseData, toSnipBeginStr) 1093 if toSnipBegin != -1 : 1094 toSnipEnd = string.find(baseData[toSnipBegin:], \ 1095 toSnipEndStr) + \ 1096 len(toSnipEndStr) 1097 baseData = baseData[:toSnipBegin] + \ 1098 baseData[toSnipBegin + toSnipEnd:] 1099 ptchData = ptchData[:toSnipBegin] + \ 1100 ptchData[toSnipBegin + toSnipEnd:] 1101 1102 if quiet : 1103 if baseData != ptchData : 1104 return 1 1105 else : 1106 if len(baseData) != len(ptchData) or baseData != ptchData : 1107 diffs = diffData(base, ptch, baseData, ptchData) 1108 difference(fileName, fileType, diffs) 1109 return 1 1110 return 0 1111 1112 1113##### 1114# Compare two objects by producing a data dump from 1115# each object, and then comparing the dump data 1116# 1117# Returns: 1 if a difference is detected 1118# 0 if no difference detected 1119# -1 upon error 1120# 1121def compareByDumping(base, ptch, quiet, fileType) : 1122 1123 fileName = fnFormat(base); 1124 t = threading.currentThread() 1125 tmpFile1 = tmpDir1 + os.path.basename(base) + t.getName() 1126 tmpFile2 = tmpDir2 + os.path.basename(ptch) + t.getName() 1127 1128 if fileType == "Lint Library" : 1129 baseCmd = lintdump_cmd + " -ir " + base + \ 1130 " | egrep -v '(LINTOBJ|LINTMOD):'" + \ 1131 " | grep -v PASS[1-3]:" + \ 1132 " > " + tmpFile1 1133 ptchCmd = lintdump_cmd + " -ir " + ptch + \ 1134 " | egrep -v '(LINTOBJ|LINTMOD):'" + \ 1135 " | grep -v PASS[1-3]:" + \ 1136 " > " + tmpFile2 1137 elif fileType == "Sqlite Database" : 1138 baseCmd = "echo .dump | " + sqlite_cmd + base + " > " + \ 1139 tmpFile1 1140 ptchCmd = "echo .dump | " + sqlite_cmd + ptch + " > " + \ 1141 tmpFile2 1142 1143 os.system(baseCmd) 1144 os.system(ptchCmd) 1145 1146 try: 1147 baseFile = open(tmpFile1) 1148 except: 1149 error("could not open: " + tmpFile1) 1150 return 1151 try: 1152 ptchFile = open(tmpFile2) 1153 except: 1154 error("could not open: " + tmpFile2) 1155 return 1156 1157 baseData = baseFile.read() 1158 ptchData = ptchFile.read() 1159 1160 baseFile.close() 1161 ptchFile.close() 1162 1163 if len(baseData) != len(ptchData) or baseData != ptchData : 1164 if not quiet : 1165 data = diffFileData(tmpFile1, tmpFile2); 1166 try: 1167 os.unlink(tmpFile1) 1168 except OSError, e: 1169 error("compareByDumping: unlink failed %s" % e) 1170 try: 1171 os.unlink(tmpFile2) 1172 except OSError, e: 1173 error("compareByDumping: unlink failed %s" % e) 1174 difference(fileName, fileType, data) 1175 return 1 1176 1177 # Remove the temporary files now. 1178 try: 1179 os.unlink(tmpFile1) 1180 except OSError, e: 1181 error("compareByDumping: unlink failed %s" % e) 1182 try: 1183 os.unlink(tmpFile2) 1184 except OSError, e: 1185 error("compareByDumping: unlink failed %s" % e) 1186 1187 return 0 1188 1189##### 1190# 1191# SIGINT signal handler. Changes thread control variable to tell the threads 1192# to finish their current job and exit. 1193# 1194def discontinue_processing(signl, frme): 1195 global keep_processing 1196 1197 print >> sys.stderr, "Caught Ctrl-C, stopping the threads" 1198 keep_processing = False 1199 1200 return 0 1201 1202##### 1203# 1204# worker thread for changedFiles processing 1205# 1206class workerThread(threading.Thread) : 1207 def run(self): 1208 global wset_lock 1209 global changedFiles 1210 global baseRoot 1211 global ptchRoot 1212 global keep_processing 1213 1214 while (keep_processing) : 1215 # grab the lock to changedFiles and remove one member 1216 # and process it 1217 wset_lock.acquire() 1218 try : 1219 fn = changedFiles.pop() 1220 except IndexError : 1221 # there is nothing more to do 1222 wset_lock.release() 1223 return 1224 wset_lock.release() 1225 1226 base = baseRoot + fn 1227 ptch = ptchRoot + fn 1228 1229 compareOneFile(base, ptch, False) 1230 1231 1232##### 1233# Compare two objects. Detect type changes. 1234# Vector off to the appropriate type specific 1235# compare routine based on the type. 1236# 1237def compareOneFile(base, ptch, quiet) : 1238 1239 # Verify the file types. 1240 # If they are different, indicate this and move on 1241 btype = getTheFileType(base) 1242 ptype = getTheFileType(ptch) 1243 1244 if btype == 'Error' or ptype == 'Error' : 1245 return -1 1246 1247 fileName = fnFormat(base) 1248 1249 if (btype != ptype) : 1250 if not quiet : 1251 difference(fileName, "file type", btype + " to " + ptype) 1252 return 1 1253 else : 1254 fileType = btype 1255 1256 if (fileType == 'ELF') : 1257 return compareElfs(base, ptch, quiet) 1258 1259 elif (fileType == 'Java Archive' or fileType == 'ELF Object Archive') : 1260 return compareArchives(base, ptch, fileType) 1261 1262 elif (fileType == 'HTML') : 1263 return compareBasic(base, ptch, quiet, fileType) 1264 1265 elif ( fileType == 'Lint Library' ) : 1266 return compareByDumping(base, ptch, quiet, fileType) 1267 1268 elif ( fileType == 'Sqlite Database' ) : 1269 return compareByDumping(base, ptch, quiet, fileType) 1270 1271 else : 1272 # it has to be some variety of text file 1273 return compareBasic(base, ptch, quiet, fileType) 1274 1275# Cleanup and self-terminate 1276def cleanup(ret) : 1277 1278 debug("Performing cleanup (" + str(ret) + ")") 1279 if os.path.isdir(tmpDir1) > 0 : 1280 shutil.rmtree(tmpDir1) 1281 1282 if os.path.isdir(tmpDir2) > 0 : 1283 shutil.rmtree(tmpDir2) 1284 1285 if logging : 1286 log.close() 1287 1288 sys.exit(ret) 1289 1290def main() : 1291 1292 # Log file handle 1293 global log 1294 1295 # Globals relating to command line options 1296 global logging, vdiffs, reportAllSects 1297 1298 # Named temporary files / directories 1299 global tmpDir1, tmpDir2 1300 1301 # Command paths 1302 global lintdump_cmd, elfdump_cmd, dump_cmd, dis_cmd, od_cmd, diff_cmd, sqlite_cmd 1303 1304 # Default search path 1305 global wsdiff_path 1306 1307 # Essentially "uname -p" 1308 global arch 1309 1310 # changed files for worker thread processing 1311 global changedFiles 1312 global baseRoot 1313 global ptchRoot 1314 1315 # Sort the list of files from a temporary file 1316 global sorted 1317 global differentFiles 1318 1319 # Debugging indicator 1320 global debugon 1321 1322 # Some globals need to be initialized 1323 debugon = logging = vdiffs = reportAllSects = sorted = False 1324 1325 1326 # Process command line arguments 1327 # Return values are returned from args() in alpha order 1328 # (Yes, python functions can return multiple values (ewww)) 1329 # Note that args() also set the globals: 1330 # logging to True if verbose logging (to a file) was enabled 1331 # vdiffs to True if logged differences aren't to be truncated 1332 # reportAllSects to True if all ELF section differences are to be reported 1333 # 1334 baseRoot, fileNamesFile, localTools, ptchRoot, results = args() 1335 1336 # 1337 # Set up the results/log file 1338 # 1339 if logging : 1340 try: 1341 log = open(results, "w") 1342 except: 1343 logging = False 1344 error("failed to open log file: " + log) 1345 sys.exit(1) 1346 1347 dateTimeStr= "# %04d-%02d-%02d at %02d:%02d:%02d" % time.localtime()[:6] 1348 v_info("# This file was produced by wsdiff") 1349 v_info(dateTimeStr) 1350 1351 # Changed files (used only for the sorted case) 1352 if sorted : 1353 differentFiles = [] 1354 1355 # 1356 # Build paths to the tools required tools 1357 # 1358 # Try to look for tools in $SRC/tools if the "-t" option 1359 # was specified 1360 # 1361 arch = commands.getoutput("uname -p") 1362 if localTools : 1363 try: 1364 src = os.environ['SRC'] 1365 except: 1366 error("-t specified, but $SRC not set. Cannot find $SRC/tools") 1367 src = "" 1368 if len(src) > 0 : 1369 wsdiff_path.insert(0, src + "/tools/proto/opt/onbld/bin") 1370 1371 lintdump_cmd = find_tool("lintdump") 1372 elfdump_cmd = find_tool("elfdump") 1373 dump_cmd = find_tool("dump") 1374 od_cmd = find_tool("od") 1375 dis_cmd = find_tool("dis") 1376 diff_cmd = find_tool("diff") 1377 sqlite_cmd = find_tool("sqlite") 1378 1379 # 1380 # Set resource limit for number of open files as high as possible. 1381 # This might get handy with big number of threads. 1382 # 1383 (nofile_soft, nofile_hard) = resource.getrlimit(resource.RLIMIT_NOFILE) 1384 try: 1385 resource.setrlimit(resource.RLIMIT_NOFILE, 1386 (nofile_hard, nofile_hard)) 1387 except: 1388 error("cannot set resource limits for number of open files") 1389 sys.exit(1) 1390 1391 # 1392 # validate the base and patch paths 1393 # 1394 if baseRoot[-1] != '/' : 1395 baseRoot += '/' 1396 1397 if ptchRoot[-1] != '/' : 1398 ptchRoot += '/' 1399 1400 if not os.path.exists(baseRoot) : 1401 error("old proto area: " + baseRoot + " does not exist") 1402 sys.exit(1) 1403 1404 if not os.path.exists(ptchRoot) : 1405 error("new proto area: " + ptchRoot + \ 1406 " does not exist") 1407 sys.exit(1) 1408 1409 # 1410 # log some information identifying the run 1411 # 1412 v_info("Old proto area: " + baseRoot) 1413 v_info("New proto area: " + ptchRoot) 1414 v_info("Results file: " + results + "\n") 1415 1416 # 1417 # Set up the temporary directories / files 1418 # Could use python's tmpdir routines, but these should 1419 # be easier to identify / keep around for debugging 1420 pid = os.getpid() 1421 tmpDir1 = "/tmp/wsdiff_tmp1_" + str(pid) + "/" 1422 tmpDir2 = "/tmp/wsdiff_tmp2_" + str(pid) + "/" 1423 try: 1424 os.makedirs(tmpDir1) 1425 except OSError, e: 1426 error("main: makedir failed %s" % e) 1427 try: 1428 os.makedirs(tmpDir2) 1429 except OSError, e: 1430 error("main: makedir failed %s" % e) 1431 1432 # Derive a catalog of new, deleted, and to-be-compared objects 1433 # either from the specified base and patch proto areas, or from 1434 # from an input file list 1435 newOrDeleted = False 1436 1437 if fileNamesFile != "" : 1438 changedFiles, newFiles, deletedFiles = \ 1439 flistCatalog(baseRoot, ptchRoot, fileNamesFile) 1440 else : 1441 changedFiles, newFiles, deletedFiles = \ 1442 protoCatalog(baseRoot, ptchRoot) 1443 1444 if len(newFiles) > 0 : 1445 newOrDeleted = True 1446 info("\nNew objects found: ") 1447 1448 if sorted : 1449 newFiles.sort() 1450 for fn in newFiles : 1451 info(fnFormat(fn)) 1452 1453 if len(deletedFiles) > 0 : 1454 newOrDeleted = True 1455 info("\nObjects removed: ") 1456 1457 if sorted : 1458 deletedFiles.sort() 1459 for fn in deletedFiles : 1460 info(fnFormat(fn)) 1461 1462 if newOrDeleted : 1463 info("\nChanged objects: ") 1464 if sorted : 1465 debug("The list will appear after the processing is done") 1466 1467 # Here's where all the heavy lifting happens 1468 # Perform a comparison on each object appearing in 1469 # both proto areas. compareOneFile will examine the 1470 # file types of each object, and will vector off to 1471 # the appropriate comparison routine, where the compare 1472 # will happen, and any differences will be reported / logged 1473 1474 # determine maximum number of worker threads by using 1475 # DMAKE_MAX_JOBS environment variable set by nightly(1) 1476 # or get number of CPUs in the system 1477 try: 1478 max_threads = int(os.environ['DMAKE_MAX_JOBS']) 1479 except: 1480 max_threads = os.sysconf("SC_NPROCESSORS_ONLN") 1481 # If we cannot get number of online CPUs in the system 1482 # run unparallelized otherwise bump the number up 20% 1483 # to achieve best results. 1484 if max_threads == -1 : 1485 max_threads = 1 1486 else : 1487 max_threads += max_threads/5 1488 1489 # Set signal handler to attempt graceful exit 1490 debug("Setting signal handler") 1491 signal.signal( signal.SIGINT, discontinue_processing ) 1492 1493 # Create and unleash the threads 1494 # Only at most max_threads must be running at any moment 1495 mythreads = [] 1496 debug("Spawning " + str(max_threads) + " threads"); 1497 for i in range(max_threads) : 1498 thread = workerThread() 1499 mythreads.append(thread) 1500 mythreads[i].start() 1501 1502 # Wait for the threads to finish and do cleanup if interrupted 1503 debug("Waiting for the threads to finish") 1504 while True: 1505 if not True in [thread.isAlive() for thread in mythreads]: 1506 break 1507 else: 1508 # Some threads are still going 1509 time.sleep(1) 1510 1511 # Interrupted by SIGINT 1512 if keep_processing == False : 1513 cleanup(1) 1514 1515 # If the list of differences was sorted it is stored in an array 1516 if sorted : 1517 differentFiles.sort() 1518 for f in differentFiles : 1519 info(fnFormat(f)) 1520 1521 # We're done, cleanup. 1522 cleanup(0) 1523 1524if __name__ == '__main__' : 1525 try: 1526 main() 1527 except KeyboardInterrupt : 1528 cleanup(1); 1529 1530 1531