blob: 7c82b1208e0761facc2ddf982883011413551936 [file] [log] [blame]
# Copyright 2014 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.
""" Merges a 32-bit APK into a 32/64-bit APK.
This script is used to make the 32-bit parts of a pure 32-bit APK identical to
those of an APK built on a 64-bit configuration (whether adding 32-bit parts to
a pure 64-bit build, or replacing 32-bit parts in a multi-architecture build to
ensurce consistency with pure 32-bit builds).
In an ideal world, the libraries and assets in a pure 32-bit APK would be
identical to the 32-bit equivalents in a 64-bit-generated APK (with secondary
ABI). However, this isn't reality. For example, subtle differences due to paths
yield different native libraries (a benign difference). Accidental differences
in build configuration yield legitimate differences (impacting functionality and
binary size). This script overwrites parts of the 64-bit APK with pieces from
the 32-bit APK, so that the two versions are identical from a 32-bit
Longer term, the 64-bit build configuration should be updated to generate pure
32-bit APKs. While slightly counter-intuitive, this would ensure that 32-bit
pieces are identical across the different versions without having to merge
To use this script, you need to
1. Build 32-bit APK as usual.
2. Build 64-bit APK.
3. Use this script to merge the 2 APKs.
import argparse
import collections
import filecmp
import logging
import os
import pprint
import shutil
import sys
import tempfile
import zipfile
SRC_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
SRC_DIR = os.path.abspath(SRC_DIR)
BUILD_ANDROID_DIR = os.path.join(SRC_DIR, 'build', 'android')
import finalize_apk # pylint: disable=wrong-import-position
from util import build_utils # pylint: disable=wrong-import-position
from pylib import constants # pylint: disable=wrong-import-position
SRC_DIR, 'third_party', 'android_sdk', 'public', 'build-tools',
constants.ANDROID_SDK_BUILD_TOOLS_VERSION, 'zipalign')
class ApkMergeFailure(Exception):
def UnpackApk(file_name, dst, ignore_paths=()):
zippy = zipfile.ZipFile(file_name)
files_to_extract = [f for f in zippy.namelist() if f not in ignore_paths]
zippy.extractall(dst, files_to_extract)
def GetNonDirFiles(top, base_dir):
""" Return a list containing all (non-directory) files in tree with top as
Each file is represented by the relative path from base_dir to that file.
If top is a file (not a directory) then a list containing only top is
if os.path.isdir(top):
ret = []
for dirpath, _, filenames in os.walk(top):
for filename in filenames:
os.path.relpath(os.path.join(dirpath, filename), base_dir))
return ret
return [os.path.relpath(top, base_dir)]
def GetDiffFiles(dcmp, base_dir):
""" Return the list of files contained only in the right directory of dcmp.
The files returned are represented by relative paths from base_dir.
copy_files = []
for file_name in dcmp.right_only:
GetNonDirFiles(os.path.join(dcmp.right, file_name), base_dir))
# we cannot merge APKs with files with similar names but different contents
if len(dcmp.diff_files) > 0:
raise ApkMergeFailure('found differing files: %s in %s and %s' %
(dcmp.diff_files, dcmp.left, dcmp.right))
if len(dcmp.funny_files) > 0:
raise ApkMergeFailure('found uncomparable files: %s in %s and %s' %
(dcmp.funny_files, dcmp.left, dcmp.right))
for sub_dcmp in dcmp.subdirs.itervalues():
copy_files.extend(GetDiffFiles(sub_dcmp, base_dir))
return copy_files
def CheckFilesExpected(actual_files, expected_files):
""" Check that the lists of actual and expected files are the same. """
actual_file_names = collections.defaultdict(int)
for f in actual_files:
actual_file_names[f] += 1
actual_file_set = set(actual_file_names.iterkeys())
expected_file_set = set(expected_files)
unexpected_file_set = actual_file_set.difference(expected_file_set)
missing_file_set = expected_file_set.difference(actual_file_set)
duplicate_file_set = set(
f for f, n in actual_file_names.iteritems() if n > 1)
errors = []
if unexpected_file_set:
' unexpected files: %s' % pprint.pformat(unexpected_file_set))
if missing_file_set:
errors.append(' missing files: %s' % pprint.pformat(missing_file_set))
if duplicate_file_set:
errors.append(' duplicate files: %s' % pprint.pformat(duplicate_file_set))
if errors:
raise ApkMergeFailure(
"Files don't match expectations:\n%s" % '\n'.join(errors))
def AddDiffFiles(diff_files, tmp_dir_32, out_zip, uncompress_shared_libraries):
""" Insert files only present in 32-bit APK into 64-bit APK (tmp_apk). """
for diff_file in diff_files:
compress = not uncompress_shared_libraries and diff_file.endswith('.so')
os.path.join(tmp_dir_32, diff_file),
def MergeApk(args, tmp_apk, tmp_dir_32, tmp_dir_64):
# expected_files is the set of 32-bit related files that we expect to differ
# between a 32- and 64-bit build. Hence, they will be skipped when seeding the
# generated APK with the original 64-bit version, and explicitly copied in
# from the 32-bit version.
expected_files = []
assets_path = 'base/assets' if args.bundle else 'assets'
expected_files.append('%s/snapshot_blob_32.bin' % assets_path)
if args.has_unwind_cfi:
expected_files.append('%s/unwind_cfi_32' % assets_path)
# All native libraries are assumed to differ, and will be merged.
with zipfile.ZipFile(args.apk_32bit) as z:
expected_files.extend([p for p in z.namelist() if p.endswith('.so')])
UnpackApk(args.apk_32bit, tmp_dir_32)
UnpackApk(args.apk_64bit, tmp_dir_64, expected_files)
# These are files that we know will be different, and we will hence ignore in
# the file comparison.
ignores = ['META-INF', 'AndroidManifest.xml']
if args.ignore_classes_dex:
ignores += ['classes.dex', 'classes2.dex', 'classes3.dex']
if args.debug:
# see
ignores += ['webview_licenses.notice']
if args.bundle:
# if merging a bundle we must ignore the bundle specific
# proto files as they will always be different.
ignores += ['BundleConfig.pb', 'native.pb']
dcmp = filecmp.dircmp(
diff_files = GetDiffFiles(dcmp, tmp_dir_32)
# Check that diff_files match exactly those files we want to insert into
# the 64-bit APK.
CheckFilesExpected(diff_files, expected_files)
with zipfile.ZipFile(tmp_apk, 'w') as out_zip:
exclude_patterns = ['META-INF/*'] + expected_files
# Build the initial merged APK from the 64-bit APK, excluding all files we
# will pull from the 32-bit APK.
path_transform = (
lambda p: None if build_utils.MatchesGlob(p, exclude_patterns) else p)
out_zip, [args.apk_64bit], path_transform=path_transform)
# Add the files from the 32-bit APK.
AddDiffFiles(diff_files, tmp_dir_32, out_zip,
def main():
parser = argparse.ArgumentParser(
description='Merge a 32-bit APK into a 64-bit APK')
# Using type=os.path.abspath converts file paths to absolute paths so that
# we can change working directory without affecting these paths
parser.add_argument('--apk_32bit', required=True, type=os.path.abspath)
parser.add_argument('--apk_64bit', required=True, type=os.path.abspath)
parser.add_argument('--out_apk', required=True, type=os.path.abspath)
parser.add_argument('--zipalign_path', type=os.path.abspath)
parser.add_argument('--keystore_path', required=True, type=os.path.abspath)
parser.add_argument('--key_name', required=True)
parser.add_argument('--key_password', required=True)
parser.add_argument('--uncompress-shared-libraries', action='store_true')
parser.add_argument('--bundle', action='store_true')
parser.add_argument('--debug', action='store_true')
# This option shall only used in debug build, see
parser.add_argument('--ignore-classes-dex', action='store_true')
parser.add_argument('--has-unwind-cfi', action='store_true',
help='Specifies if the 32-bit apk has unwind_cfi file')
args = parser.parse_args()
if (args.zipalign_path is not None and
not os.path.isfile(args.zipalign_path)):
# If given an invalid path, fall back to try the default.
logging.warning('zipalign path not found: %s', args.zipalign_path)
logging.warning('falling back to: %s', DEFAULT_ZIPALIGN_PATH)
args.zipalign_path = None
if args.zipalign_path is None:
# When no path given, try the default.
if not os.path.isfile(DEFAULT_ZIPALIGN_PATH):
return 'ERROR: zipalign path not found: %s' % DEFAULT_ZIPALIGN_PATH
args.zipalign_path = DEFAULT_ZIPALIGN_PATH
tmp_dir = tempfile.mkdtemp()
tmp_dir_64 = os.path.join(tmp_dir, '64_bit')
tmp_dir_32 = os.path.join(tmp_dir, '32_bit')
tmp_apk = os.path.join(tmp_dir, 'tmp.apk')
new_apk = args.out_apk
MergeApk(args, tmp_apk, tmp_dir_32, tmp_dir_64)
apksigner_jar = os.path.join(
os.path.dirname(args.zipalign_path), 'lib', 'apksigner.jar')
# Official APKs are re-signed anyways, so it is not important to figure out
# the correct min_sdk_version. Use 21 since that's the lowest supported
# webview version.
min_sdk_version = 21
finalize_apk.FinalizeApk(apksigner_jar, args.zipalign_path, tmp_apk,
new_apk, args.keystore_path, args.key_password,
args.key_name, min_sdk_version)
return 0
if __name__ == '__main__':