1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5'''Given a library, dependentlibs.py prints the list of libraries it depends
6upon that are in the same directory, followed by the library itself.
7'''
8
9import os
10import re
11import subprocess
12import sys
13import mozpack.path as mozpath
14from collections import OrderedDict
15from mozpack.executables import (
16    get_type,
17    ELF,
18    MACHO,
19)
20from buildconfig import substs
21
22def dependentlibs_dumpbin(lib):
23    '''Returns the list of dependencies declared in the given DLL'''
24    try:
25        proc = subprocess.Popen(['dumpbin', '-dependents', lib], stdout = subprocess.PIPE)
26    except OSError:
27        # dumpbin is missing, probably mingw compilation. Try using objdump.
28        return dependentlibs_mingw_objdump(lib)
29    deps = []
30    for line in proc.stdout:
31        # Each line containing an imported library name starts with 4 spaces
32        match = re.match('    (\S+)', line)
33        if match:
34             deps.append(match.group(1))
35        elif len(deps):
36             # There may be several groups of library names, but only the
37             # first one is interesting. The second one is for delayload-ed
38             # libraries.
39             break
40    proc.wait()
41    return deps
42
43def dependentlibs_mingw_objdump(lib):
44    try:
45        proc = subprocess.Popen(['objdump', '-x', lib], stdout = subprocess.PIPE)
46    except OSError:
47        # objdump is missing, try using llvm-objdump.
48        proc = subprocess.Popen(['llvm-objdump', '-private-headers', lib], stdout = subprocess.PIPE)
49    deps = []
50    for line in proc.stdout:
51        match = re.match('\s+DLL Name: (\S+)', line)
52        if match:
53            deps.append(match.group(1))
54    proc.wait()
55    return deps
56
57def dependentlibs_readelf(lib):
58    '''Returns the list of dependencies declared in the given ELF .so'''
59    proc = subprocess.Popen([substs.get('TOOLCHAIN_PREFIX', '') + 'readelf', '-d', lib], stdout = subprocess.PIPE)
60    deps = []
61    for line in proc.stdout:
62        # Each line has the following format:
63        #  tag (TYPE)          value
64        # or with BSD readelf:
65        #  tag TYPE            value
66        # Looking for NEEDED type entries
67        tmp = line.split(' ', 3)
68        if len(tmp) > 3 and 'NEEDED' in tmp[2]:
69            # NEEDED lines look like:
70            # 0x00000001 (NEEDED)             Shared library: [libname]
71            # or with BSD readelf:
72            # 0x00000001 NEEDED               Shared library: [libname]
73            match = re.search('\[(.*)\]', tmp[3])
74            if match:
75                deps.append(match.group(1))
76    proc.wait()
77    return deps
78
79def dependentlibs_otool(lib):
80    '''Returns the list of dependencies declared in the given MACH-O dylib'''
81    proc = subprocess.Popen([substs['OTOOL'], '-l', lib], stdout = subprocess.PIPE)
82    deps= []
83    cmd = None
84    for line in proc.stdout:
85        # otool -l output contains many different things. The interesting data
86        # is under "Load command n" sections, with the content:
87        #           cmd LC_LOAD_DYLIB
88        #       cmdsize 56
89        #          name libname (offset 24)
90        tmp = line.split()
91        if len(tmp) < 2:
92            continue
93        if tmp[0] == 'cmd':
94            cmd = tmp[1]
95        elif cmd == 'LC_LOAD_DYLIB' and tmp[0] == 'name':
96            deps.append(re.sub('^@executable_path/','',tmp[1]))
97    proc.wait()
98    return deps
99
100def dependentlibs(lib, libpaths, func):
101    '''For a given library, returns the list of recursive dependencies that can
102    be found in the given list of paths, followed by the library itself.'''
103    assert(libpaths)
104    assert(isinstance(libpaths, list))
105    deps = OrderedDict()
106    for dep in func(lib):
107        if dep in deps or os.path.isabs(dep):
108            continue
109        for dir in libpaths:
110            deppath = os.path.join(dir, dep)
111            if os.path.exists(deppath):
112                deps.update(dependentlibs(deppath, libpaths, func))
113                # Black list the ICU data DLL because preloading it at startup
114                # leads to startup performance problems because of its excessive
115                # size (around 10MB).
116                if not dep.startswith("icu"):
117                    deps[dep] = deppath
118                break
119
120    return deps
121
122def gen_list(output, lib):
123    libpaths = [os.path.join(substs['DIST'], 'bin')]
124    binary_type = get_type(lib)
125    if binary_type == ELF:
126        func = dependentlibs_readelf
127    elif binary_type == MACHO:
128        func = dependentlibs_otool
129    else:
130        ext = os.path.splitext(lib)[1]
131        assert(ext == '.dll')
132        func = dependentlibs_dumpbin
133
134    deps = dependentlibs(lib, libpaths, func)
135    deps[lib] = mozpath.join(libpaths[0], lib)
136    output.write('\n'.join(deps.keys()) + '\n')
137
138    with open(output.name + ".gtest", 'w') as gtest_out:
139        libs = deps.keys()
140        libs[-1] = 'gtest/' + libs[-1]
141        gtest_out.write('\n'.join(libs) + '\n')
142
143    return set(deps.values())
144
145def main():
146    gen_list(sys.stdout, sys.argv[1])
147
148if __name__ == '__main__':
149    main()
150