summaryrefslogtreecommitdiff
path: root/vndk/tools/header-checker/utils/utils.py
blob: 03e82554c58426a94933b14fc1e4dbd79f9745f0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#!/usr/bin/env python3

import collections
import os
import re
import shutil
import subprocess
import sys
import tempfile
import collections


SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))

try:
    AOSP_DIR = os.environ['ANDROID_BUILD_TOP']
except KeyError:
    print('error: ANDROID_BUILD_TOP environment variable is not set.',
          file=sys.stderr)
    sys.exit(1)

BUILTIN_HEADERS_DIR = (
    os.path.join(AOSP_DIR, 'bionic', 'libc', 'include'),
    os.path.join(AOSP_DIR, 'external', 'libcxx', 'include'),
    os.path.join(AOSP_DIR, 'prebuilts', 'clang-tools', 'linux-x86',
                 'clang-headers'),
)

SO_EXT = '.so'
SOURCE_ABI_DUMP_EXT_END = '.lsdump'
SOURCE_ABI_DUMP_EXT = SO_EXT + SOURCE_ABI_DUMP_EXT_END
VENDOR_SUFFIX = '.vendor'

DEFAULT_CPPFLAGS = ['-x', 'c++', '-std=c++11']
DEFAULT_CFLAGS = ['-std=gnu99']
DEFAULT_HEADER_FLAGS = ["-dump-function-declarations"]
DEFAULT_FORMAT = 'ProtobufTextFormat'

BuildTarget = collections.namedtuple(
    'BuildTarget', ['product', 'release', 'variant'])


class Arch(object):
    """A CPU architecture of a build target."""
    def __init__(self, is_2nd, build_target):
        extra = '_2ND' if is_2nd else ''
        build_vars_to_fetch = ['TARGET_ARCH',
                               'TARGET{}_ARCH'.format(extra),
                               'TARGET{}_ARCH_VARIANT'.format(extra),
                               'TARGET{}_CPU_VARIANT'.format(extra)]
        build_vars = get_build_vars(build_vars_to_fetch, build_target)
        self.primary_arch = build_vars[0]
        assert self.primary_arch != ''
        self.arch = build_vars[1]
        self.arch_variant = build_vars[2]
        self.cpu_variant = build_vars[3]

    def get_arch_str(self):
        """Return a string that represents the architecture and the primary
        architecture.
        """
        if not self.arch or self.arch == self.primary_arch:
            return self.primary_arch
        return self.arch + '_' + self.primary_arch

    def get_arch_cpu_str(self):
        """Return a string that represents the architecture, the architecture
        variant, and the CPU variant.

        If TARGET_ARCH == TARGET_ARCH_VARIANT, soong makes targetArchVariant
        empty. This is the case for aosp_x86_64.
        """
        if not self.arch_variant or self.arch_variant == self.arch:
            arch_variant = ''
        else:
            arch_variant = '_' + self.arch_variant

        if not self.cpu_variant or self.cpu_variant == 'generic':
            cpu_variant = ''
        else:
            cpu_variant = '_' + self.cpu_variant

        return self.arch + arch_variant + cpu_variant


def _validate_dump_content(dump_path):
    """Make sure that the dump contains relative source paths."""
    with open(dump_path, 'r') as f:
        for line_number, line in enumerate(f, 1):
            start = 0
            while True:
                start = line.find(AOSP_DIR, start)
                if start < 0:
                    break
                # The substring is not preceded by a common path character.
                if start == 0 or not (line[start - 1].isalnum() or
                                      line[start - 1] in '.-_/'):
                    raise ValueError(f'{dump_path} contains absolute path to '
                                     f'$ANDROID_BUILD_TOP at line '
                                     f'{line_number}:\n{line}')
                start += len(AOSP_DIR)


def copy_reference_dump(lib_path, reference_dump_dir):
    reference_dump_path = os.path.join(
        reference_dump_dir, os.path.basename(lib_path))
    os.makedirs(os.path.dirname(reference_dump_path), exist_ok=True)
    _validate_dump_content(lib_path)
    shutil.copyfile(lib_path, reference_dump_path)
    print('Created abi dump at', reference_dump_path)
    return reference_dump_path


def run_header_abi_dumper(input_path, output_path, cflags=tuple(),
                          export_include_dirs=tuple(), flags=tuple()):
    """Run header-abi-dumper to dump ABI from `input_path` and the output is
    written to `output_path`."""
    input_ext = os.path.splitext(input_path)[1]
    cmd = ['header-abi-dumper', '-o', output_path, input_path]
    for dir in export_include_dirs:
        cmd += ['-I', dir]
    cmd += flags
    if '-output-format' not in flags:
        cmd += ['-output-format', DEFAULT_FORMAT]
    if input_ext == ".h":
        cmd += DEFAULT_HEADER_FLAGS
    cmd += ['--']
    cmd += cflags
    if input_ext in ('.cpp', '.cc', '.h'):
        cmd += DEFAULT_CPPFLAGS
    else:
        cmd += DEFAULT_CFLAGS

    for dir in BUILTIN_HEADERS_DIR:
        cmd += ['-isystem', dir]
    # The export include dirs imply local include dirs.
    for dir in export_include_dirs:
        cmd += ['-I', dir]
    subprocess.check_call(cmd, cwd=AOSP_DIR)
    _validate_dump_content(output_path)


def run_header_abi_linker(inputs, output_path, version_script, api, arch_str,
                          flags=tuple()):
    """Link inputs, taking version_script into account"""
    cmd = ['header-abi-linker', '-o', output_path, '-v', version_script,
           '-api', api, '-arch', arch_str]
    cmd += flags
    if '-input-format' not in flags:
        cmd += ['-input-format', DEFAULT_FORMAT]
    if '-output-format' not in flags:
        cmd += ['-output-format', DEFAULT_FORMAT]
    cmd += inputs
    subprocess.check_call(cmd, cwd=AOSP_DIR)
    _validate_dump_content(output_path)


def make_targets(build_target, args):
    make_cmd = ['build/soong/soong_ui.bash', '--make-mode', '-j',
                'TARGET_PRODUCT=' + build_target.product,
                'TARGET_BUILD_VARIANT=' + build_target.variant]
    if build_target.release:
        make_cmd.append('TARGET_RELEASE=' + build_target.release)
    make_cmd += args
    subprocess.check_call(make_cmd, cwd=AOSP_DIR)


def make_tree(build_target):
    """Build all lsdump files."""
    return make_targets(build_target, ['findlsdumps'])


def make_libraries(build_target, arches, libs, exclude_tags):
    """Build lsdump files for specific libs."""
    lsdump_paths = read_lsdump_paths(build_target, arches, exclude_tags,
                                     build=True)
    make_target_paths = []
    for name in libs:
        if not (name in lsdump_paths and lsdump_paths[name]):
            raise KeyError('Cannot find lsdump for %s.' % name)
        for tag_path_dict in lsdump_paths[name].values():
            make_target_paths.extend(tag_path_dict.values())
    make_targets(build_target, make_target_paths)


def get_lsdump_paths_file_path(build_target):
    """Get the path to lsdump_paths.txt."""
    product_out = get_build_vars(['PRODUCT_OUT'], build_target)[0]
    return os.path.join(product_out, 'lsdump_paths.txt')


def _get_module_variant_sort_key(suffix):
    for variant in suffix.split('_'):
        match = re.match(r'apex(\d+)$', variant)
        if match:
            return (int(match.group(1)), suffix)
    return (-1, suffix)


def _get_module_variant_dir_name(tag, arch_cpu_str):
    """Return the module variant directory name.

    For example, android_x86_shared, android_vendor.R_arm_armv7-a-neon_shared.
    """
    if tag in ('LLNDK', 'NDK', 'PLATFORM'):
        return f'android_{arch_cpu_str}_shared'
    if tag == 'VENDOR':
        return f'android_vendor_{arch_cpu_str}_shared'
    if tag == 'PRODUCT':
        return f'android_product_{arch_cpu_str}_shared'
    raise ValueError(tag + ' is not a known tag.')


def _read_lsdump_paths(lsdump_paths_file_path, arches, exclude_tags):
    """Read lsdump paths from lsdump_paths.txt for each libname and variant.

    This function returns a dictionary, {lib_name: {arch_cpu: {tag: path}}}.
    For example,
    {
      "libc": {
        "x86_x86_64": {
          "NDK": "path/to/libc.so.lsdump"
        }
      }
    }
    """
    lsdump_paths = collections.defaultdict(
        lambda: collections.defaultdict(dict))
    suffixes = collections.defaultdict(
        lambda: collections.defaultdict(dict))

    with open(lsdump_paths_file_path, 'r') as lsdump_paths_file:
        for line in lsdump_paths_file:
            tag, path = (x.strip() for x in line.split(':', 1))
            if not path or tag in exclude_tags:
                continue
            dir_path, filename = os.path.split(path)
            if not filename.endswith(SOURCE_ABI_DUMP_EXT):
                continue
            libname = filename[:-len(SOURCE_ABI_DUMP_EXT)]
            if not libname:
                continue
            # dir_path may contain soong config hash.
            # For example, the following dir_paths are valid.
            # android_x86_x86_64_shared/012abc/libc.so.lsdump
            # android_x86_x86_64_shared/libc.so.lsdump
            dirnames = []
            dir_path, dirname = os.path.split(dir_path)
            dirnames.append(dirname)
            dirname = os.path.basename(dir_path)
            dirnames.append(dirname)
            for arch in arches:
                arch_cpu = arch.get_arch_cpu_str()
                prefix = _get_module_variant_dir_name(tag, arch_cpu)
                variant = next((d for d in dirnames if d.startswith(prefix)),
                               None)
                if not variant:
                    continue
                new_suffix = variant[len(prefix):]
                old_suffix = suffixes[libname][arch_cpu].get(tag)
                if (not old_suffix or
                        _get_module_variant_sort_key(new_suffix) >
                        _get_module_variant_sort_key(old_suffix)):
                    lsdump_paths[libname][arch_cpu][tag] = path
                    suffixes[libname][arch_cpu][tag] = new_suffix
    return lsdump_paths


def read_lsdump_paths(build_target, arches, exclude_tags, build):
    """Build lsdump_paths.txt and read the paths."""
    lsdump_paths_file_path = get_lsdump_paths_file_path(build_target)
    lsdump_paths_file_abspath = os.path.join(AOSP_DIR, lsdump_paths_file_path)
    if build:
        if os.path.lexists(lsdump_paths_file_abspath):
            os.unlink(lsdump_paths_file_abspath)
        make_targets(build_target, [lsdump_paths_file_path])
    return _read_lsdump_paths(lsdump_paths_file_abspath, arches, exclude_tags)


def find_lib_lsdumps(lsdump_paths, libs, arch):
    """Find the lsdump corresponding to libs for the given architecture.

    This function returns a list of (tag, absolute_path).
    For example,
    [
      (
        "NDK",
        "/path/to/libc.so.lsdump"
      )
    ]
    """
    arch_cpu = arch.get_arch_cpu_str()
    result = []
    if libs:
        for lib_name in libs:
            if not (lib_name in lsdump_paths and
                    arch_cpu in lsdump_paths[lib_name]):
                raise KeyError('Cannot find lsdump for %s, %s.' %
                               (lib_name, arch_cpu))
            result.extend(lsdump_paths[lib_name][arch_cpu].items())
    else:
        for arch_tag_path_dict in lsdump_paths.values():
            result.extend(arch_tag_path_dict[arch_cpu].items())
    return [(tag, os.path.join(AOSP_DIR, path)) for tag, path in result]


def run_abi_diff(old_dump_path, new_dump_path, output_path, arch_str, lib_name,
                 flags):
    abi_diff_cmd = ['header-abi-diff', '-new', new_dump_path, '-old',
                    old_dump_path, '-arch', arch_str, '-lib', lib_name,
                    '-o', output_path]
    abi_diff_cmd += flags
    if '-input-format-old' not in flags:
        abi_diff_cmd += ['-input-format-old', DEFAULT_FORMAT]
    if '-input-format-new' not in flags:
        abi_diff_cmd += ['-input-format-new', DEFAULT_FORMAT]
    return subprocess.run(abi_diff_cmd).returncode


def run_and_read_abi_diff(old_dump_path, new_dump_path, arch_str, lib_name,
                          flags=tuple()):
    with tempfile.TemporaryDirectory() as tmp:
        output_name = os.path.join(tmp, lib_name) + '.abidiff'
        result = run_abi_diff(old_dump_path, new_dump_path, output_name,
                              arch_str, lib_name, flags)
        with open(output_name, 'r') as output_file:
            return result, output_file.read()


def get_build_vars(names, build_target):
    """ Get build system variable for the launched target."""
    env = os.environ.copy()
    env['TARGET_PRODUCT'] = build_target.product
    env['TARGET_BUILD_VARIANT'] = build_target.variant
    if build_target.release:
        env['TARGET_RELEASE'] = build_target.release
    cmd = [
        os.path.join('build', 'soong', 'soong_ui.bash'),
        '--dumpvars-mode', '-vars', ' '.join(names),
    ]

    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE, cwd=AOSP_DIR, env=env)
    out, err = proc.communicate()

    if proc.returncode != 0:
        print("error: %s" % err.decode('utf-8'), file=sys.stderr)
        return None

    build_vars = out.decode('utf-8').strip().splitlines()

    build_vars_list = []
    for build_var in build_vars:
        value = build_var.partition('=')[2]
        build_vars_list.append(value.replace('\'', ''))
    return build_vars_list