• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3import os
4import re
5import shutil
6import subprocess
7import sys
8import tempfile
9import collections
10
11
12SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
13
14try:
15    AOSP_DIR = os.environ['ANDROID_BUILD_TOP']
16except KeyError:
17    print('error: ANDROID_BUILD_TOP environment variable is not set.',
18          file=sys.stderr)
19    sys.exit(1)
20
21BUILTIN_HEADERS_DIR = (
22    os.path.join(AOSP_DIR, 'bionic', 'libc', 'include'),
23    os.path.join(AOSP_DIR, 'external', 'libcxx', 'include'),
24    os.path.join(AOSP_DIR, 'prebuilts', 'clang-tools', 'linux-x86',
25                 'clang-headers'),
26)
27
28SO_EXT = '.so'
29SOURCE_ABI_DUMP_EXT_END = '.lsdump'
30SOURCE_ABI_DUMP_EXT = SO_EXT + SOURCE_ABI_DUMP_EXT_END
31VENDOR_SUFFIX = '.vendor'
32
33DEFAULT_CPPFLAGS = ['-x', 'c++', '-std=c++11']
34DEFAULT_CFLAGS = ['-std=gnu99']
35DEFAULT_HEADER_FLAGS = ["-dump-function-declarations"]
36DEFAULT_FORMAT = 'ProtobufTextFormat'
37
38
39class Target(object):
40    def __init__(self, is_2nd, product):
41        extra = '_2ND' if is_2nd else ''
42        build_vars_to_fetch = ['TARGET_ARCH',
43                               'TARGET{}_ARCH'.format(extra),
44                               'TARGET{}_ARCH_VARIANT'.format(extra),
45                               'TARGET{}_CPU_VARIANT'.format(extra)]
46        build_vars = get_build_vars_for_product(build_vars_to_fetch, product)
47        self.primary_arch = build_vars[0]
48        assert self.primary_arch != ''
49        self.arch = build_vars[1]
50        self.arch_variant = build_vars[2]
51        self.cpu_variant = build_vars[3]
52
53    def get_arch_str(self):
54        """Return a string that represents the architecture and the primary
55        architecture.
56        """
57        if not self.arch or self.arch == self.primary_arch:
58            return self.primary_arch
59        return self.arch + '_' + self.primary_arch
60
61    def get_arch_cpu_str(self):
62        """Return a string that represents the architecture, the architecture
63        variant, and the CPU variant.
64
65        If TARGET_ARCH == TARGET_ARCH_VARIANT, soong makes targetArchVariant
66        empty. This is the case for aosp_x86_64.
67        """
68        if not self.arch_variant or self.arch_variant == self.arch:
69            arch_variant = ''
70        else:
71            arch_variant = '_' + self.arch_variant
72
73        if not self.cpu_variant or self.cpu_variant == 'generic':
74            cpu_variant = ''
75        else:
76            cpu_variant = '_' + self.cpu_variant
77
78        return self.arch + arch_variant + cpu_variant
79
80
81def _validate_dump_content(dump_path):
82    """Make sure that the dump contains relative source paths."""
83    with open(dump_path, 'r') as f:
84        for line_number, line in enumerate(f, 1):
85            start = 0
86            while True:
87                start = line.find(AOSP_DIR, start)
88                if start < 0:
89                    break
90                # The substring is not preceded by a common path character.
91                if start == 0 or not (line[start - 1].isalnum() or
92                                      line[start - 1] in '.-_/'):
93                    raise ValueError(f'{dump_path} contains absolute path to '
94                                     f'$ANDROID_BUILD_TOP at line '
95                                     f'{line_number}:\n{line}')
96                start += len(AOSP_DIR)
97
98
99def copy_reference_dump(lib_path, reference_dump_dir):
100    reference_dump_path = os.path.join(
101        reference_dump_dir, os.path.basename(lib_path))
102    os.makedirs(os.path.dirname(reference_dump_path), exist_ok=True)
103    _validate_dump_content(lib_path)
104    shutil.copyfile(lib_path, reference_dump_path)
105    print('Created abi dump at', reference_dump_path)
106    return reference_dump_path
107
108
109def run_header_abi_dumper(input_path, output_path, cflags=tuple(),
110                          export_include_dirs=tuple(), flags=tuple()):
111    """Run header-abi-dumper to dump ABI from `input_path` and the output is
112    written to `output_path`."""
113    input_ext = os.path.splitext(input_path)[1]
114    cmd = ['header-abi-dumper', '-o', output_path, input_path]
115    for dir in export_include_dirs:
116        cmd += ['-I', dir]
117    cmd += flags
118    if '-output-format' not in flags:
119        cmd += ['-output-format', DEFAULT_FORMAT]
120    if input_ext == ".h":
121        cmd += DEFAULT_HEADER_FLAGS
122    cmd += ['--']
123    cmd += cflags
124    if input_ext in ('.cpp', '.cc', '.h'):
125        cmd += DEFAULT_CPPFLAGS
126    else:
127        cmd += DEFAULT_CFLAGS
128
129    for dir in BUILTIN_HEADERS_DIR:
130        cmd += ['-isystem', dir]
131    # The export include dirs imply local include dirs.
132    for dir in export_include_dirs:
133        cmd += ['-I', dir]
134    subprocess.check_call(cmd, cwd=AOSP_DIR)
135    _validate_dump_content(output_path)
136
137
138def run_header_abi_linker(inputs, output_path, version_script, api, arch,
139                          flags=tuple()):
140    """Link inputs, taking version_script into account"""
141    cmd = ['header-abi-linker', '-o', output_path, '-v', version_script,
142           '-api', api, '-arch', arch]
143    cmd += flags
144    if '-input-format' not in flags:
145        cmd += ['-input-format', DEFAULT_FORMAT]
146    if '-output-format' not in flags:
147        cmd += ['-output-format', DEFAULT_FORMAT]
148    cmd += inputs
149    subprocess.check_call(cmd, cwd=AOSP_DIR)
150    _validate_dump_content(output_path)
151
152
153def make_targets(product, variant, targets):
154    make_cmd = ['build/soong/soong_ui.bash', '--make-mode', '-j',
155                'TARGET_PRODUCT=' + product, 'TARGET_BUILD_VARIANT=' + variant]
156    make_cmd += targets
157    subprocess.check_call(make_cmd, cwd=AOSP_DIR)
158
159
160def make_tree(product, variant):
161    """Build all lsdump files."""
162    return make_targets(product, variant, ['findlsdumps'])
163
164
165def make_libraries(product, variant, vndk_version, targets, libs,
166                   exclude_tags):
167    """Build lsdump files for specific libs."""
168    lsdump_paths = read_lsdump_paths(product, variant, vndk_version, targets,
169                                     exclude_tags, build=True)
170    make_target_paths = []
171    for name in libs:
172        if not (name in lsdump_paths and lsdump_paths[name]):
173            raise KeyError('Cannot find lsdump for %s.' % name)
174        for tag_path_dict in lsdump_paths[name].values():
175            make_target_paths.extend(tag_path_dict.values())
176    make_targets(product, variant, make_target_paths)
177
178
179def get_lsdump_paths_file_path(product, variant):
180    """Get the path to lsdump_paths.txt."""
181    product_out = get_build_vars_for_product(
182        ['PRODUCT_OUT'], product, variant)[0]
183    return os.path.join(product_out, 'lsdump_paths.txt')
184
185
186def _get_module_variant_sort_key(suffix):
187    for variant in suffix.split('_'):
188        match = re.match(r'apex(\d+)$', variant)
189        if match:
190            return (int(match.group(1)), suffix)
191    return (-1, suffix)
192
193
194def _get_module_variant_dir_name(tag, vndk_version, arch_cpu_str):
195    """Return the module variant directory name.
196
197    For example, android_x86_shared, android_vendor.R_arm_armv7-a-neon_shared.
198    """
199    if tag in ('LLNDK', 'NDK', 'PLATFORM'):
200        return f'android_{arch_cpu_str}_shared'
201    if tag.startswith('VNDK') or tag == 'VENDOR':
202        return f'android_vendor.{vndk_version}_{arch_cpu_str}_shared'
203    if tag == 'PRODUCT':
204        return f'android_product.{vndk_version}_{arch_cpu_str}_shared'
205    raise ValueError(tag + ' is not a known tag.')
206
207
208def _read_lsdump_paths(lsdump_paths_file_path, vndk_version, targets,
209                       exclude_tags):
210    """Read lsdump paths from lsdump_paths.txt for each libname and variant.
211
212    This function returns a dictionary, {lib_name: {arch_cpu: {tag: path}}}.
213    For example,
214    {
215      "libc": {
216        "x86_x86_64": {
217          "NDK": "path/to/libc.so.lsdump"
218        }
219      }
220    }
221    """
222    lsdump_paths = collections.defaultdict(
223        lambda: collections.defaultdict(dict))
224    suffixes = collections.defaultdict(dict)
225
226    with open(lsdump_paths_file_path, 'r') as lsdump_paths_file:
227        for line in lsdump_paths_file:
228            tag, path = (x.strip() for x in line.split(':', 1))
229            if not path or tag in exclude_tags:
230                continue
231            dirname, filename = os.path.split(path)
232            if not filename.endswith(SOURCE_ABI_DUMP_EXT):
233                continue
234            libname = filename[:-len(SOURCE_ABI_DUMP_EXT)]
235            if not libname:
236                continue
237            variant = os.path.basename(dirname)
238            if not variant:
239                continue
240            for target in targets:
241                arch_cpu = target.get_arch_cpu_str()
242                prefix = _get_module_variant_dir_name(tag, vndk_version,
243                                                      arch_cpu)
244                if not variant.startswith(prefix):
245                    continue
246                new_suffix = variant[len(prefix):]
247                old_suffix = suffixes[libname].get(arch_cpu)
248                if (not old_suffix or
249                        _get_module_variant_sort_key(new_suffix) >
250                        _get_module_variant_sort_key(old_suffix)):
251                    lsdump_paths[libname][arch_cpu][tag] = path
252                    suffixes[libname][arch_cpu] = new_suffix
253    return lsdump_paths
254
255
256def read_lsdump_paths(product, variant, vndk_version, targets, exclude_tags,
257                      build):
258    """Build lsdump_paths.txt and read the paths."""
259    lsdump_paths_file_path = get_lsdump_paths_file_path(product, variant)
260    lsdump_paths_file_abspath = os.path.join(AOSP_DIR, lsdump_paths_file_path)
261    if build:
262        if os.path.lexists(lsdump_paths_file_abspath):
263            os.unlink(lsdump_paths_file_abspath)
264        make_targets(product, variant, [lsdump_paths_file_path])
265    return _read_lsdump_paths(lsdump_paths_file_abspath, vndk_version,
266                              targets, exclude_tags)
267
268
269def find_lib_lsdumps(lsdump_paths, libs, target):
270    """Find the lsdump corresponding to libs for the given target.
271
272    This function returns a list of (tag, absolute_path).
273    For example,
274    [
275      (
276        "NDK",
277        "/path/to/libc.so.lsdump"
278      )
279    ]
280    """
281    arch_cpu = target.get_arch_cpu_str()
282    result = []
283    if libs:
284        for lib_name in libs:
285            if not (lib_name in lsdump_paths and
286                    arch_cpu in lsdump_paths[lib_name]):
287                raise KeyError('Cannot find lsdump for %s, %s.' %
288                               (lib_name, arch_cpu))
289            result.extend(lsdump_paths[lib_name][arch_cpu].items())
290    else:
291        for arch_tag_path_dict in lsdump_paths.values():
292            result.extend(arch_tag_path_dict[arch_cpu].items())
293    return [(tag, os.path.join(AOSP_DIR, path)) for tag, path in result]
294
295
296def run_abi_diff(old_test_dump_path, new_test_dump_path, arch, lib_name,
297                 flags=tuple()):
298    abi_diff_cmd = ['header-abi-diff', '-new', new_test_dump_path, '-old',
299                    old_test_dump_path, '-arch', arch, '-lib', lib_name]
300    with tempfile.TemporaryDirectory() as tmp:
301        output_name = os.path.join(tmp, lib_name) + '.abidiff'
302        abi_diff_cmd += ['-o', output_name]
303        abi_diff_cmd += flags
304        if '-input-format-old' not in flags:
305            abi_diff_cmd += ['-input-format-old', DEFAULT_FORMAT]
306        if '-input-format-new' not in flags:
307            abi_diff_cmd += ['-input-format-new', DEFAULT_FORMAT]
308
309        result = subprocess.run(abi_diff_cmd)
310        output = ""
311        if os.path.isfile(output_name):
312            with open(output_name, 'r') as output_file:
313                output = output_file.read()
314    return result.returncode, output
315
316
317def get_build_vars_for_product(names, product=None, variant=None):
318    """ Get build system variable for the launched target."""
319
320    if product is None and 'ANDROID_PRODUCT_OUT' not in os.environ:
321        return None
322
323    env = os.environ.copy()
324    if product:
325        env['TARGET_PRODUCT'] = product
326    if variant:
327        env['TARGET_BUILD_VARIANT'] = variant
328    cmd = [
329        os.path.join('build', 'soong', 'soong_ui.bash'),
330        '--dumpvars-mode', '-vars', ' '.join(names),
331    ]
332
333    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
334                            stderr=subprocess.PIPE, cwd=AOSP_DIR, env=env)
335    out, err = proc.communicate()
336
337    if proc.returncode != 0:
338        print("error: %s" % err.decode('utf-8'), file=sys.stderr)
339        return None
340
341    build_vars = out.decode('utf-8').strip().splitlines()
342
343    build_vars_list = []
344    for build_var in build_vars:
345        value = build_var.partition('=')[2]
346        build_vars_list.append(value.replace('\'', ''))
347    return build_vars_list
348