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