Home | History | Annotate | Download | only in utils
      1 #!/usr/bin/env python3
      2 
      3 import gzip
      4 import os
      5 import subprocess
      6 import sys
      7 import tempfile
      8 import collections
      9 
     10 
     11 SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
     12 
     13 try:
     14     AOSP_DIR = os.environ['ANDROID_BUILD_TOP']
     15 except KeyError:
     16     print('error: ANDROID_BUILD_TOP environment variable is not set.',
     17           file=sys.stderr)
     18     sys.exit(1)
     19 
     20 BUILTIN_HEADERS_DIR = (
     21     os.path.join(AOSP_DIR, 'bionic', 'libc', 'include'),
     22     os.path.join(AOSP_DIR, 'external', 'libcxx', 'include'),
     23     os.path.join(AOSP_DIR, 'prebuilts', 'clang-tools', 'linux-x86',
     24                  'clang-headers'),
     25 )
     26 
     27 EXPORTED_HEADERS_DIR = (
     28     os.path.join(AOSP_DIR, 'development', 'vndk', 'tools', 'header-checker',
     29                  'tests'),
     30 )
     31 
     32 SO_EXT = '.so'
     33 SOURCE_ABI_DUMP_EXT_END = '.lsdump'
     34 SOURCE_ABI_DUMP_EXT = SO_EXT + SOURCE_ABI_DUMP_EXT_END
     35 COMPRESSED_SOURCE_ABI_DUMP_EXT = SOURCE_ABI_DUMP_EXT + '.gz'
     36 VENDOR_SUFFIX = '.vendor'
     37 
     38 DEFAULT_CPPFLAGS = ['-x', 'c++', '-std=c++11']
     39 DEFAULT_CFLAGS = ['-std=gnu99']
     40 DEFAULT_HEADER_FLAGS = ["-dump-function-declarations"]
     41 DEFAULT_FORMAT = 'ProtobufTextFormat'
     42 
     43 
     44 def get_reference_dump_dir(reference_dump_dir_stem,
     45                            reference_dump_dir_insertion, lib_arch):
     46     reference_dump_dir = os.path.join(reference_dump_dir_stem, lib_arch)
     47     reference_dump_dir = os.path.join(reference_dump_dir,
     48                                       reference_dump_dir_insertion)
     49     return reference_dump_dir
     50 
     51 
     52 def copy_reference_dumps(lib_paths, reference_dir_stem,
     53                          reference_dump_dir_insertion, lib_arch, compress):
     54     reference_dump_dir = get_reference_dump_dir(reference_dir_stem,
     55                                                 reference_dump_dir_insertion,
     56                                                 lib_arch)
     57     num_created = 0
     58     for lib_path in lib_paths:
     59         copy_reference_dump(lib_path, reference_dump_dir, compress)
     60         num_created += 1
     61     return num_created
     62 
     63 
     64 def copy_reference_dump(lib_path, reference_dump_dir, compress):
     65     reference_dump_path = os.path.join(
     66         reference_dump_dir, os.path.basename(lib_path))
     67     if compress:
     68         reference_dump_path += '.gz'
     69     os.makedirs(os.path.dirname(reference_dump_path), exist_ok=True)
     70     output_content = read_output_content(lib_path, AOSP_DIR)
     71     if compress:
     72         with gzip.open(reference_dump_path, 'wb') as f:
     73             f.write(bytes(output_content, 'utf-8'))
     74     else:
     75         with open(reference_dump_path, 'wb') as f:
     76             f.write(bytes(output_content, 'utf-8'))
     77     print('Created abi dump at', reference_dump_path)
     78     return reference_dump_path
     79 
     80 
     81 def read_output_content(output_path, replace_str):
     82     with open(output_path, 'r') as f:
     83         return f.read().replace(replace_str, '')
     84 
     85 
     86 def run_header_abi_dumper(input_path, cflags=tuple(),
     87                           export_include_dirs=EXPORTED_HEADERS_DIR,
     88                           flags=tuple()):
     89     """Run header-abi-dumper to dump ABI from `input_path` and return the
     90     output."""
     91     with tempfile.TemporaryDirectory() as tmp:
     92         output_path = os.path.join(tmp, os.path.basename(input_path)) + '.dump'
     93         run_header_abi_dumper_on_file(input_path, output_path,
     94                                       export_include_dirs, cflags, flags)
     95         return read_output_content(output_path, AOSP_DIR)
     96 
     97 
     98 def run_header_abi_dumper_on_file(input_path, output_path,
     99                                   export_include_dirs=tuple(), cflags=tuple(),
    100                                   flags=tuple()):
    101     """Run header-abi-dumper to dump ABI from `input_path` and the output is
    102     written to `output_path`."""
    103     input_ext = os.path.splitext(input_path)[1]
    104     cmd = ['header-abi-dumper', '-o', output_path, input_path]
    105     for dir in export_include_dirs:
    106         cmd += ['-I', dir]
    107     cmd += flags
    108     if '-output-format' not in flags:
    109         cmd += ['-output-format', DEFAULT_FORMAT]
    110     if input_ext == ".h":
    111         cmd += DEFAULT_HEADER_FLAGS
    112     cmd += ['--']
    113     cmd += cflags
    114     if input_ext in ('.cpp', '.cc', '.h'):
    115         cmd += DEFAULT_CPPFLAGS
    116     else:
    117         cmd += DEFAULT_CFLAGS
    118 
    119     for dir in BUILTIN_HEADERS_DIR:
    120         cmd += ['-isystem', dir]
    121     # The export include dirs imply local include dirs.
    122     for dir in export_include_dirs:
    123         cmd += ['-I', dir]
    124     subprocess.check_call(cmd)
    125 
    126 
    127 def run_header_abi_linker(output_path, inputs, version_script, api, arch,
    128                           flags=tuple()):
    129     """Link inputs, taking version_script into account"""
    130     cmd = ['header-abi-linker', '-o', output_path, '-v', version_script,
    131            '-api', api, '-arch', arch]
    132     cmd += flags
    133     if '-input-format' not in flags:
    134         cmd += ['-input-format', DEFAULT_FORMAT]
    135     if '-output-format' not in flags:
    136         cmd += ['-output-format', DEFAULT_FORMAT]
    137     cmd += inputs
    138     subprocess.check_call(cmd)
    139     return read_output_content(output_path, AOSP_DIR)
    140 
    141 
    142 def make_targets(product, variant, targets):
    143     make_cmd = ['build/soong/soong_ui.bash', '--make-mode', '-j',
    144                 'TARGET_PRODUCT=' + product, 'TARGET_BUILD_VARIANT=' + variant]
    145     make_cmd += targets
    146     subprocess.check_call(make_cmd, cwd=AOSP_DIR)
    147 
    148 
    149 def make_tree(product, variant):
    150     """Build all lsdump files."""
    151     return make_targets(product, variant, ['findlsdumps'])
    152 
    153 
    154 def make_libraries(product, variant, targets, libs, llndk_mode):
    155     """Build lsdump files for specific libs."""
    156     lsdump_paths = read_lsdump_paths(product, variant, targets, build=True)
    157     targets = []
    158     for name in libs:
    159         targets.extend(lsdump_paths[name].values())
    160     make_targets(product, variant, targets)
    161 
    162 
    163 def get_lsdump_paths_file_path(product, variant):
    164     """Get the path to lsdump_paths.txt."""
    165     product_out = get_build_vars_for_product(
    166         ['PRODUCT_OUT'], product, variant)[0]
    167     return os.path.join(product_out, 'lsdump_paths.txt')
    168 
    169 
    170 def _is_sanitizer_variation(variation):
    171     """Check whether the variation is introduced by a sanitizer."""
    172     return variation in {'asan', 'hwasan', 'tsan', 'intOverflow', 'cfi', 'scs'}
    173 
    174 
    175 def _are_sanitizer_variations(variations):
    176     """Check whether these variations are introduced by sanitizers."""
    177     if isinstance(variations, str):
    178         variations = [v for v in variations.split('_') if v]
    179     return all(_is_sanitizer_variation(v) for v in variations)
    180 
    181 
    182 def _read_lsdump_paths(lsdump_paths_file_path, targets):
    183     """Read lsdump path from lsdump_paths.txt for each libname and variant."""
    184     lsdump_paths = collections.defaultdict(dict)
    185     suffixes = collections.defaultdict(dict)
    186 
    187     prefixes = []
    188     prefixes.extend(get_module_variant_dir_name(
    189         target.arch, target.arch_variant, target.cpu_variant, '_core_shared')
    190         for target in targets)
    191     prefixes.extend(get_module_variant_dir_name(
    192         target.arch, target.arch_variant, target.cpu_variant, '_vendor_shared')
    193         for target in targets)
    194 
    195     with open(lsdump_paths_file_path, 'r') as lsdump_paths_file:
    196         for line in lsdump_paths_file:
    197             path = line.strip()
    198             if not path:
    199                 continue
    200             dirname, filename = os.path.split(path)
    201             if not filename.endswith(SOURCE_ABI_DUMP_EXT):
    202                 continue
    203             libname = filename[:-len(SOURCE_ABI_DUMP_EXT)]
    204             if not libname:
    205                 continue
    206             variant = os.path.basename(dirname)
    207             if not variant:
    208                 continue
    209             for prefix in prefixes:
    210                 if not variant.startswith(prefix):
    211                     continue
    212                 new_suffix = variant[len(prefix):]
    213                 if not _are_sanitizer_variations(new_suffix):
    214                     continue
    215                 old_suffix = suffixes[libname].get(prefix)
    216                 if not old_suffix or new_suffix > old_suffix:
    217                     lsdump_paths[libname][prefix] = path
    218                     suffixes[libname][prefix] = new_suffix
    219     return lsdump_paths
    220 
    221 
    222 def read_lsdump_paths(product, variant, targets, build=True):
    223     """Build lsdump_paths.txt and read the paths."""
    224     lsdump_paths_file_path = get_lsdump_paths_file_path(product, variant)
    225     if build:
    226         make_targets(product, variant, [lsdump_paths_file_path])
    227     lsdump_paths_file_abspath = os.path.join(AOSP_DIR, lsdump_paths_file_path)
    228     return _read_lsdump_paths(lsdump_paths_file_abspath, targets)
    229 
    230 
    231 def get_module_variant_dir_name(arch, arch_variant, cpu_variant,
    232                                 variant_suffix):
    233     """Create module variant directory name from the target architecture, the
    234     target architecture variant, the target CPU variant, and a variant suffix
    235     (e.g. `_core_shared`, `_vendor_shared`, etc)."""
    236 
    237     if not arch_variant or arch_variant == arch:
    238         arch_variant = ''
    239     else:
    240         arch_variant = '_' + arch_variant
    241 
    242     if not cpu_variant or cpu_variant == 'generic':
    243         cpu_variant = ''
    244     else:
    245         cpu_variant = '_' + cpu_variant
    246 
    247     return 'android_' + arch + arch_variant + cpu_variant + variant_suffix
    248 
    249 
    250 def find_lib_lsdumps(module_variant_dir_name, lsdump_paths, libs):
    251     """Find the lsdump corresponding to lib_name for the given module variant
    252     if it exists."""
    253     result = []
    254     for lib_name, variations in lsdump_paths.items():
    255         if libs and lib_name not in libs:
    256             continue
    257         for variation, path in variations.items():
    258             if variation.startswith(module_variant_dir_name):
    259                 result.append(os.path.join(AOSP_DIR, path.strip()))
    260     return result
    261 
    262 
    263 def run_abi_diff(old_test_dump_path, new_test_dump_path, arch, lib_name,
    264                  flags=tuple()):
    265     abi_diff_cmd = ['header-abi-diff', '-new', new_test_dump_path, '-old',
    266                     old_test_dump_path, '-arch', arch, '-lib', lib_name]
    267     with tempfile.TemporaryDirectory() as tmp:
    268         output_name = os.path.join(tmp, lib_name) + '.abidiff'
    269         abi_diff_cmd += ['-o', output_name]
    270         abi_diff_cmd += flags
    271         if '-input-format-old' not in flags:
    272             abi_diff_cmd += ['-input-format-old', DEFAULT_FORMAT]
    273         if '-input-format-new' not in flags:
    274             abi_diff_cmd += ['-input-format-new', DEFAULT_FORMAT]
    275         try:
    276             subprocess.check_call(abi_diff_cmd)
    277         except subprocess.CalledProcessError as err:
    278             return err.returncode
    279 
    280     return 0
    281 
    282 
    283 def get_build_vars_for_product(names, product=None, variant=None):
    284     """ Get build system variable for the launched target."""
    285 
    286     if product is None and 'ANDROID_PRODUCT_OUT' not in os.environ:
    287         return None
    288 
    289     env = os.environ.copy()
    290     if product:
    291         env['TARGET_PRODUCT'] = product
    292     if variant:
    293         env['TARGET_BUILD_VARIANT'] = variant
    294     cmd = [
    295         os.path.join('build', 'soong', 'soong_ui.bash'),
    296         '--dumpvars-mode', '-vars', ' '.join(names),
    297     ]
    298 
    299     proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
    300                             stderr=subprocess.PIPE, cwd=AOSP_DIR, env=env)
    301     out, err = proc.communicate()
    302 
    303     if proc.returncode != 0:
    304         print("error: %s" % err.decode('utf-8'), file=sys.stderr)
    305         return None
    306 
    307     build_vars = out.decode('utf-8').strip().splitlines()
    308 
    309     build_vars_list = []
    310     for build_var in build_vars:
    311         value = build_var.partition('=')[2]
    312         build_vars_list.append(value.replace('\'', ''))
    313     return build_vars_list
    314