1#!/usr/bin/env python3
2
3from __future__ import print_function
4
5import argparse
6import os
7import re
8import subprocess
9import sys
10import traceback
11
12# Python 2 and 3 compatibility layers.
13if sys.version_info >= (3, 0):
14    from os import makedirs
15    from shutil import which
16
17    def get_byte(buf, idx):
18        return buf[idx]
19
20    def check_silent_call(cmd):
21        subprocess.check_call(cmd, stdout=subprocess.DEVNULL,
22                              stderr=subprocess.DEVNULL)
23else:
24    def makedirs(path, exist_ok):
25        if exist_ok and os.path.isdir(path):
26            return
27        return os.makedirs(path)
28
29    def which(cmd, mode=os.F_OK | os.X_OK, path=None):
30        def is_executable(path):
31            return (os.path.exists(file_path) and \
32                    os.access(file_path, mode) and \
33                    not os.path.isdir(file_path))
34        if path is None:
35            path = os.environ.get('PATH', os.defpath)
36        for path_dir in path.split(os.pathsep):
37            for file_name in os.listdir(path_dir):
38                if file_name != cmd:
39                    continue
40                file_path = os.path.join(path_dir, file_name)
41                if is_executable(file_path):
42                    return file_path
43        return None
44
45    def get_byte(buf, idx):
46        return ord(buf[idx])
47
48    def check_silent_call(cmd):
49        with open(os.devnull, 'wb') as devnull:
50            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
51
52    FileNotFoundError = OSError
53
54
55# Path constants.
56SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
57AOSP_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, *['..'] * 4))
58ABI_DUMPER = os.path.join(AOSP_DIR, 'external', 'abi-dumper', 'abi-dumper.pl')
59VTABLE_DUMPER = 'vndk-vtable-dumper'
60BINARY_ABI_DUMP_EXT = '.bdump'
61
62
63# Compilation targets.
64class Target(object):
65    def __init__(self, arch, gcc_arch, gcc_prefix, gcc_version, lib_dir_name):
66        self.arch = arch
67        self.gcc_dir = self._get_prebuilts_gcc(gcc_arch, gcc_prefix,
68                                               gcc_version)
69        self.gcc_prefix = gcc_prefix
70        self.lib_dir_name = lib_dir_name
71
72    def _get_prebuilts_host(self):
73        """Get the host dir for prebuilts"""
74        if sys.platform.startswith('linux'):
75            return 'linux-x86'
76        if sys.platform.startswith('darwin'):
77            return 'darwin-x86'
78        raise NotImplementedError('unknown platform')
79
80    def _get_prebuilts_gcc(self, gcc_arch, gcc_prefix, gcc_version):
81        """Get the path to gcc for the current platform"""
82        return os.path.join(AOSP_DIR, 'prebuilts', 'gcc',
83                            self._get_prebuilts_host(), gcc_arch,
84                            gcc_prefix + gcc_version)
85
86    def get_exe(self, name):
87        """Get the path to prebuilt executable"""
88        return os.path.join(self.gcc_dir, 'bin', self.gcc_prefix + name)
89
90class TargetRegistry(object):
91    def __init__(self):
92        self.targets = dict()
93
94    def add(self, arch, gcc_arch, gcc_prefix, gcc_version, lib_dir_name):
95         self.targets[arch] = Target(arch, gcc_arch, gcc_prefix, gcc_version,
96                                     lib_dir_name)
97
98    def get(self, arch_name, var_name):
99        try:
100            return self.targets[arch_name]
101        except KeyError:
102            print('{}: error: unknown {}: {}'
103                    .format(sys.argv[0], var_name, arch_name), file=sys.stderr)
104            sys.exit(1)
105
106    @staticmethod
107    def create():
108        res = TargetRegistry()
109        res.add('arm', 'arm', 'arm-linux-androideabi-', '4.9', 'lib')
110        res.add('arm64', 'aarch64', 'aarch64-linux-android-', '4.9', 'lib64')
111        res.add('mips', 'mips', 'mips64el-linux-android-', '4.9', 'lib')
112        res.add('mips64', 'mips', 'mips64el-linux-android-', '4.9', 'lib64')
113        res.add('x86', 'x86', 'x86_64-linux-android-', '4.9', 'lib')
114        res.add('x86_64', 'x86', 'x86_64-linux-android-', '4.9', 'lib64')
115        return res
116
117
118# Command tests.
119def test_command(name, options, expected_output):
120    def is_command_valid():
121        try:
122            if os.path.exists(name) and os.access(name, os.F_OK | os.X_OK):
123                exec_path = name
124            else:
125                exec_path = which(name)
126                if not exec_path:
127                    return False
128            output = subprocess.check_output([exec_path] + options)
129            return (expected_output in output)
130        except Exception:
131            traceback.print_exc()
132            return False
133
134    if not is_command_valid():
135        print('error: failed to run {} command'.format(name), file=sys.stderr)
136        sys.exit(1)
137
138def test_readelf_command(readelf):
139    test_command(readelf, ['-v'], b'GNU readelf')
140
141def test_objdump_command(objdump):
142    test_command(objdump, ['-v'], b'GNU objdump')
143
144def test_vtable_dumper_command():
145    test_command(VTABLE_DUMPER, ['--version'], b'vndk-vtable-dumper')
146
147def test_abi_dumper_command():
148    test_command(ABI_DUMPER, ['-v'], b'ABI Dumper')
149
150def test_all_commands(readelf, objdump):
151    test_readelf_command(readelf)
152    test_objdump_command(objdump)
153    test_vtable_dumper_command()
154    test_abi_dumper_command()
155
156
157# ELF file format constants.
158ELF_MAGIC = b'\x7fELF'
159
160EI_CLASS = 4
161EI_DATA = 5
162EI_NIDENT = 8
163
164ELFCLASS32 = 1
165ELFCLASS64 = 2
166
167ELFDATA2LSB = 1
168ELFDATA2MSB = 2
169
170
171# ELF file check utilities.
172def is_elf_ident(buf):
173    # Check the length of ELF ident.
174    if len(buf) != EI_NIDENT:
175        return False
176
177    # Check ELF magic word.
178    if buf[0:4] != ELF_MAGIC:
179        return False
180
181    # Check ELF machine word size.
182    ei_class = get_byte(buf, EI_CLASS)
183    if ei_class != ELFCLASS32 and ei_class != ELFCLASS64:
184        return False
185
186    # Check ELF endianness.
187    ei_data = get_byte(buf, EI_DATA)
188    if ei_data != ELFDATA2LSB and ei_data != ELFDATA2MSB:
189        return False
190
191    return True
192
193def is_elf_file(path):
194    try:
195        with open(path, 'rb') as f:
196            return is_elf_ident(f.read(EI_NIDENT))
197    except FileNotFoundError:
198        return False
199
200def create_vndk_lib_name_filter(file_list_path):
201    if not file_list_path:
202        def accept_all_filenames(name):
203            return True
204        return accept_all_filenames
205
206    with open(file_list_path, 'r') as f:
207        lines = f.read().splitlines()
208
209    patt = re.compile('^(?:' +
210                      '|'.join('(?:' + re.escape(x) + ')' for x in lines) +
211                      ')$')
212    def accept_matched_filenames(name):
213        return patt.match(name)
214    return accept_matched_filenames
215
216def create_abi_reference_dump(out_dir, symbols_dir, api_level, show_commands,
217                              target, is_vndk_lib_name):
218    # Check command line tools.
219    readelf = target.get_exe('readelf')
220    objdump = target.get_exe('objdump')
221    test_all_commands(readelf, objdump)
222
223    # Check library directory.
224    lib_dir = os.path.join(symbols_dir, 'system', target.lib_dir_name)
225    if not os.path.exists(lib_dir):
226        print('error: failed to find lib directory:', lib_dir, file=sys.stderr)
227        sys.exit(1)
228
229    # Append target architecture to output directory path.
230    out_dir = os.path.join(out_dir, target.arch)
231
232    # Process libraries.
233    cmd_base = [ABI_DUMPER, '-lver', api_level, '-objdump', objdump,
234                '-readelf', readelf, '-vt-dumper', which(VTABLE_DUMPER),
235                '-use-tu-dump', '--quiet']
236
237    num_processed = 0
238    lib_dir = os.path.abspath(lib_dir)
239    prefix_len = len(lib_dir) + 1
240    for base, dirnames, filenames in os.walk(lib_dir):
241        for filename in filenames:
242            if not is_vndk_lib_name(filename):
243                continue
244
245            path = os.path.join(base, filename)
246            if not is_elf_file(path):
247                continue
248
249            rel_path = path[prefix_len:]
250            out_path = os.path.join(out_dir, rel_path) + BINARY_ABI_DUMP_EXT
251
252            makedirs(os.path.dirname(out_path), exist_ok=True)
253            cmd = cmd_base + [path, '-o', out_path]
254            if show_commands:
255                print('run:', ' '.join(cmd))
256            else:
257                print('process:', path)
258            check_silent_call(cmd)
259            num_processed += 1
260
261    return num_processed
262
263def get_build_var_from_build_system(name):
264    """Get build system variable for the launched target."""
265    if 'ANDROID_PRODUCT_OUT' not in os.environ:
266        return None
267
268    cmd = ['make', '--no-print-directory', '-f', 'build/core/config.mk',
269           'dumpvar-' + name]
270
271    environ = dict(os.environ)
272    environ['CALLED_FROM_SETUP'] = 'true'
273    environ['BUILD_SYSTEM'] = 'build/core'
274
275    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
276                            stderr=subprocess.PIPE, env=environ,
277                            cwd=AOSP_DIR)
278    out, err = proc.communicate()
279    return out.decode('utf-8').strip()
280
281def get_build_var(name, args):
282    """Get build system variable either from command line option or build
283    system."""
284    value = getattr(args, name.lower(), None)
285    return value if value else get_build_var_from_build_system(name)
286
287def report_missing_argument(parser, arg_name):
288    parser.print_usage()
289    print('{}: error: the following arguments are required: {}'
290            .format(sys.argv[0], arg_name), file=sys.stderr)
291    sys.exit(1)
292
293def main():
294    # Parse command line options.
295    parser = argparse.ArgumentParser()
296    parser.add_argument('--output', '-o', metavar='path',
297                        help='output directory for abi reference dump')
298    parser.add_argument('--vndk-list', help='VNDK library list')
299    parser.add_argument('--api-level', default='24', help='VNDK API level')
300    parser.add_argument('--target-arch', help='target architecture')
301    parser.add_argument('--target-2nd-arch', help='second target architecture')
302    parser.add_argument('--product-out', help='android product out')
303    parser.add_argument('--target-product', help='target product')
304    parser.add_argument('--target-build-variant', help='target build variant')
305    parser.add_argument('--symbols-dir', help='unstripped symbols directory')
306    parser.add_argument('--show-commands', action='store_true',
307                        help='Show the abi-dumper command')
308    args = parser.parse_args()
309
310    # Check the symbols directory.
311    if args.symbols_dir:
312        symbols_dir = args.symbols_dir
313    else:
314        # If the user did not specify the symbols directory, try to create
315        # one from ANDROID_PRODUCT_OUT.
316        product_out = get_build_var('PRODUCT_OUT', args)
317        if not product_out:
318            report_missing_argument(parser, '--symbols-dir')
319        if not os.path.isabs(product_out):
320            product_out = os.path.join(AOSP_DIR, product_out)
321        symbols_dir = os.path.join(product_out, 'symbols')
322
323    # Check the output directory.
324    if args.output:
325        out_dir = args.output
326    else:
327        # If the user did not specify the output directory, try to create one
328        # default output directory from TARGET_PRODUCT and
329        # TARGET_BUILD_VARIANT.
330
331        target_product = get_build_var('TARGET_PRODUCT', args)
332        target_build_variant = get_build_var('TARGET_BUILD_VARIANT', args)
333        if not target_product or not target_build_variant:
334            report_missing_argument(parser, '--output/-o')
335        lunch_name = target_product + '-' + target_build_variant
336        out_dir = os.path.join(AOSP_DIR, 'vndk', 'dumps', lunch_name)
337
338    # Check the targets.
339    target_registry = TargetRegistry.create()
340    targets = []
341
342    arch_name = get_build_var('TARGET_ARCH', args)
343    if not arch_name:
344        report_missing_argument(parser, '--target-arch')
345    targets.append(target_registry.get(arch_name, 'TARGET_ARCH'))
346    must_have_2nd_arch = (targets[0].lib_dir_name == 'lib64')
347
348    arch_name = get_build_var('TARGET_2ND_ARCH', args)
349    if arch_name:
350        targets.append(target_registry.get(arch_name, 'TARGET_2ND_ARCH'))
351    elif must_have_2nd_arch:
352        report_missing_argument(parser, '--target-2nd-arch')
353
354    # Dump all libraries for the specified architectures.
355    num_processed = 0
356    for target in targets:
357        num_processed += create_abi_reference_dump(
358                out_dir, symbols_dir, args.api_level, args.show_commands,
359                target, create_vndk_lib_name_filter(args.vndk_list))
360
361    # Print a summary at the end.
362    _TERM_WIDTH = 79
363    print()
364    print('-' * _TERM_WIDTH)
365    print('msg: Reference dump created at directory:', out_dir)
366    print('msg: Processed', num_processed, 'libraries')
367
368if __name__ == '__main__':
369    main()
370