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