1#!/bin/env python 2# This Source Code Form is subject to the terms of the Mozilla Public 3# License, v. 2.0. If a copy of the MPL was not distributed with this 4# file, You can obtain one at http://mozilla.org/MPL/2.0/. 5# 6# Usage: symbolstore.py <params> <dump_syms path> <symbol store path> 7# <debug info files or dirs> 8# Runs dump_syms on each debug info file specified on the command line, 9# then places the resulting symbol file in the proper directory 10# structure in the symbol store path. Accepts multiple files 11# on the command line, so can be called as part of a pipe using 12# find <dir> | xargs symbolstore.pl <dump_syms> <storepath> 13# But really, you might just want to pass it <dir>. 14# 15# Parameters accepted: 16# -c : Copy debug info files to the same directory structure 17# as sym files. On Windows, this will also copy 18# binaries into the symbol store. 19# -a "<archs>" : Run dump_syms -a <arch> for each space separated 20# cpu architecture in <archs> (only on OS X) 21# -s <srcdir> : Use <srcdir> as the top source directory to 22# generate relative filenames. 23 24from __future__ import print_function 25 26import buildconfig 27import errno 28import sys 29import platform 30import os 31import re 32import shutil 33import textwrap 34import subprocess 35import time 36import ctypes 37 38from optparse import OptionParser 39 40from mozbuild.util import memoize 41from mozbuild.generated_sources import ( 42 get_filename_with_digest, 43 get_generated_sources, 44 get_s3_region_and_bucket, 45) 46from mozpack.copier import FileRegistry 47from mozpack.manifests import ( 48 InstallManifest, 49 UnreadableInstallManifest, 50) 51 52# Utility classes 53 54 55class VCSFileInfo: 56 """ A base class for version-controlled file information. Ensures that the 57 following attributes are generated only once (successfully): 58 59 self.root 60 self.clean_root 61 self.revision 62 self.filename 63 64 The attributes are generated by a single call to the GetRoot, 65 GetRevision, and GetFilename methods. Those methods are explicitly not 66 implemented here and must be implemented in derived classes. """ 67 68 def __init__(self, file): 69 if not file: 70 raise ValueError 71 self.file = file 72 73 def __getattr__(self, name): 74 """ __getattr__ is only called for attributes that are not set on self, 75 so setting self.[attr] will prevent future calls to the GetRoot, 76 GetRevision, and GetFilename methods. We don't set the values on 77 failure on the off chance that a future call might succeed. """ 78 79 if name == "root": 80 root = self.GetRoot() 81 if root: 82 self.root = root 83 return root 84 85 elif name == "clean_root": 86 clean_root = self.GetCleanRoot() 87 if clean_root: 88 self.clean_root = clean_root 89 return clean_root 90 91 elif name == "revision": 92 revision = self.GetRevision() 93 if revision: 94 self.revision = revision 95 return revision 96 97 elif name == "filename": 98 filename = self.GetFilename() 99 if filename: 100 self.filename = filename 101 return filename 102 103 raise AttributeError 104 105 def GetRoot(self): 106 """ This method should return the unmodified root for the file or 'None' 107 on failure. """ 108 raise NotImplementedError 109 110 def GetCleanRoot(self): 111 """ This method should return the repository root for the file or 'None' 112 on failure. """ 113 raise NotImplementedError 114 115 def GetRevision(self): 116 """ This method should return the revision number for the file or 'None' 117 on failure. """ 118 raise NotImplementedError 119 120 def GetFilename(self): 121 """ This method should return the repository-specific filename for the 122 file or 'None' on failure. """ 123 raise NotImplementedError 124 125 126# This regex separates protocol and optional username/password from a url. 127# For instance, all the following urls will be transformed into 128# 'foo.com/bar': 129# 130# http://foo.com/bar 131# svn+ssh://user@foo.com/bar 132# svn+ssh://user:pass@foo.com/bar 133# 134rootRegex = re.compile(r"^\S+?:/+(?:[^\s/]*@)?(\S+)$") 135 136 137def read_output(*args): 138 (stdout, _) = subprocess.Popen( 139 args=args, universal_newlines=True, stdout=subprocess.PIPE).communicate() 140 return stdout.rstrip() 141 142 143class HGRepoInfo: 144 def __init__(self, path): 145 self.path = path 146 147 rev = os.environ.get("MOZ_SOURCE_CHANGESET") 148 if not rev: 149 rev = read_output("hg", "-R", path, "parent", "--template={node}") 150 151 # Look for the default hg path. If MOZ_SOURCE_REPO is set, we 152 # don't bother asking hg. 153 hg_root = os.environ.get("MOZ_SOURCE_REPO") 154 if hg_root: 155 root = hg_root 156 else: 157 root = read_output("hg", "-R", path, "showconfig", "paths.default") 158 if not root: 159 print("Failed to get HG Repo for %s" % path, file=sys.stderr) 160 cleanroot = None 161 if root: 162 match = rootRegex.match(root) 163 if match: 164 cleanroot = match.group(1) 165 if cleanroot.endswith("/"): 166 cleanroot = cleanroot[:-1] 167 if cleanroot is None: 168 print( 169 textwrap.dedent( 170 """\ 171 Could not determine repo info for %s. This is either not a clone of the web-based 172 repository, or you have not specified MOZ_SOURCE_REPO, or the clone is corrupt.""" 173 ) % path, 174 sys.stderr, 175 ) 176 sys.exit(1) 177 self.rev = rev 178 self.root = root 179 self.cleanroot = cleanroot 180 181 def GetFileInfo(self, file): 182 return HGFileInfo(file, self) 183 184 185class HGFileInfo(VCSFileInfo): 186 def __init__(self, file, repo): 187 VCSFileInfo.__init__(self, file) 188 self.repo = repo 189 self.file = os.path.relpath(file, repo.path) 190 191 def GetRoot(self): 192 return self.repo.root 193 194 def GetCleanRoot(self): 195 return self.repo.cleanroot 196 197 def GetRevision(self): 198 return self.repo.rev 199 200 def GetFilename(self): 201 if self.revision and self.clean_root: 202 return "hg:%s:%s:%s" % (self.clean_root, self.file, self.revision) 203 return self.file 204 205 206class GitRepoInfo: 207 """ 208 Info about a local git repository. Does not currently 209 support discovering info about a git clone, the info must be 210 provided out-of-band. 211 """ 212 213 def __init__(self, path, rev, root): 214 self.path = path 215 cleanroot = None 216 if root: 217 match = rootRegex.match(root) 218 if match: 219 cleanroot = match.group(1) 220 if cleanroot.endswith("/"): 221 cleanroot = cleanroot[:-1] 222 if cleanroot is None: 223 print( 224 textwrap.dedent( 225 """\ 226 Could not determine repo info for %s (%s). This is either not a clone of a web-based 227 repository, or you have not specified MOZ_SOURCE_REPO, or the clone is corrupt.""" 228 ) 229 % (path, root), 230 file=sys.stderr, 231 ) 232 sys.exit(1) 233 self.rev = rev 234 self.cleanroot = cleanroot 235 236 def GetFileInfo(self, file): 237 return GitFileInfo(file, self) 238 239 240class GitFileInfo(VCSFileInfo): 241 def __init__(self, file, repo): 242 VCSFileInfo.__init__(self, file) 243 self.repo = repo 244 self.file = os.path.relpath(file, repo.path) 245 246 def GetRoot(self): 247 return self.repo.path 248 249 def GetCleanRoot(self): 250 return self.repo.cleanroot 251 252 def GetRevision(self): 253 return self.repo.rev 254 255 def GetFilename(self): 256 if self.revision and self.clean_root: 257 return "git:%s:%s:%s" % (self.clean_root, self.file, self.revision) 258 return self.file 259 260 261# Utility functions 262 263 264# A cache of files for which VCS info has already been determined. Used to 265# prevent extra filesystem activity or process launching. 266vcsFileInfoCache = {} 267 268if platform.system() == "Windows": 269 270 def realpath(path): 271 """ 272 Normalize a path using `GetFinalPathNameByHandleW` to get the 273 path with all components in the case they exist in on-disk, so 274 that making links to a case-sensitive server (hg.mozilla.org) works. 275 276 This function also resolves any symlinks in the path. 277 """ 278 # Return the original path if something fails, which can happen for paths that 279 # don't exist on this system (like paths from the CRT). 280 result = path 281 282 ctypes.windll.kernel32.SetErrorMode(ctypes.c_uint(1)) 283 handle = ctypes.windll.kernel32.CreateFileW(path, 284 # GENERIC_READ 285 0x80000000, 286 # FILE_SHARE_READ 287 1, 288 None, 289 # OPEN_EXISTING 290 3, 291 # FILE_FLAG_BACKUP_SEMANTICS 292 # This is necessary to open 293 # directory handles. 294 0x02000000, 295 None) 296 if handle != -1: 297 size = ctypes.windll.kernel32.GetFinalPathNameByHandleW(handle, None, 0, 0) 298 buf = ctypes.create_unicode_buffer(size) 299 if ( 300 ctypes.windll.kernel32.GetFinalPathNameByHandleW(handle, buf, size, 0) 301 > 0 302 ): 303 # The return value of GetFinalPathNameByHandleW uses the 304 # '\\?\' prefix. 305 result = buf.value[4:] 306 ctypes.windll.kernel32.CloseHandle(handle) 307 return result 308 309 310else: 311 # Just use the os.path version otherwise. 312 realpath = os.path.realpath 313 314 315def IsInDir(file, dir): 316 # the lower() is to handle win32+vc8, where 317 # the source filenames come out all lowercase, 318 # but the srcdir can be mixed case 319 return os.path.abspath(file).lower().startswith(os.path.abspath(dir).lower()) 320 321 322def GetVCSFilenameFromSrcdir(file, srcdir): 323 if srcdir not in Dumper.srcdirRepoInfo: 324 # Not in cache, so find it adnd cache it 325 if os.path.isdir(os.path.join(srcdir, ".hg")): 326 Dumper.srcdirRepoInfo[srcdir] = HGRepoInfo(srcdir) 327 else: 328 # Unknown VCS or file is not in a repo. 329 return None 330 return Dumper.srcdirRepoInfo[srcdir].GetFileInfo(file) 331 332 333def GetVCSFilename(file, srcdirs): 334 """Given a full path to a file, and the top source directory, 335 look for version control information about this file, and return 336 a tuple containing 337 1) a specially formatted filename that contains the VCS type, 338 VCS location, relative filename, and revision number, formatted like: 339 vcs:vcs location:filename:revision 340 For example: 341 cvs:cvs.mozilla.org/cvsroot:mozilla/browser/app/nsBrowserApp.cpp:1.36 342 2) the unmodified root information if it exists""" 343 (path, filename) = os.path.split(file) 344 if path == "" or filename == "": 345 return (file, None) 346 347 fileInfo = None 348 root = "" 349 if file in vcsFileInfoCache: 350 # Already cached this info, use it. 351 fileInfo = vcsFileInfoCache[file] 352 else: 353 for srcdir in srcdirs: 354 if not IsInDir(file, srcdir): 355 continue 356 fileInfo = GetVCSFilenameFromSrcdir(file, srcdir) 357 if fileInfo: 358 vcsFileInfoCache[file] = fileInfo 359 break 360 361 if fileInfo: 362 file = fileInfo.filename 363 root = fileInfo.root 364 365 # we want forward slashes on win32 paths 366 return (file.replace("\\", "/"), root) 367 368 369def validate_install_manifests(install_manifest_args): 370 args = [] 371 for arg in install_manifest_args: 372 bits = arg.split(",") 373 if len(bits) != 2: 374 raise ValueError('Invalid format for --install-manifest: ' 375 'specify manifest,target_dir') 376 manifest_file, destination = [os.path.abspath(b) for b in bits] 377 if not os.path.isfile(manifest_file): 378 raise IOError(errno.ENOENT, "Manifest file not found", manifest_file) 379 if not os.path.isdir(destination): 380 raise IOError(errno.ENOENT, "Install directory not found", destination) 381 try: 382 manifest = InstallManifest(manifest_file) 383 except UnreadableInstallManifest: 384 raise IOError(errno.EINVAL, "Error parsing manifest file", manifest_file) 385 args.append((manifest, destination)) 386 return args 387 388 389def make_file_mapping(install_manifests): 390 file_mapping = {} 391 for manifest, destination in install_manifests: 392 destination = os.path.abspath(destination) 393 reg = FileRegistry() 394 manifest.populate_registry(reg) 395 for dst, src in reg: 396 if hasattr(src, "path"): 397 # Any paths that get compared to source file names need to go through realpath. 398 abs_dest = realpath(os.path.join(destination, dst)) 399 file_mapping[abs_dest] = realpath(src.path) 400 return file_mapping 401 402 403@memoize 404def get_generated_file_s3_path(filename, rel_path, bucket): 405 """Given a filename, return a path formatted similarly to 406 GetVCSFilename but representing a file available in an s3 bucket.""" 407 with open(filename, "rb") as f: 408 path = get_filename_with_digest(rel_path, f.read()) 409 return "s3:{bucket}:{path}:".format(bucket=bucket, path=path) 410 411 412def GetPlatformSpecificDumper(**kwargs): 413 """This function simply returns a instance of a subclass of Dumper 414 that is appropriate for the current platform.""" 415 return {"WINNT": Dumper_Win32, "Linux": Dumper_Linux, "Darwin": Dumper_Mac}[ 416 buildconfig.substs["OS_ARCH"] 417 ](**kwargs) 418 419 420def SourceIndex(fileStream, outputPath, vcs_root): 421 """Takes a list of files, writes info to a data block in a .stream file""" 422 # Creates a .pdb.stream file in the mozilla\objdir to be used for source indexing 423 # Create the srcsrv data block that indexes the pdb file 424 result = True 425 pdbStreamFile = open(outputPath, "w") 426 pdbStreamFile.write('SRCSRV: ini ------------------------------------------------\r' 427 + '\nVERSION=2\r\nINDEXVERSION=2\r' 428 + '\nVERCTRL=http\r' 429 + '\nSRCSRV: variables ------------------------------------------\r' 430 + '\nHGSERVER=') 431 pdbStreamFile.write(vcs_root) 432 pdbStreamFile.write('\r\nSRCSRVVERCTRL=http\r' 433 + '\nHTTP_EXTRACT_TARGET=%hgserver%/raw-file/%var3%/%var2%\r' 434 + '\nSRCSRVTRG=%http_extract_target%\r' 435 + '\nSRCSRV: source files ---------------------------------------\r\n''') 436 pdbStreamFile.write(fileStream) 437 # can't do string interpolation because the source server also uses this 438 # so there are % in the above 439 pdbStreamFile.write( 440 "SRCSRV: end ------------------------------------------------\r\n\n" 441 ) 442 pdbStreamFile.close() 443 return result 444 445 446class Dumper: 447 """This class can dump symbols from a file with debug info, and 448 store the output in a directory structure that is valid for use as 449 a Breakpad symbol server. Requires a path to a dump_syms binary-- 450 |dump_syms| and a directory to store symbols in--|symbol_path|. 451 Optionally takes a list of processor architectures to process from 452 each debug file--|archs|, the full path to the top source 453 directory--|srcdir|, for generating relative source file names, 454 and an option to copy debug info files alongside the dumped 455 symbol files--|copy_debug|, mostly useful for creating a 456 Microsoft Symbol Server from the resulting output. 457 458 You don't want to use this directly if you intend to process files. 459 Instead, call GetPlatformSpecificDumper to get an instance of a 460 subclass.""" 461 462 srcdirRepoInfo = {} 463 464 def __init__( 465 self, 466 dump_syms, 467 symbol_path, 468 archs=None, 469 srcdirs=[], 470 copy_debug=False, 471 vcsinfo=False, 472 srcsrv=False, 473 generated_files=None, 474 s3_bucket=None, 475 file_mapping=None, 476 ): 477 # popen likes absolute paths, at least on windows 478 self.dump_syms = os.path.abspath(dump_syms) 479 self.symbol_path = symbol_path 480 if archs is None: 481 # makes the loop logic simpler 482 self.archs = [""] 483 else: 484 self.archs = ["-a %s" % a for a in archs.split()] 485 # Any paths that get compared to source file names need to go through realpath. 486 self.srcdirs = [realpath(s) for s in srcdirs] 487 self.copy_debug = copy_debug 488 self.vcsinfo = vcsinfo 489 self.srcsrv = srcsrv 490 self.generated_files = generated_files or {} 491 self.s3_bucket = s3_bucket 492 self.file_mapping = file_mapping or {} 493 # Add a static mapping for Rust sources. Since Rust 1.30 official Rust builds map 494 # source paths to start with "/rust/<sha>/". 495 rust_sha = buildconfig.substs["RUSTC_COMMIT"] 496 rust_srcdir = "/rustc/" + rust_sha 497 self.srcdirs.append(rust_srcdir) 498 Dumper.srcdirRepoInfo[rust_srcdir] = GitRepoInfo( 499 rust_srcdir, rust_sha, "https://github.com/rust-lang/rust/" 500 ) 501 502 # subclasses override this 503 def ShouldProcess(self, file): 504 return True 505 506 def RunFileCommand(self, file): 507 """Utility function, returns the output of file(1)""" 508 # we use -L to read the targets of symlinks, 509 # and -b to print just the content, not the filename 510 return read_output("file", "-Lb", file) 511 512 # This is a no-op except on Win32 513 def SourceServerIndexing(self, debug_file, guid, sourceFileStream, vcs_root): 514 return "" 515 516 # subclasses override this if they want to support this 517 def CopyDebug(self, file, debug_file, guid, code_file, code_id): 518 pass 519 520 def Process(self, file_to_process, count_ctors=False): 521 """Process the given file.""" 522 if self.ShouldProcess(os.path.abspath(file_to_process)): 523 self.ProcessFile(file_to_process, count_ctors=count_ctors) 524 525 def ProcessFile(self, file, dsymbundle=None, count_ctors=False): 526 """Dump symbols from these files into a symbol file, stored 527 in the proper directory structure in |symbol_path|; processing is performed 528 asynchronously, and Finish must be called to wait for it complete and cleanup. 529 All files after the first are fallbacks in case the first file does not process 530 successfully; if it does, no other files will be touched.""" 531 print("Beginning work for file: %s" % file, file=sys.stderr) 532 533 # tries to get the vcs root from the .mozconfig first - if it's not set 534 # the tinderbox vcs path will be assigned further down 535 vcs_root = os.environ.get("MOZ_SOURCE_REPO") 536 for arch_num, arch in enumerate(self.archs): 537 self.ProcessFileWork( 538 file, arch_num, arch, vcs_root, dsymbundle, count_ctors=count_ctors 539 ) 540 541 def dump_syms_cmdline(self, file, arch, dsymbundle=None): 542 """ 543 Get the commandline used to invoke dump_syms. 544 """ 545 # The Mac dumper overrides this. 546 return [self.dump_syms, file] 547 548 def ProcessFileWork( 549 self, file, arch_num, arch, vcs_root, dsymbundle=None, count_ctors=False 550 ): 551 ctors = 0 552 t_start = time.time() 553 print("Processing file: %s" % file, file=sys.stderr) 554 555 sourceFileStream = "" 556 code_id, code_file = None, None 557 try: 558 cmd = self.dump_syms_cmdline(file, arch, dsymbundle=dsymbundle) 559 print(' '.join(cmd), file=sys.stderr) 560 proc = subprocess.Popen(cmd, universal_newlines=True, stdout=subprocess.PIPE, 561 stderr=open(os.devnull, 'wb')) 562 module_line = next(proc.stdout) 563 if module_line.startswith("MODULE"): 564 # MODULE os cpu guid debug_file 565 (guid, debug_file) = (module_line.split())[3:5] 566 # strip off .pdb extensions, and append .sym 567 sym_file = re.sub("\.pdb$", "", debug_file) + ".sym" 568 # we do want forward slashes here 569 rel_path = os.path.join(debug_file, guid, sym_file).replace("\\", "/") 570 full_path = os.path.normpath(os.path.join(self.symbol_path, rel_path)) 571 try: 572 os.makedirs(os.path.dirname(full_path)) 573 except OSError: # already exists 574 pass 575 f = open(full_path, "w") 576 f.write(module_line) 577 # now process the rest of the output 578 for line in proc.stdout: 579 if line.startswith("FILE"): 580 # FILE index filename 581 (x, index, filename) = line.rstrip().split(None, 2) 582 # We want original file paths for the source server. 583 sourcepath = filename 584 filename = realpath(filename) 585 if filename in self.file_mapping: 586 filename = self.file_mapping[filename] 587 if self.vcsinfo: 588 gen_path = self.generated_files.get(filename) 589 if gen_path and self.s3_bucket: 590 filename = get_generated_file_s3_path( 591 filename, gen_path, self.s3_bucket 592 ) 593 rootname = "" 594 else: 595 (filename, rootname) = GetVCSFilename( 596 filename, self.srcdirs 597 ) 598 # sets vcs_root in case the loop through files were to end 599 # on an empty rootname 600 if vcs_root is None: 601 if rootname: 602 vcs_root = rootname 603 # gather up files with hg for indexing 604 if filename.startswith("hg"): 605 (ver, checkout, source_file, revision) = filename.split( 606 ":", 3 607 ) 608 sourceFileStream += sourcepath + "*" + source_file 609 sourceFileStream += "*" + revision + "\r\n" 610 f.write("FILE %s %s\n" % (index, filename)) 611 elif line.startswith("INFO CODE_ID "): 612 # INFO CODE_ID code_id code_file 613 # This gives some info we can use to 614 # store binaries in the symbol store. 615 bits = line.rstrip().split(None, 3) 616 if len(bits) == 4: 617 code_id, code_file = bits[2:] 618 f.write(line) 619 else: 620 if count_ctors and line.startswith("FUNC "): 621 # Static initializers, as created by clang and gcc 622 # have symbols that start with "_GLOBAL_sub" 623 if "_GLOBAL__sub_" in line: 624 ctors += 1 625 # MSVC creates `dynamic initializer for '...'` 626 # symbols. 627 elif "`dynamic initializer for '" in line: 628 ctors += 1 629 630 # pass through all other lines unchanged 631 f.write(line) 632 f.close() 633 retcode = proc.wait() 634 if retcode != 0: 635 raise RuntimeError("dump_syms failed with error code %d" % retcode) 636 # we output relative paths so callers can get a list of what 637 # was generated 638 print(rel_path) 639 if self.srcsrv and vcs_root: 640 # add source server indexing to the pdb file 641 self.SourceServerIndexing( 642 debug_file, guid, sourceFileStream, vcs_root 643 ) 644 # only copy debug the first time if we have multiple architectures 645 if self.copy_debug and arch_num == 0: 646 self.CopyDebug(file, debug_file, guid, code_file, code_id) 647 except StopIteration: 648 pass 649 except Exception as e: 650 print("Unexpected error: %s" % str(e), file=sys.stderr) 651 raise 652 653 if dsymbundle: 654 shutil.rmtree(dsymbundle) 655 656 if count_ctors: 657 import json 658 659 perfherder_data = { 660 "framework": {"name": "build_metrics"}, 661 "suites": [ 662 { 663 "name": "compiler_metrics", 664 "subtests": [ 665 { 666 "name": "num_static_constructors", 667 "value": ctors, 668 "alertChangeType": "absolute", 669 "alertThreshold": 3, 670 } 671 ], 672 } 673 ], 674 } 675 perfherder_extra_options = os.environ.get("PERFHERDER_EXTRA_OPTIONS", "") 676 for opt in perfherder_extra_options.split(): 677 for suite in perfherder_data["suites"]: 678 if opt not in suite.get("extraOptions", []): 679 suite.setdefault("extraOptions", []).append(opt) 680 681 if "asan" not in perfherder_extra_options.lower(): 682 print( 683 "PERFHERDER_DATA: %s" % json.dumps(perfherder_data), file=sys.stderr 684 ) 685 686 elapsed = time.time() - t_start 687 print("Finished processing %s in %.2fs" % (file, elapsed), file=sys.stderr) 688 689 690# Platform-specific subclasses. For the most part, these just have 691# logic to determine what files to extract symbols from. 692 693 694def locate_pdb(path): 695 """Given a path to a binary, attempt to locate the matching pdb file with simple heuristics: 696 * Look for a pdb file with the same base name next to the binary 697 * Look for a pdb file with the same base name in the cwd 698 699 Returns the path to the pdb file if it exists, or None if it could not be located. 700 """ 701 path, ext = os.path.splitext(path) 702 pdb = path + ".pdb" 703 if os.path.isfile(pdb): 704 return pdb 705 # If there's no pdb next to the file, see if there's a pdb with the same root name 706 # in the cwd. We build some binaries directly into dist/bin, but put the pdb files 707 # in the relative objdir, which is the cwd when running this script. 708 base = os.path.basename(pdb) 709 pdb = os.path.join(os.getcwd(), base) 710 if os.path.isfile(pdb): 711 return pdb 712 return None 713 714 715class Dumper_Win32(Dumper): 716 fixedFilenameCaseCache = {} 717 718 def ShouldProcess(self, file): 719 """This function will allow processing of exe or dll files that have pdb 720 files with the same base name next to them.""" 721 if file.endswith(".exe") or file.endswith(".dll"): 722 if locate_pdb(file) is not None: 723 return True 724 return False 725 726 def CopyDebug(self, file, debug_file, guid, code_file, code_id): 727 file = locate_pdb(file) 728 729 def compress(path): 730 compressed_file = path[:-1] + "_" 731 # ignore makecab's output 732 makecab = buildconfig.substs["MAKECAB"] 733 wine = buildconfig.substs.get("WINE") 734 if wine and makecab.lower().endswith(".exe"): 735 cmd = [wine, makecab] 736 else: 737 cmd = [makecab] 738 success = subprocess.call( 739 cmd + ["-D", "CompressionType=MSZIP", path, compressed_file], 740 stdout=open(os.devnull, "w"), 741 stderr=subprocess.STDOUT, 742 ) 743 if success == 0 and os.path.exists(compressed_file): 744 os.unlink(path) 745 return True 746 return False 747 748 rel_path = os.path.join(debug_file, guid, debug_file).replace("\\", "/") 749 full_path = os.path.normpath(os.path.join(self.symbol_path, rel_path)) 750 shutil.copyfile(file, full_path) 751 if compress(full_path): 752 print(rel_path[:-1] + "_") 753 else: 754 print(rel_path) 755 756 # Copy the binary file as well 757 if code_file and code_id: 758 full_code_path = os.path.join(os.path.dirname(file), code_file) 759 if os.path.exists(full_code_path): 760 rel_path = os.path.join(code_file, code_id, code_file).replace( 761 "\\", "/" 762 ) 763 full_path = os.path.normpath(os.path.join(self.symbol_path, rel_path)) 764 try: 765 os.makedirs(os.path.dirname(full_path)) 766 except OSError as e: 767 if e.errno != errno.EEXIST: 768 raise 769 shutil.copyfile(full_code_path, full_path) 770 if compress(full_path): 771 print(rel_path[:-1] + "_") 772 else: 773 print(rel_path) 774 775 def SourceServerIndexing(self, debug_file, guid, sourceFileStream, vcs_root): 776 # Creates a .pdb.stream file in the mozilla\objdir to be used for source indexing 777 streamFilename = debug_file + ".stream" 778 stream_output_path = os.path.abspath(streamFilename) 779 # Call SourceIndex to create the .stream file 780 result = SourceIndex(sourceFileStream, stream_output_path, vcs_root) 781 if self.copy_debug: 782 pdbstr = buildconfig.substs["PDBSTR"] 783 wine = buildconfig.substs.get("WINE") 784 if wine: 785 cmd = [wine, pdbstr] 786 else: 787 cmd = [pdbstr] 788 subprocess.call( 789 cmd 790 + [ 791 "-w", 792 "-p:" + os.path.basename(debug_file), 793 "-i:" + os.path.basename(streamFilename), 794 "-s:srcsrv", 795 ], 796 cwd=os.path.dirname(stream_output_path), 797 ) 798 # clean up all the .stream files when done 799 os.remove(stream_output_path) 800 return result 801 802 803class Dumper_Linux(Dumper): 804 objcopy = os.environ["OBJCOPY"] if "OBJCOPY" in os.environ else "objcopy" 805 806 def ShouldProcess(self, file): 807 """This function will allow processing of files that are 808 executable, or end with the .so extension, and additionally 809 file(1) reports as being ELF files. It expects to find the file 810 command in PATH.""" 811 if file.endswith(".so") or os.access(file, os.X_OK): 812 return self.RunFileCommand(file).startswith("ELF") 813 return False 814 815 def CopyDebug(self, file, debug_file, guid, code_file, code_id): 816 # We want to strip out the debug info, and add a 817 # .gnu_debuglink section to the object, so the debugger can 818 # actually load our debug info later. 819 # In some odd cases, the object might already have an irrelevant 820 # .gnu_debuglink section, and objcopy doesn't want to add one in 821 # such cases, so we make it remove it any existing one first. 822 file_dbg = file + ".dbg" 823 if ( 824 subprocess.call([self.objcopy, "--only-keep-debug", file, file_dbg]) == 0 825 and subprocess.call( 826 [ 827 self.objcopy, 828 "--remove-section", 829 ".gnu_debuglink", 830 "--add-gnu-debuglink=%s" % file_dbg, 831 file, 832 ] 833 ) 834 == 0 835 ): 836 rel_path = os.path.join(debug_file, guid, debug_file + ".dbg") 837 full_path = os.path.normpath(os.path.join(self.symbol_path, rel_path)) 838 shutil.move(file_dbg, full_path) 839 # gzip the shipped debug files 840 os.system("gzip -4 -f %s" % full_path) 841 print(rel_path + ".gz") 842 else: 843 if os.path.isfile(file_dbg): 844 os.unlink(file_dbg) 845 846 847class Dumper_Solaris(Dumper): 848 def RunFileCommand(self, file): 849 """Utility function, returns the output of file(1)""" 850 try: 851 output = os.popen("file " + file).read() 852 return output.split("\t")[1] 853 except Exception: 854 return "" 855 856 def ShouldProcess(self, file): 857 """This function will allow processing of files that are 858 executable, or end with the .so extension, and additionally 859 file(1) reports as being ELF files. It expects to find the file 860 command in PATH.""" 861 if file.endswith(".so") or os.access(file, os.X_OK): 862 return self.RunFileCommand(file).startswith("ELF") 863 return False 864 865 866class Dumper_Mac(Dumper): 867 def ShouldProcess(self, file): 868 """This function will allow processing of files that are 869 executable, or end with the .dylib extension, and additionally 870 file(1) reports as being Mach-O files. It expects to find the file 871 command in PATH.""" 872 if file.endswith(".dylib") or os.access(file, os.X_OK): 873 return self.RunFileCommand(file).startswith("Mach-O") 874 return False 875 876 def ProcessFile(self, file, count_ctors=False): 877 print("Starting Mac pre-processing on file: %s" % file, file=sys.stderr) 878 dsymbundle = self.GenerateDSYM(file) 879 if dsymbundle: 880 # kick off new jobs per-arch with our new list of files 881 Dumper.ProcessFile( 882 self, file, dsymbundle=dsymbundle, count_ctors=count_ctors 883 ) 884 885 def dump_syms_cmdline(self, file, arch, dsymbundle=None): 886 """ 887 Get the commandline used to invoke dump_syms. 888 """ 889 # dump_syms wants the path to the original binary and the .dSYM 890 # in order to dump all the symbols. 891 if dsymbundle: 892 # This is the .dSYM bundle. 893 return [self.dump_syms] + arch.split() + ["-g", dsymbundle, file] 894 return Dumper.dump_syms_cmdline(self, file, arch) 895 896 def GenerateDSYM(self, file): 897 """dump_syms on Mac needs to be run on a dSYM bundle produced 898 by dsymutil(1), so run dsymutil here and pass the bundle name 899 down to the superclass method instead.""" 900 t_start = time.time() 901 print("Running Mac pre-processing on file: %s" % (file,), file=sys.stderr) 902 903 dsymbundle = file + ".dSYM" 904 if os.path.exists(dsymbundle): 905 shutil.rmtree(dsymbundle) 906 dsymutil = buildconfig.substs["DSYMUTIL"] 907 # dsymutil takes --arch=foo instead of -a foo like everything else 908 cmd = ([dsymutil] + 909 [a.replace('-a ', '--arch=') for a in self.archs if a] + 910 [file]) 911 print(' '.join(cmd), file=sys.stderr) 912 913 dsymutil_proc = subprocess.Popen(cmd, universal_newlines=True, 914 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 915 dsymout, dsymerr = dsymutil_proc.communicate() 916 if dsymutil_proc.returncode != 0: 917 raise RuntimeError("Error running dsymutil: %s" % dsymerr) 918 919 # Regular dsymutil won't produce a .dSYM for files without symbols. 920 if not os.path.exists(dsymbundle): 921 print("No symbols found in file: %s" % (file,), file=sys.stderr) 922 return False 923 924 # llvm-dsymutil will produce a .dSYM for files without symbols or 925 # debug information, but only sometimes will it warn you about this. 926 # We don't want to run dump_syms on such bundles, because asserts 927 # will fire in debug mode and who knows what will happen in release. 928 # 929 # So we check for the error message and bail if it appears. If it 930 # doesn't, we carefully check the bundled DWARF to see if dump_syms 931 # will be OK with it. 932 if "warning: no debug symbols in" in dsymerr: 933 print(dsymerr, file=sys.stderr) 934 return False 935 936 contents_dir = os.path.join(dsymbundle, "Contents", "Resources", "DWARF") 937 if not os.path.exists(contents_dir): 938 print( 939 "No DWARF information in .dSYM bundle %s" % (dsymbundle,), 940 file=sys.stderr, 941 ) 942 return False 943 944 files = os.listdir(contents_dir) 945 if len(files) != 1: 946 print("Unexpected files in .dSYM bundle %s" % (files,), file=sys.stderr) 947 return False 948 949 otool_out = subprocess.check_output([buildconfig.substs['OTOOL'], 950 '-l', 951 os.path.join(contents_dir, files[0])], 952 universal_newlines=True) 953 if 'sectname __debug_info' not in otool_out: 954 print("No symbols in .dSYM bundle %s" % (dsymbundle,), 955 file=sys.stderr) 956 return False 957 958 elapsed = time.time() - t_start 959 print("Finished processing %s in %.2fs" % (file, elapsed), file=sys.stderr) 960 return dsymbundle 961 962 def CopyDebug(self, file, debug_file, guid, code_file, code_id): 963 """ProcessFile has already produced a dSYM bundle, so we should just 964 copy that to the destination directory. However, we'll package it 965 into a .tar.bz2 because the debug symbols are pretty huge, and 966 also because it's a bundle, so it's a directory. |file| here is the 967 the original filename.""" 968 dsymbundle = file + ".dSYM" 969 rel_path = os.path.join( 970 debug_file, guid, os.path.basename(dsymbundle) + ".tar.bz2" 971 ) 972 full_path = os.path.abspath(os.path.join(self.symbol_path, rel_path)) 973 success = subprocess.call( 974 ["tar", "cjf", full_path, os.path.basename(dsymbundle)], 975 cwd=os.path.dirname(dsymbundle), 976 stdout=open(os.devnull, "w"), 977 stderr=subprocess.STDOUT, 978 ) 979 if success == 0 and os.path.exists(full_path): 980 print(rel_path) 981 982 983# Entry point if called as a standalone program 984 985 986def main(): 987 parser = OptionParser( 988 usage="usage: %prog [options] <dump_syms binary> <symbol store path> <debug info files>" 989 ) 990 parser.add_option( 991 "-c", 992 "--copy", 993 action="store_true", 994 dest="copy_debug", 995 default=False, 996 help="Copy debug info files into the same directory structure as symbol files" 997 ) 998 parser.add_option( 999 "-a", 1000 "--archs", 1001 action="store", 1002 dest="archs", 1003 help="Run dump_syms -a <arch> for each space separated" 1004 + "cpu architecture in ARCHS (only on OS X)") 1005 parser.add_option( 1006 "-s", 1007 "--srcdir", 1008 action="append", 1009 dest="srcdir", 1010 default=[], 1011 help="Use SRCDIR to determine relative paths to source files", 1012 ) 1013 parser.add_option( 1014 "-v", 1015 "--vcs-info", 1016 action="store_true", 1017 dest="vcsinfo", 1018 help="Try to retrieve VCS info for each FILE listed in the output", 1019 ) 1020 parser.add_option( 1021 "-i", 1022 "--source-index", 1023 action="store_true", 1024 dest="srcsrv", 1025 default=False, 1026 help="Add source index information to debug files, making them suitable" 1027 + " for use in a source server.", 1028 ) 1029 parser.add_option( 1030 "--install-manifest", 1031 action="append", 1032 dest="install_manifests", 1033 default=[], 1034 help="""Use this install manifest to map filenames back 1035to canonical locations in the source repository. Specify 1036<install manifest filename>,<install destination> as a comma-separated pair.""" 1037 ) 1038 parser.add_option( 1039 "--count-ctors", 1040 action="store_true", 1041 dest="count_ctors", 1042 default=False, 1043 help="Count static initializers", 1044 ) 1045 (options, args) = parser.parse_args() 1046 1047 # check to see if the pdbstr.exe exists 1048 if options.srcsrv: 1049 if "PDBSTR" not in buildconfig.substs: 1050 print("pdbstr was not found by configure.\n", file=sys.stderr) 1051 sys.exit(1) 1052 1053 if len(args) < 3: 1054 parser.error("not enough arguments") 1055 exit(1) 1056 1057 try: 1058 manifests = validate_install_manifests(options.install_manifests) 1059 except (IOError, ValueError) as e: 1060 parser.error(str(e)) 1061 exit(1) 1062 file_mapping = make_file_mapping(manifests) 1063 # Any paths that get compared to source file names need to go through realpath. 1064 generated_files = { 1065 realpath(os.path.join(buildconfig.topobjdir, f)): f 1066 for (f, _) in get_generated_sources() 1067 } 1068 _, bucket = get_s3_region_and_bucket() 1069 dumper = GetPlatformSpecificDumper( 1070 dump_syms=args[0], 1071 symbol_path=args[1], 1072 copy_debug=options.copy_debug, 1073 archs=options.archs, 1074 srcdirs=options.srcdir, 1075 vcsinfo=options.vcsinfo, 1076 srcsrv=options.srcsrv, 1077 generated_files=generated_files, 1078 s3_bucket=bucket, 1079 file_mapping=file_mapping, 1080 ) 1081 1082 dumper.Process(args[2], options.count_ctors) 1083 1084 1085# run main if run directly 1086if __name__ == "__main__": 1087 main() 1088