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