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