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