blob: 019baface15160b85c5ee766d84beb5a6df31b16 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Checks the number of static initializers in an APK's library."""
import argparse
import os
import re
import subprocess
import sys
import tempfile
import zipfile
from util import build_utils
_DUMP_STATIC_INITIALIZERS_PATH = os.path.join(build_utils.DIR_SOURCE_ROOT,
'tools', 'linux',
'dump-static-initializers.py')
def _RunReadelf(so_path, options, tool_prefix=''):
return subprocess.check_output([tool_prefix + 'readelf'] + options +
[so_path])
def _ParseLibBuildId(so_path, tool_prefix):
"""Returns the Build ID of the given native library."""
stdout = _RunReadelf(so_path, ['-n'], tool_prefix)
match = re.search(r'Build ID: (\w+)', stdout)
return match.group(1) if match else None
def _VerifyLibBuildIdsMatch(tool_prefix, *so_files):
if len(set(_ParseLibBuildId(f, tool_prefix) for f in so_files)) > 1:
raise Exception('Found differing build ids in output directory and apk. '
'Your output directory is likely stale.')
def _GetStaticInitializers(so_path, tool_prefix):
output = subprocess.check_output(
[_DUMP_STATIC_INITIALIZERS_PATH, '-d', so_path, '-t', tool_prefix])
summary = re.search(r'Found \d+ static initializers in (\d+) files.', output)
return output.splitlines()[:-1], int(summary.group(1))
def _PrintDumpSIsCount(apk_so_name, unzipped_so, out_dir, tool_prefix):
lib_name = os.path.basename(apk_so_name).replace('crazy.', '')
so_with_symbols_path = os.path.join(out_dir, 'lib.unstripped', lib_name)
if not os.path.exists(so_with_symbols_path):
raise Exception('Unstripped .so not found. Looked here: %s',
so_with_symbols_path)
_VerifyLibBuildIdsMatch(tool_prefix, unzipped_so, so_with_symbols_path)
sis, _ = _GetStaticInitializers(so_with_symbols_path, tool_prefix)
for si in sis:
print si
# Mostly copied from //infra/scripts/legacy/scripts/slave/chromium/sizes.py.
def _ReadInitArray(so_path, tool_prefix):
stdout = _RunReadelf(so_path, ['-SW'], tool_prefix)
# Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8
match = re.search(r'\.init_array.*$', stdout, re.MULTILINE)
if not match:
raise Exception('Did not find section: .init_array in:\n' + stdout)
size_str = re.split(r'\W+', match.group(0))[5]
return int(size_str, 16)
def _CountStaticInitializers(so_path, tool_prefix):
# Find the number of files with at least one static initializer.
# First determine if we're 32 or 64 bit
stdout = _RunReadelf(so_path, ['-h'], tool_prefix)
elf_class_line = re.search('Class:.*$', stdout, re.MULTILINE).group(0)
elf_class = re.split(r'\W+', elf_class_line)[1]
if elf_class == 'ELF32':
word_size = 4
else:
word_size = 8
# Then find the number of files with global static initializers.
# NOTE: this is very implementation-specific and makes assumptions
# about how compiler and linker implement global static initializers.
init_array_size = _ReadInitArray(so_path, tool_prefix)
return init_array_size / word_size
def _AnalyzeStaticInitializers(apk_filename, tool_prefix, dump_sis, out_dir,
ignored_libs):
# Static initializer counting mostly copies logic in
# infra/scripts/legacy/scripts/slave/chromium/sizes.py.
with zipfile.ZipFile(apk_filename) as z:
so_files = [
f for f in z.infolist() if f.filename.endswith('.so')
and f.file_size > 0 and os.path.basename(f.filename) not in ignored_libs
]
# Skip checking static initializers for secondary abi libs. They will be
# checked by 32-bit bots. This avoids the complexity of finding 32 bit .so
# files in the output directory in 64 bit builds.
has_64 = any('64' in f.filename for f in so_files)
files_to_check = [f for f in so_files if not has_64 or '64' in f.filename]
si_count = 0
for f in files_to_check:
with tempfile.NamedTemporaryFile() as temp:
temp.write(z.read(f))
temp.flush()
si_count += _CountStaticInitializers(temp.name, tool_prefix)
if dump_sis:
# Print count and list of SIs reported by dump-static-initializers.py.
# Doesn't work well on all archs (particularly arm), which is why
# the readelf method is used for tracking SI counts.
_PrintDumpSIsCount(f.filename, temp.name, out_dir, tool_prefix)
return si_count
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--touch', help='File to touch upon success')
parser.add_argument('--tool-prefix', required=True,
help='Prefix for nm and friends')
parser.add_argument('--expected-count', required=True, type=int,
help='Fail if number of static initializers is not '
'equal to this value.')
parser.add_argument('apk', help='APK file path.')
args = parser.parse_args()
#TODO(crbug.com/838414): add support for files included via loadable_modules.
ignored_libs = ['libarcore_sdk_c.so']
si_count = _AnalyzeStaticInitializers(args.apk, args.tool_prefix, False, '.',
ignored_libs)
if si_count != args.expected_count:
print 'Expected {} static initializers, but found {}.'.format(
args.expected_count, si_count)
if args.expected_count > si_count:
print 'You have removed one or more static initializers. Thanks!'
print 'To fix the build, update the expectation in:'
print ' //chrome/android/static_initializers.gni'
else:
print 'Dumping static initializers via dump-static-initializers.py:'
sys.stdout.flush()
_AnalyzeStaticInitializers(args.apk, args.tool_prefix, True, '.',
ignored_libs)
print
print 'If the above list is not useful, consider listing them with:'
print ' //tools/binary_size/diagnose_bloat.py'
print
print 'For more information:'
print (' https://chromium.googlesource.com/chromium/src/+/master/docs/'
'static_initializers.md')
sys.exit(1)
if args.touch:
open(args.touch, 'w')
if __name__ == '__main__':
main()