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