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