#!/usr/bin/env python3
from __future__ import print_function
import argparse
import os
import re
import subprocess
import sys
import traceback
# Python 2 and 3 compatibility layers.
if sys.version_info >= (3, 0):
from os import makedirs
from shutil import which
def get_byte(buf, idx):
return buf[idx]
def check_silent_call(cmd):
subprocess.check_call(cmd, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
else:
def makedirs(path, exist_ok):
if exist_ok and os.path.isdir(path):
return
return os.makedirs(path)
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
def is_executable(path):
return (os.path.exists(file_path) and \
os.access(file_path, mode) and \
not os.path.isdir(file_path))
if path is None:
path = os.environ.get('PATH', os.defpath)
for path_dir in path.split(os.pathsep):
for file_name in os.listdir(path_dir):
if file_name != cmd:
continue
file_path = os.path.join(path_dir, file_name)
if is_executable(file_path):
return file_path
return None
def get_byte(buf, idx):
return ord(buf[idx])
def check_silent_call(cmd):
with open(os.devnull, 'wb') as devnull:
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
FileNotFoundError = OSError
# Path constants.
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
AOSP_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, *['..'] * 4))
ABI_DUMPER = os.path.join(AOSP_DIR, 'external', 'abi-dumper', 'abi-dumper.pl')
VTABLE_DUMPER = 'vndk-vtable-dumper'
BINARY_ABI_DUMP_EXT = '.bdump'
# Compilation targets.
class Target(object):
def __init__(self, arch, gcc_arch, gcc_prefix, gcc_version, lib_dir_name):
self.arch = arch
self.gcc_dir = self._get_prebuilts_gcc(gcc_arch, gcc_prefix,
gcc_version)
self.gcc_prefix = gcc_prefix
self.lib_dir_name = lib_dir_name
def _get_prebuilts_host(self):
"""Get the host dir for prebuilts"""
if sys.platform.startswith('linux'):
return 'linux-x86'
if sys.platform.startswith('darwin'):
return 'darwin-x86'
raise NotImplementedError('unknown platform')
def _get_prebuilts_gcc(self, gcc_arch, gcc_prefix, gcc_version):
"""Get the path to gcc for the current platform"""
return os.path.join(AOSP_DIR, 'prebuilts', 'gcc',
self._get_prebuilts_host(), gcc_arch,
gcc_prefix + gcc_version)
def get_exe(self, name):
"""Get the path to prebuilt executable"""
return os.path.join(self.gcc_dir, 'bin', self.gcc_prefix + name)
class TargetRegistry(object):
def __init__(self):
self.targets = dict()
def add(self, arch, gcc_arch, gcc_prefix, gcc_version, lib_dir_name):
self.targets[arch] = Target(arch, gcc_arch, gcc_prefix, gcc_version,
lib_dir_name)
def get(self, arch_name, var_name):
try:
return self.targets[arch_name]
except KeyError:
print('{}: error: unknown {}: {}'
.format(sys.argv[0], var_name, arch_name), file=sys.stderr)
sys.exit(1)
@staticmethod
def create():
res = TargetRegistry()
res.add('arm', 'arm', 'arm-linux-androideabi-', '4.9', 'lib')
res.add('arm64', 'aarch64', 'aarch64-linux-android-', '4.9', 'lib64')
res.add('mips', 'mips', 'mips64el-linux-android-', '4.9', 'lib')
res.add('mips64', 'mips', 'mips64el-linux-android-', '4.9', 'lib64')
res.add('x86', 'x86', 'x86_64-linux-android-', '4.9', 'lib')
res.add('x86_64', 'x86', 'x86_64-linux-android-', '4.9', 'lib64')
return res
# Command tests.
def test_command(name, options, expected_output):
def is_command_valid():
try:
if os.path.exists(name) and os.access(name, os.F_OK | os.X_OK):
exec_path = name
else:
exec_path = which(name)
if not exec_path:
return False
output = subprocess.check_output([exec_path] + options)
return (expected_output in output)
except Exception:
traceback.print_exc()
return False
if not is_command_valid():
print('error: failed to run {} command'.format(name), file=sys.stderr)
sys.exit(1)
def test_readelf_command(readelf):
test_command(readelf, ['-v'], b'GNU readelf')
def test_objdump_command(objdump):
test_command(objdump, ['-v'], b'GNU objdump')
def test_vtable_dumper_command():
test_command(VTABLE_DUMPER, ['--version'], b'vndk-vtable-dumper')
def test_abi_dumper_command():
test_command(ABI_DUMPER, ['-v'], b'ABI Dumper')
def test_all_commands(readelf, objdump):
test_readelf_command(readelf)
test_objdump_command(objdump)
test_vtable_dumper_command()
test_abi_dumper_command()
# ELF file format constants.
ELF_MAGIC = b'\x7fELF'
EI_CLASS = 4
EI_DATA = 5
EI_NIDENT = 8
ELFCLASS32 = 1
ELFCLASS64 = 2
ELFDATA2LSB = 1
ELFDATA2MSB = 2
# ELF file check utilities.
def is_elf_ident(buf):
# Check the length of ELF ident.
if len(buf) != EI_NIDENT:
return False
# Check ELF magic word.
if buf[0:4] != ELF_MAGIC:
return False
# Check ELF machine word size.
ei_class = get_byte(buf, EI_CLASS)
if ei_class != ELFCLASS32 and ei_class != ELFCLASS64:
return False
# Check ELF endianness.
ei_data = get_byte(buf, EI_DATA)
if ei_data != ELFDATA2LSB and ei_data != ELFDATA2MSB:
return False
return True
def is_elf_file(path):
try:
with open(path, 'rb') as f:
return is_elf_ident(f.read(EI_NIDENT))
except FileNotFoundError:
return False
def create_vndk_lib_name_filter(file_list_path):
if not file_list_path:
def accept_all_filenames(name):
return True
return accept_all_filenames
with open(file_list_path, 'r') as f:
lines = f.read().splitlines()
patt = re.compile('^(?:' +
'|'.join('(?:' + re.escape(x) + ')' for x in lines) +
')$')
def accept_matched_filenames(name):
return patt.match(name)
return accept_matched_filenames
def create_abi_reference_dump(out_dir, symbols_dir, api_level, show_commands,
target, is_vndk_lib_name):
# Check command line tools.
readelf = target.get_exe('readelf')
objdump = target.get_exe('objdump')
test_all_commands(readelf, objdump)
# Check library directory.
lib_dir = os.path.join(symbols_dir, 'system', target.lib_dir_name)
if not os.path.exists(lib_dir):
print('error: failed to find lib directory:', lib_dir, file=sys.stderr)
sys.exit(1)
# Append target architecture to output directory path.
out_dir = os.path.join(out_dir, target.arch)
# Process libraries.
cmd_base = [ABI_DUMPER, '-lver', api_level, '-objdump', objdump,
'-readelf', readelf, '-vt-dumper', which(VTABLE_DUMPER),
'-use-tu-dump', '--quiet']
num_processed = 0
lib_dir = os.path.abspath(lib_dir)
prefix_len = len(lib_dir) + 1
for base, dirnames, filenames in os.walk(lib_dir):
for filename in filenames:
if not is_vndk_lib_name(filename):
continue
path = os.path.join(base, filename)
if not is_elf_file(path):
continue
rel_path = path[prefix_len:]
out_path = os.path.join(out_dir, rel_path) + BINARY_ABI_DUMP_EXT
makedirs(os.path.dirname(out_path), exist_ok=True)
cmd = cmd_base + [path, '-o', out_path]
if show_commands:
print('run:', ' '.join(cmd))
else:
print('process:', path)
check_silent_call(cmd)
num_processed += 1
return num_processed
def get_build_var_from_build_system(name):
"""Get build system variable for the launched target."""
if 'ANDROID_PRODUCT_OUT' not in os.environ:
return None
cmd = ['make', '--no-print-directory', '-f', 'build/core/config.mk',
'dumpvar-' + name]
environ = dict(os.environ)
environ['CALLED_FROM_SETUP'] = 'true'
environ['BUILD_SYSTEM'] = 'build/core'
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=environ,
cwd=AOSP_DIR)
out, err = proc.communicate()
return out.decode('utf-8').strip()
def get_build_var(name, args):
"""Get build system variable either from command line option or build
system."""
value = getattr(args, name.lower(), None)
return value if value else get_build_var_from_build_system(name)
def report_missing_argument(parser, arg_name):
parser.print_usage()
print('{}: error: the following arguments are required: {}'
.format(sys.argv[0], arg_name), file=sys.stderr)
sys.exit(1)
def main():
# Parse command line options.
parser = argparse.ArgumentParser()
parser.add_argument('--output', '-o', metavar='path',
help='output directory for abi reference dump')
parser.add_argument('--vndk-list', help='VNDK library list')
parser.add_argument('--api-level', default='24', help='VNDK API level')
parser.add_argument('--target-arch', help='target architecture')
parser.add_argument('--target-2nd-arch', help='second target architecture')
parser.add_argument('--product-out', help='android product out')
parser.add_argument('--target-product', help='target product')
parser.add_argument('--target-build-variant', help='target build variant')
parser.add_argument('--symbols-dir', help='unstripped symbols directory')
parser.add_argument('--show-commands', action='store_true',
help='Show the abi-dumper command')
args = parser.parse_args()
# Check the symbols directory.
if args.symbols_dir:
symbols_dir = args.symbols_dir
else:
# If the user did not specify the symbols directory, try to create
# one from ANDROID_PRODUCT_OUT.
product_out = get_build_var('PRODUCT_OUT', args)
if not product_out:
report_missing_argument(parser, '--symbols-dir')
if not os.path.isabs(product_out):
product_out = os.path.join(AOSP_DIR, product_out)
symbols_dir = os.path.join(product_out, 'symbols')
# Check the output directory.
if args.output:
out_dir = args.output
else:
# If the user did not specify the output directory, try to create one
# default output directory from TARGET_PRODUCT and
# TARGET_BUILD_VARIANT.
target_product = get_build_var('TARGET_PRODUCT', args)
target_build_variant = get_build_var('TARGET_BUILD_VARIANT', args)
if not target_product or not target_build_variant:
report_missing_argument(parser, '--output/-o')
lunch_name = target_product + '-' + target_build_variant
out_dir = os.path.join(AOSP_DIR, 'vndk', 'dumps', lunch_name)
# Check the targets.
target_registry = TargetRegistry.create()
targets = []
arch_name = get_build_var('TARGET_ARCH', args)
if not arch_name:
report_missing_argument(parser, '--target-arch')
targets.append(target_registry.get(arch_name, 'TARGET_ARCH'))
must_have_2nd_arch = (targets[0].lib_dir_name == 'lib64')
arch_name = get_build_var('TARGET_2ND_ARCH', args)
if arch_name:
targets.append(target_registry.get(arch_name, 'TARGET_2ND_ARCH'))
elif must_have_2nd_arch:
report_missing_argument(parser, '--target-2nd-arch')
# Dump all libraries for the specified architectures.
num_processed = 0
for target in targets:
num_processed += create_abi_reference_dump(
out_dir, symbols_dir, args.api_level, args.show_commands,
target, create_vndk_lib_name_filter(args.vndk_list))
# Print a summary at the end.
_TERM_WIDTH = 79
print()
print('-' * _TERM_WIDTH)
print('msg: Reference dump created at directory:', out_dir)
print('msg: Processed', num_processed, 'libraries')
if __name__ == '__main__':
main()