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