1#!/usr/bin/env python
2
3import argparse
4import os
5import platform
6import subprocess
7
8# This list contains symbols that _might_ be exported for some platforms
9PLATFORM_SYMBOLS = [
10    '__bss_end__',
11    '__bss_start__',
12    '__bss_start',
13    '__cxa_guard_abort',
14    '__cxa_guard_acquire',
15    '__cxa_guard_release',
16    '__end__',
17    '_bss_end__',
18    '_edata',
19    '_end',
20    '_fini',
21    '_init',
22]
23
24def get_symbols_nm(nm, lib):
25    '''
26    List all the (non platform-specific) symbols exported by the library
27    using `nm`
28    '''
29    symbols = []
30    platform_name = platform.system()
31    output = subprocess.check_output([nm, '-gP', lib],
32                                     stderr=open(os.devnull, 'w')).decode("ascii")
33    for line in output.splitlines():
34        fields = line.split()
35        if len(fields) == 2 or fields[1] == 'U':
36            continue
37        symbol_name = fields[0]
38        if platform_name == 'Linux':
39            if symbol_name in PLATFORM_SYMBOLS:
40                continue
41        elif platform_name == 'Darwin':
42            assert symbol_name[0] == '_'
43            symbol_name = symbol_name[1:]
44        symbols.append(symbol_name)
45    return symbols
46
47
48def get_symbols_dumpbin(dumpbin, lib):
49    '''
50    List all the (non platform-specific) symbols exported by the library
51    using `dumpbin`
52    '''
53    symbols = []
54    output = subprocess.check_output([dumpbin, '/exports', lib],
55                                     stderr=open(os.devnull, 'w')).decode("ascii")
56    for line in output.splitlines():
57        fields = line.split()
58        # The lines with the symbols are made of at least 4 columns; see details below
59        if len(fields) < 4:
60            continue
61        try:
62            # Making sure the first 3 columns are a dec counter, a hex counter
63            # and a hex address
64            _ = int(fields[0], 10)
65            _ = int(fields[1], 16)
66            _ = int(fields[2], 16)
67        except ValueError:
68            continue
69        symbol_name = fields[3]
70        # De-mangle symbols
71        if symbol_name[0] == '_':
72            symbol_name = symbol_name[1:].split('@')[0]
73        symbols.append(symbol_name)
74    return symbols
75
76
77def main():
78    parser = argparse.ArgumentParser()
79    parser.add_argument('--symbols-file',
80                        action='store',
81                        required=True,
82                        help='path to file containing symbols')
83    parser.add_argument('--lib',
84                        action='store',
85                        required=True,
86                        help='path to library')
87    parser.add_argument('--nm',
88                        action='store',
89                        help='path to binary (or name in $PATH)')
90    parser.add_argument('--dumpbin',
91                        action='store',
92                        help='path to binary (or name in $PATH)')
93    parser.add_argument('--ignore-symbol',
94                        action='append',
95                        help='do not process this symbol')
96    args = parser.parse_args()
97
98    try:
99        if platform.system() == 'Windows':
100            if not args.dumpbin:
101                parser.error('--dumpbin is mandatory')
102            lib_symbols = get_symbols_dumpbin(args.dumpbin, args.lib)
103        else:
104            if not args.nm:
105                parser.error('--nm is mandatory')
106            lib_symbols = get_symbols_nm(args.nm, args.lib)
107    except:
108        # We can't run this test, but we haven't technically failed it either
109        # Return the GNU "skip" error code
110        exit(77)
111    mandatory_symbols = []
112    optional_symbols = []
113    with open(args.symbols_file) as symbols_file:
114        qualifier_optional = '(optional)'
115        for line in symbols_file.readlines():
116
117            # Strip comments
118            line = line.split('#')[0]
119            line = line.strip()
120            if not line:
121                continue
122
123            # Line format:
124            # [qualifier] symbol
125            qualifier = None
126            symbol = None
127
128            fields = line.split()
129            if len(fields) == 1:
130                symbol = fields[0]
131            elif len(fields) == 2:
132                qualifier = fields[0]
133                symbol = fields[1]
134            else:
135                print(args.symbols_file + ': invalid format: ' + line)
136                exit(1)
137
138            # The only supported qualifier is 'optional', which means the
139            # symbol doesn't have to be exported by the library
140            if qualifier and not qualifier == qualifier_optional:
141                print(args.symbols_file + ': invalid qualifier: ' + qualifier)
142                exit(1)
143
144            if qualifier == qualifier_optional:
145                optional_symbols.append(symbol)
146            else:
147                mandatory_symbols.append(symbol)
148
149    unknown_symbols = []
150    for symbol in lib_symbols:
151        if symbol in mandatory_symbols:
152            continue
153        if symbol in optional_symbols:
154            continue
155        if args.ignore_symbol and symbol in args.ignore_symbol:
156            continue
157        if symbol[:2] == '_Z':
158            # As ajax found out, the compiler intentionally exports symbols
159            # that we explicitely asked it not to export, and we can't do
160            # anything about it:
161            # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36022#c4
162            continue
163        unknown_symbols.append(symbol)
164
165    missing_symbols = [
166        sym for sym in mandatory_symbols if sym not in lib_symbols
167    ]
168
169    for symbol in unknown_symbols:
170        print(args.lib + ': unknown symbol exported: ' + symbol)
171
172    for symbol in missing_symbols:
173        print(args.lib + ': missing symbol: ' + symbol)
174
175    if unknown_symbols or missing_symbols:
176        exit(1)
177    exit(0)
178
179
180if __name__ == '__main__':
181    main()
182