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