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