blob: 31f2a77cd5deb7625be47b6399e19ec6390ab8d3 [file] [log] [blame]
#!/usr/bin/env python3
# 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."""
from __future__ import print_function
import argparse
import os
import re
import subprocess
import sys
import tempfile
import zipfile
from util import build_utils
'tools', 'linux',
def _RunReadelf(so_path, options, tool_prefix=''):
return subprocess.check_output([tool_prefix + 'readelf'] + options +
def _ParseLibBuildId(so_path, tool_prefix):
"""Returns the Build ID of the given native library."""
stdout = _RunReadelf(so_path, ['-n'], tool_prefix)
match ='Build ID: (\w+)', stdout)
return 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 ='Found \d+ static initializers in (\d+) files.', output)
return output.splitlines()[:-1], int(
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',
_VerifyLibBuildIdsMatch(tool_prefix, unzipped_so, so_with_symbols_path)
sis, _ = _GetStaticInitializers(so_with_symbols_path, tool_prefix)
for si in sis:
# Mostly copied from //infra/scripts/legacy/scripts/slave/chromium/
def _ReadInitArray(so_path, tool_prefix, expect_no_initializers):
stdout = _RunReadelf(so_path, ['-SW'], tool_prefix)
# Matches: .init_array INIT_ARRAY 000000000516add0 5169dd0 000010 00 WA 0 0 8
match ='\.init_array.*$', stdout, re.MULTILINE)
if expect_no_initializers:
if match:
raise Exception(
'Expected no initializers for %s, yet some were found' % so_path)
return 0
elif not match:
raise Exception('Did not find section: .init_array in {}:\n{}'.format(
so_path, stdout))
size_str = re.split(r'\W+',[5]
return int(size_str, 16)
def _CountStaticInitializers(so_path, tool_prefix, expect_no_initializers):
# 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 ='Class:.*$', stdout, re.MULTILINE).group(0)
elf_class = re.split(r'\W+', elf_class_line)[1]
if elf_class == 'ELF32':
word_size = 4
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, expect_no_initializers)
return init_array_size / word_size
def _AnalyzeStaticInitializers(apk_or_aab, tool_prefix, dump_sis, out_dir,
ignored_libs, no_initializers_libs):
# Static initializer counting mostly copies logic in
# infra/scripts/legacy/scripts/slave/chromium/
with zipfile.ZipFile(apk_or_aab) 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]
# Do not check partitioned libs. They have no ".init_array" section since
# all SIs are considered "roots" by the linker, and so end up in the base
# module.
files_to_check = [
f for f in files_to_check if not f.filename.endswith('')
si_count = 0
for f in files_to_check:
lib_basename = os.path.basename(f.filename)
expect_no_initializers = lib_basename in no_initializers_libs
with tempfile.NamedTemporaryFile(prefix=lib_basename) as temp:
si_count += _CountStaticInitializers(, tool_prefix,
if dump_sis:
# Print count and list of SIs reported by
# Doesn't work well on all archs (particularly arm), which is why
# the readelf method is used for tracking SI counts.
_PrintDumpSIsCount(f.filename,, 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_or_aab', help='Path to .apk or .aab file.')
args = parser.parse_args()
# TODO( add support for files included via loadable_modules.
ignored_libs = {
'', '',
# The chromium linker doesn't have static initializers, which makes the
# regular check throw. It should not have any.
no_initializers_libs = ['']
si_count = _AnalyzeStaticInitializers(args.apk_or_aab, 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')
print('Dumping static initializers via')
_AnalyzeStaticInitializers(args.apk_or_aab, args.tool_prefix, True, '.',
ignored_libs, no_initializers_libs)
print('If the above list is not useful, consider listing them with:')
print(' //tools/binary_size/')
print('For more information:')
if args.touch:
open(args.touch, 'w')
if __name__ == '__main__':