17ec681f3Smrg#!/usr/bin/env python3
27ec681f3Smrg
37ec681f3Smrgimport argparse
47ec681f3Smrgimport os
57ec681f3Smrgimport platform
67ec681f3Smrgimport subprocess
77ec681f3Smrg
87ec681f3Smrg# This list contains symbols that _might_ be exported for some platforms
97ec681f3SmrgPLATFORM_SYMBOLS = [
107ec681f3Smrg    '__bss_end__',
117ec681f3Smrg    '__bss_start__',
127ec681f3Smrg    '__bss_start',
137ec681f3Smrg    '__cxa_guard_abort',
147ec681f3Smrg    '__cxa_guard_acquire',
157ec681f3Smrg    '__cxa_guard_release',
167ec681f3Smrg    '__end__',
177ec681f3Smrg    '__odr_asan._glapi_Context',
187ec681f3Smrg    '__odr_asan._glapi_Dispatch',
197ec681f3Smrg    '_bss_end__',
207ec681f3Smrg    '_edata',
217ec681f3Smrg    '_end',
227ec681f3Smrg    '_fini',
237ec681f3Smrg    '_init',
247ec681f3Smrg    '_fbss',
257ec681f3Smrg    '_fdata',
267ec681f3Smrg    '_ftext',
277ec681f3Smrg]
287ec681f3Smrg
297ec681f3Smrgdef get_symbols_nm(nm, lib):
307ec681f3Smrg    '''
317ec681f3Smrg    List all the (non platform-specific) symbols exported by the library
327ec681f3Smrg    using `nm`
337ec681f3Smrg    '''
347ec681f3Smrg    symbols = []
357ec681f3Smrg    platform_name = platform.system()
367ec681f3Smrg    output = subprocess.check_output([nm, '-gP', lib],
377ec681f3Smrg                                     stderr=open(os.devnull, 'w')).decode("ascii")
387ec681f3Smrg    for line in output.splitlines():
397ec681f3Smrg        fields = line.split()
407ec681f3Smrg        if len(fields) == 2 or fields[1] == 'U':
417ec681f3Smrg            continue
427ec681f3Smrg        symbol_name = fields[0]
437ec681f3Smrg        if platform_name == 'Linux':
447ec681f3Smrg            if symbol_name in PLATFORM_SYMBOLS:
457ec681f3Smrg                continue
467ec681f3Smrg        elif platform_name == 'Darwin':
477ec681f3Smrg            assert symbol_name[0] == '_'
487ec681f3Smrg            symbol_name = symbol_name[1:]
497ec681f3Smrg        symbols.append(symbol_name)
507ec681f3Smrg    return symbols
517ec681f3Smrg
527ec681f3Smrg
537ec681f3Smrgdef get_symbols_dumpbin(dumpbin, lib):
547ec681f3Smrg    '''
557ec681f3Smrg    List all the (non platform-specific) symbols exported by the library
567ec681f3Smrg    using `dumpbin`
577ec681f3Smrg    '''
587ec681f3Smrg    symbols = []
597ec681f3Smrg    output = subprocess.check_output([dumpbin, '/exports', lib],
607ec681f3Smrg                                     stderr=open(os.devnull, 'w')).decode("ascii")
617ec681f3Smrg    for line in output.splitlines():
627ec681f3Smrg        fields = line.split()
637ec681f3Smrg        # The lines with the symbols are made of at least 4 columns; see details below
647ec681f3Smrg        if len(fields) < 4:
657ec681f3Smrg            continue
667ec681f3Smrg        try:
677ec681f3Smrg            # Making sure the first 3 columns are a dec counter, a hex counter
687ec681f3Smrg            # and a hex address
697ec681f3Smrg            _ = int(fields[0], 10)
707ec681f3Smrg            _ = int(fields[1], 16)
717ec681f3Smrg            _ = int(fields[2], 16)
727ec681f3Smrg        except ValueError:
737ec681f3Smrg            continue
747ec681f3Smrg        symbol_name = fields[3]
757ec681f3Smrg        # De-mangle symbols
767ec681f3Smrg        if symbol_name[0] == '_' and '@' in symbol_name:
777ec681f3Smrg            symbol_name = symbol_name[1:].split('@')[0]
787ec681f3Smrg        symbols.append(symbol_name)
797ec681f3Smrg    return symbols
807ec681f3Smrg
817ec681f3Smrg
827ec681f3Smrgdef main():
837ec681f3Smrg    parser = argparse.ArgumentParser()
847ec681f3Smrg    parser.add_argument('--symbols-file',
857ec681f3Smrg                        action='store',
867ec681f3Smrg                        required=True,
877ec681f3Smrg                        help='path to file containing symbols')
887ec681f3Smrg    parser.add_argument('--lib',
897ec681f3Smrg                        action='store',
907ec681f3Smrg                        required=True,
917ec681f3Smrg                        help='path to library')
927ec681f3Smrg    parser.add_argument('--nm',
937ec681f3Smrg                        action='store',
947ec681f3Smrg                        help='path to binary (or name in $PATH)')
957ec681f3Smrg    parser.add_argument('--dumpbin',
967ec681f3Smrg                        action='store',
977ec681f3Smrg                        help='path to binary (or name in $PATH)')
987ec681f3Smrg    parser.add_argument('--ignore-symbol',
997ec681f3Smrg                        action='append',
1007ec681f3Smrg                        help='do not process this symbol')
1017ec681f3Smrg    args = parser.parse_args()
1027ec681f3Smrg
1037ec681f3Smrg    try:
1047ec681f3Smrg        if platform.system() == 'Windows':
1057ec681f3Smrg            if not args.dumpbin:
1067ec681f3Smrg                parser.error('--dumpbin is mandatory')
1077ec681f3Smrg            lib_symbols = get_symbols_dumpbin(args.dumpbin, args.lib)
1087ec681f3Smrg        else:
1097ec681f3Smrg            if not args.nm:
1107ec681f3Smrg                parser.error('--nm is mandatory')
1117ec681f3Smrg            lib_symbols = get_symbols_nm(args.nm, args.lib)
1127ec681f3Smrg    except:
1137ec681f3Smrg        # We can't run this test, but we haven't technically failed it either
1147ec681f3Smrg        # Return the GNU "skip" error code
1157ec681f3Smrg        exit(77)
1167ec681f3Smrg    mandatory_symbols = []
1177ec681f3Smrg    optional_symbols = []
1187ec681f3Smrg    with open(args.symbols_file) as symbols_file:
1197ec681f3Smrg        qualifier_optional = '(optional)'
1207ec681f3Smrg        for line in symbols_file.readlines():
1217ec681f3Smrg
1227ec681f3Smrg            # Strip comments
1237ec681f3Smrg            line = line.split('#')[0]
1247ec681f3Smrg            line = line.strip()
1257ec681f3Smrg            if not line:
1267ec681f3Smrg                continue
1277ec681f3Smrg
1287ec681f3Smrg            # Line format:
1297ec681f3Smrg            # [qualifier] symbol
1307ec681f3Smrg            qualifier = None
1317ec681f3Smrg            symbol = None
1327ec681f3Smrg
1337ec681f3Smrg            fields = line.split()
1347ec681f3Smrg            if len(fields) == 1:
1357ec681f3Smrg                symbol = fields[0]
1367ec681f3Smrg            elif len(fields) == 2:
1377ec681f3Smrg                qualifier = fields[0]
1387ec681f3Smrg                symbol = fields[1]
1397ec681f3Smrg            else:
1407ec681f3Smrg                print(args.symbols_file + ': invalid format: ' + line)
1417ec681f3Smrg                exit(1)
1427ec681f3Smrg
1437ec681f3Smrg            # The only supported qualifier is 'optional', which means the
1447ec681f3Smrg            # symbol doesn't have to be exported by the library
1457ec681f3Smrg            if qualifier and not qualifier == qualifier_optional:
1467ec681f3Smrg                print(args.symbols_file + ': invalid qualifier: ' + qualifier)
1477ec681f3Smrg                exit(1)
1487ec681f3Smrg
1497ec681f3Smrg            if qualifier == qualifier_optional:
1507ec681f3Smrg                optional_symbols.append(symbol)
1517ec681f3Smrg            else:
1527ec681f3Smrg                mandatory_symbols.append(symbol)
1537ec681f3Smrg
1547ec681f3Smrg    unknown_symbols = []
1557ec681f3Smrg    for symbol in lib_symbols:
1567ec681f3Smrg        if symbol in mandatory_symbols:
1577ec681f3Smrg            continue
1587ec681f3Smrg        if symbol in optional_symbols:
1597ec681f3Smrg            continue
1607ec681f3Smrg        if args.ignore_symbol and symbol in args.ignore_symbol:
1617ec681f3Smrg            continue
1627ec681f3Smrg        if symbol[:2] == '_Z':
1637ec681f3Smrg            # As ajax found out, the compiler intentionally exports symbols
1647ec681f3Smrg            # that we explicitely asked it not to export, and we can't do
1657ec681f3Smrg            # anything about it:
1667ec681f3Smrg            # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36022#c4
1677ec681f3Smrg            continue
1687ec681f3Smrg        unknown_symbols.append(symbol)
1697ec681f3Smrg
1707ec681f3Smrg    missing_symbols = [
1717ec681f3Smrg        sym for sym in mandatory_symbols if sym not in lib_symbols
1727ec681f3Smrg    ]
1737ec681f3Smrg
1747ec681f3Smrg    for symbol in unknown_symbols:
1757ec681f3Smrg        print(args.lib + ': unknown symbol exported: ' + symbol)
1767ec681f3Smrg
1777ec681f3Smrg    for symbol in missing_symbols:
1787ec681f3Smrg        print(args.lib + ': missing symbol: ' + symbol)
1797ec681f3Smrg
1807ec681f3Smrg    if unknown_symbols or missing_symbols:
1817ec681f3Smrg        exit(1)
1827ec681f3Smrg    exit(0)
1837ec681f3Smrg
1847ec681f3Smrg
1857ec681f3Smrgif __name__ == '__main__':
1867ec681f3Smrg    main()
187