| #!/usr/bin/python |
| # 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 64-bit and a 32-bit APK into a single APK |
| |
| """ |
| |
| import os |
| import sys |
| import shutil |
| import zipfile |
| import filecmp |
| import tempfile |
| import argparse |
| import subprocess |
| |
| SRC_DIR = os.path.join(os.path.dirname(__file__), '..', '..') |
| SRC_DIR = os.path.abspath(SRC_DIR) |
| BUILD_ANDROID_GYP_DIR = os.path.join(SRC_DIR, 'build/android/gyp') |
| sys.path.append(BUILD_ANDROID_GYP_DIR) |
| |
| import finalize_apk |
| from util import build_utils |
| |
| class ApkMergeFailure(Exception): |
| pass |
| |
| |
| def UnpackApk(file_name, dst): |
| zippy = zipfile.ZipFile(file_name) |
| zippy.extractall(dst) |
| |
| |
| def GetNonDirFiles(top, base_dir): |
| """ Return a list containing all (non-directory) files in tree with top as |
| root. |
| |
| 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 |
| returned. |
| """ |
| if os.path.isdir(top): |
| ret = [] |
| for dirpath, _, filenames in os.walk(top): |
| for filename in filenames: |
| ret.append( |
| os.path.relpath(os.path.join(dirpath, filename), base_dir)) |
| return ret |
| else: |
| 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: |
| copy_files.extend( |
| 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: |
| 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. """ |
| file_set = set() |
| for file_name in actual_files: |
| base_name = os.path.basename(file_name) |
| if base_name not in expected_files: |
| raise ApkMergeFailure('Found unexpected file named %s.' % |
| file_name) |
| if base_name in file_set: |
| raise ApkMergeFailure('Duplicate file %s to add to APK!' % |
| file_name) |
| file_set.add(base_name) |
| |
| if len(file_set) != len(expected_files): |
| raise ApkMergeFailure('Missing expected files to add to APK!') |
| |
| |
| def AddDiffFiles(diff_files, tmp_dir_32, tmp_apk, expected_files): |
| """ Insert files only present in 32-bit APK into 64-bit APK (tmp_apk). """ |
| old_dir = os.getcwd() |
| # Move into 32-bit directory to make sure the files we insert have correct |
| # relative paths. |
| os.chdir(tmp_dir_32) |
| try: |
| for diff_file in diff_files: |
| extra_flags = expected_files[os.path.basename(diff_file)] |
| build_utils.CheckOutput(['zip', '-r', '-X', '--no-dir-entries', |
| tmp_apk, diff_file] + extra_flags) |
| except build_utils.CalledProcessError as e: |
| raise ApkMergeFailure( |
| 'Failed to add file %s to APK: %s' % (diff_file, e.output)) |
| finally: |
| # Move out of 32-bit directory when done |
| os.chdir(old_dir) |
| |
| |
| def RemoveMetafiles(tmp_apk): |
| """ Remove all meta info to avoid certificate clashes """ |
| try: |
| build_utils.CheckOutput(['zip', '-d', tmp_apk, 'META-INF/*']) |
| except build_utils.CalledProcessError as e: |
| raise ApkMergeFailure('Failed to delete Meta folder: ' + e.output) |
| |
| |
| def SignAndAlignApk(tmp_apk, signed_tmp_apk, new_apk, zipalign_path, |
| keystore_path, key_name, key_password): |
| try: |
| finalize_apk.JarSigner( |
| keystore_path, |
| key_name, |
| key_password, |
| tmp_apk, |
| signed_tmp_apk) |
| except build_utils.CalledProcessError as e: |
| raise ApkMergeFailure('Failed to sign APK: ' + e.output) |
| |
| try: |
| finalize_apk.AlignApk(zipalign_path, signed_tmp_apk, new_apk) |
| except build_utils.CalledProcessError as e: |
| raise ApkMergeFailure('Failed to align APK: ' + e.output) |
| |
| |
| 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', required=True, 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) |
| args = parser.parse_args() |
| |
| 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') |
| signed_tmp_apk = os.path.join(tmp_dir, 'signed.apk') |
| new_apk = args.out_apk |
| |
| # Expected files to copy from 32- to 64-bit APK together with an extra flag |
| # setting the compression level of the file |
| expected_files = {'snapshot_blob_32.bin': ['-0'], |
| 'natives_blob_32.bin': ['-0'], |
| 'libwebviewchromium.so': []} |
| |
| try: |
| shutil.copyfile(args.apk_64bit, tmp_apk) |
| |
| # need to unpack APKs to compare their contents |
| UnpackApk(args.apk_64bit, tmp_dir_64) |
| UnpackApk(args.apk_32bit, tmp_dir_32) |
| |
| dcmp = filecmp.dircmp( |
| tmp_dir_64, |
| tmp_dir_32, |
| ignore=['META-INF', 'AndroidManifest.xml']) |
| |
| 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) |
| |
| RemoveMetafiles(tmp_apk) |
| |
| AddDiffFiles(diff_files, tmp_dir_32, tmp_apk, expected_files) |
| |
| SignAndAlignApk(tmp_apk, signed_tmp_apk, new_apk, args.zipalign_path, |
| args.keystore_path, args.key_name, args.key_password) |
| |
| except ApkMergeFailure as e: |
| print e |
| return 1 |
| finally: |
| shutil.rmtree(tmp_dir) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |