198 lines
7.7 KiB
Python
Executable File
198 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2022 L. E. Segovia <amy@amyspark.me>
|
|
#
|
|
# This file is part of the FFmpeg Meson build
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import argparse
|
|
import errno
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
|
|
|
|
def output(platform, symbols):
|
|
if platform == 'win':
|
|
print("EXPORTS")
|
|
print(*[f' {symbol}' for symbol in sorted(set(symbols))], sep='\n')
|
|
elif platform == 'darwin':
|
|
print(*[f'{prefix}{symbol}' for symbol in sorted(set(symbols))], sep='\n')
|
|
else:
|
|
print('{')
|
|
print(' global:')
|
|
print(
|
|
*[f' {prefix}{symbol};' for symbol in sorted(set(symbols))], sep='\n')
|
|
print(' local:')
|
|
print(' *;')
|
|
print('};')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
arg_parser = argparse.ArgumentParser(
|
|
description='Craft the symbols exports file')
|
|
|
|
arg_parser.add_argument('--prefix', metavar='PREFIX',
|
|
help='Prefix for extern symbols')
|
|
g = arg_parser.add_argument_group('Library parsing tool')
|
|
group = g.add_mutually_exclusive_group(required=True)
|
|
group.add_argument('--nm', metavar='NM_PATH', type=pathlib.Path,
|
|
help='If specified, runs this instead of dumpbin (MinGW)')
|
|
group.add_argument('--dumpbin', metavar='DUMPBIN_PATH', type=pathlib.Path,
|
|
help='If specified, runs this instead of nm (MSVC)')
|
|
group.add_argument(
|
|
'--list', action='store_true', help='If specified, consider FILE as an exported symbols list instead of a library')
|
|
g = arg_parser.add_argument_group('Symbol naming')
|
|
group = g.add_mutually_exclusive_group(required=True)
|
|
group.add_argument('--regex', metavar='REGEX', type=str,
|
|
nargs='+',
|
|
help='Regular expression for exported symbols')
|
|
group.add_argument('--vscript', metavar='VERSION_SCRIPT',
|
|
type=argparse.FileType('r'), help='Version script')
|
|
arg_parser.add_argument('--os', type=str, choices=('win', 'linux', 'darwin'),
|
|
default='linux', required=True,
|
|
help='Target operating system for the exports file (win = MSVC module definition file, linux = version script, darwin = exported symbols list)')
|
|
arg_parser.add_argument('libnames', metavar='FILE', type=pathlib.Path,
|
|
nargs='+',
|
|
help='Source file(s) to parse')
|
|
|
|
args = arg_parser.parse_args()
|
|
|
|
libnames = args.libnames
|
|
|
|
for libname in libnames:
|
|
if not libname.exists():
|
|
raise FileNotFoundError(
|
|
errno.ENOENT, os.strerror(errno.ENOENT), libname)
|
|
|
|
if not args.list and len(libnames) > 1:
|
|
raise ValueError("Expect 1 filename as argument.")
|
|
|
|
prefix = args.prefix or ''
|
|
started = 0
|
|
regex = []
|
|
|
|
if args.vscript:
|
|
for line in args.vscript:
|
|
# We only care about global symbols
|
|
if re.match(r'^\s+global:', line):
|
|
started = 1
|
|
line = re.sub(r'^\s+global: *', '', line)
|
|
else:
|
|
if re.match(r'^\s+local:', line):
|
|
started = 0
|
|
|
|
if started == 0:
|
|
continue
|
|
|
|
line = line.replace(';', '')
|
|
|
|
for exp in line.split():
|
|
# Remove leading and trailing whitespace
|
|
regex.append(exp.strip())
|
|
else:
|
|
regex.extend(args.regex)
|
|
|
|
# Ensure things are compatible on Windows with Python 3.7.x
|
|
libname_path_posix = pathlib.Path(libnames[0]).as_posix()
|
|
|
|
if args.list:
|
|
dump = []
|
|
for libname in libnames:
|
|
syms = libname.open('r', encoding='utf-8').readlines()
|
|
# Strip whitespaces
|
|
syms = [x.strip() for x in syms]
|
|
# Exclude blank lines
|
|
syms = [x for x in syms if len(x) > 0]
|
|
dump.extend(syms)
|
|
elif args.nm is not None:
|
|
# Use eval, since NM="nm -g"
|
|
# Add -j to ensure only symbol names are output (otherwise in macOS
|
|
# a race condition can occur in the redirection)
|
|
# And use `--no-llvm-bc` in case it's /usr/bin/nm on macOS
|
|
s = subprocess.run(
|
|
[args.nm, '-U', '-g', '-j', '--no-llvm-bc', libname_path_posix],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
check=False,
|
|
)
|
|
if s.returncode != 0:
|
|
# If it fails, retry with --defined-only (non macOS)
|
|
s = subprocess.run(
|
|
[args.nm, '--defined-only', '-g', '-j', '--no-llvm-bc', libname_path_posix],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
check=False,
|
|
)
|
|
if s.returncode != 0:
|
|
# If it fails, retry without skipping LLVM bitcode (macOS flag)
|
|
# Don't use -U, as that was an alias for --unicode= instead of
|
|
# --defined-only before Binutils 2.39
|
|
s = subprocess.run(
|
|
[args.nm, '--defined-only', '-g', '-j', libname_path_posix],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
check=False,
|
|
)
|
|
if s.returncode != 0:
|
|
# -j was added only in Binutils 2.37
|
|
s = subprocess.run(
|
|
[args.nm, '--defined-only', '-g', libname_path_posix],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
universal_newlines=True,
|
|
check=True,
|
|
)
|
|
dump = s.stdout.splitlines()
|
|
# Exclude lines with ':' (object name)
|
|
dump = [x for x in dump if ":" not in x]
|
|
# Exclude blank lines
|
|
dump = [x for x in dump if len(x) > 0]
|
|
# Subst the prefix out
|
|
dump = [re.sub(f'^{prefix}', '', x) for x in dump]
|
|
else:
|
|
dump = subprocess.run([pathlib.Path(args.dumpbin).as_posix(), '-linkermember:1', libname_path_posix],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True).stdout.splitlines()
|
|
# Find the index of the first line with
|
|
# "public symbols", keep the rest
|
|
# Then the line with " Summary",
|
|
# delete it and the rest
|
|
for i, line in enumerate(dump):
|
|
if 'public symbols' in line:
|
|
start = i
|
|
elif re.match(r'\s+Summary', line):
|
|
end = i
|
|
dump = dump[start:end]
|
|
# Substitute prefix out
|
|
dump = [re.sub(fr'\s+{prefix}', ' ', x) for x in dump]
|
|
# Substitute big chonky spaces out
|
|
dump = [re.sub(r'\s+', ' ', x) for x in dump]
|
|
# Exclude blank lines
|
|
dump = [x for x in dump if len(x) > 0]
|
|
# Take only the *second* field (split by spaces)
|
|
# Python's split excludes whitespace at the beginning
|
|
dump = [x.split()[1] for x in dump]
|
|
|
|
symbols = []
|
|
for exp in regex:
|
|
for i in dump:
|
|
if re.match(exp, i):
|
|
symbols.append(i)
|
|
|
|
output(args.os, symbols)
|