OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """ Merges a 64-bit and a 32-bit APK into a single APK |
| 7 |
| 8 """ |
| 9 |
| 10 import os |
| 11 import sys |
| 12 import shutil |
| 13 import zipfile |
| 14 import filecmp |
| 15 import tempfile |
| 16 import argparse |
| 17 import subprocess |
| 18 |
| 19 SRC_DIR = os.path.join(os.path.dirname(__file__), '..', '..') |
| 20 SRC_DIR = os.path.abspath(SRC_DIR) |
| 21 BUILD_ANDROID_GYP_DIR = os.path.join(SRC_DIR, 'build/android/gyp') |
| 22 sys.path.append(BUILD_ANDROID_GYP_DIR) |
| 23 |
| 24 import finalize_apk |
| 25 from util import build_utils |
| 26 |
| 27 class ApkMergeFailure(Exception): |
| 28 pass |
| 29 |
| 30 |
| 31 def UnpackApk(file_name, dst): |
| 32 zippy = zipfile.ZipFile(file_name) |
| 33 zippy.extractall(dst) |
| 34 |
| 35 |
| 36 def GetNonDirFiles(top, base_dir): |
| 37 """ Return a list containing all (non-directory) files in tree with top as |
| 38 root. |
| 39 |
| 40 Each file is represented by the relative path from base_dir to that file. |
| 41 If top is a file (not a directory) then a list containing only top is |
| 42 returned. |
| 43 """ |
| 44 if os.path.isdir(top): |
| 45 ret = [] |
| 46 for dirpath, _, filenames in os.walk(top): |
| 47 for filename in filenames: |
| 48 ret.append( |
| 49 os.path.relpath(os.path.join(dirpath, filename), base_dir)) |
| 50 return ret |
| 51 else: |
| 52 return [os.path.relpath(top, base_dir)] |
| 53 |
| 54 |
| 55 def GetDiffFiles(dcmp, base_dir): |
| 56 """ Return the list of files contained only in the right directory of dcmp. |
| 57 |
| 58 The files returned are represented by relative paths from base_dir. |
| 59 """ |
| 60 copy_files = [] |
| 61 for file_name in dcmp.right_only: |
| 62 copy_files.extend( |
| 63 GetNonDirFiles(os.path.join(dcmp.right, file_name), base_dir)) |
| 64 |
| 65 # we cannot merge APKs with files with similar names but different contents |
| 66 if len(dcmp.diff_files) > 0: |
| 67 raise ApkMergeFailure('found differing files: %s in %s and %s' % |
| 68 (dcmp.diff_files, dcmp.left, dcmp.right)) |
| 69 |
| 70 if len(dcmp.funny_files) > 0: |
| 71 ApkMergeFailure('found uncomparable files: %s in %s and %s' % |
| 72 (dcmp.funny_files, dcmp.left, dcmp.right)) |
| 73 |
| 74 for sub_dcmp in dcmp.subdirs.itervalues(): |
| 75 copy_files.extend(GetDiffFiles(sub_dcmp, base_dir)) |
| 76 return copy_files |
| 77 |
| 78 |
| 79 def CheckFilesExpected(actual_files, expected_files): |
| 80 """ Check that the lists of actual and expected files are the same. """ |
| 81 file_set = set() |
| 82 for file_name in actual_files: |
| 83 base_name = os.path.basename(file_name) |
| 84 if base_name not in expected_files: |
| 85 raise ApkMergeFailure('Found unexpected file named %s.' % |
| 86 file_name) |
| 87 if base_name in file_set: |
| 88 raise ApkMergeFailure('Duplicate file %s to add to APK!' % |
| 89 file_name) |
| 90 file_set.add(base_name) |
| 91 |
| 92 if len(file_set) != len(expected_files): |
| 93 raise ApkMergeFailure('Missing expected files to add to APK!') |
| 94 |
| 95 |
| 96 def AddDiffFiles(diff_files, tmp_dir_32, tmp_apk, expected_files): |
| 97 """ Insert files only present in 32-bit APK into 64-bit APK (tmp_apk). """ |
| 98 old_dir = os.getcwd() |
| 99 # Move into 32-bit directory to make sure the files we insert have correct |
| 100 # relative paths. |
| 101 os.chdir(tmp_dir_32) |
| 102 try: |
| 103 for diff_file in diff_files: |
| 104 extra_flags = expected_files[os.path.basename(diff_file)] |
| 105 build_utils.CheckOutput(['zip', '-r', '-X', '--no-dir-entries', |
| 106 tmp_apk, diff_file] + extra_flags) |
| 107 except build_utils.CalledProcessError as e: |
| 108 raise ApkMergeFailure( |
| 109 'Failed to add file %s to APK: %s' % (diff_file, e.output)) |
| 110 finally: |
| 111 # Move out of 32-bit directory when done |
| 112 os.chdir(old_dir) |
| 113 |
| 114 |
| 115 def RemoveMetafiles(tmp_apk): |
| 116 """ Remove all meta info to avoid certificate clashes """ |
| 117 try: |
| 118 build_utils.CheckOutput(['zip', '-d', tmp_apk, 'META-INF/*']) |
| 119 except build_utils.CalledProcessError as e: |
| 120 raise ApkMergeFailure('Failed to delete Meta folder: ' + e.output) |
| 121 |
| 122 |
| 123 def SignAndAlignApk(tmp_apk, signed_tmp_apk, new_apk, zipalign_path, |
| 124 keystore_path, key_name, key_password): |
| 125 try: |
| 126 finalize_apk.JarSigner( |
| 127 keystore_path, |
| 128 key_name, |
| 129 key_password, |
| 130 tmp_apk, |
| 131 signed_tmp_apk) |
| 132 except build_utils.CalledProcessError as e: |
| 133 raise ApkMergeFailure('Failed to sign APK: ' + e.output) |
| 134 |
| 135 try: |
| 136 finalize_apk.AlignApk(zipalign_path, signed_tmp_apk, new_apk) |
| 137 except build_utils.CalledProcessError as e: |
| 138 raise ApkMergeFailure('Failed to align APK: ' + e.output) |
| 139 |
| 140 |
| 141 def main(): |
| 142 parser = argparse.ArgumentParser( |
| 143 description='Merge a 32-bit APK into a 64-bit APK') |
| 144 # Using type=os.path.abspath converts file paths to absolute paths so that |
| 145 # we can change working directory without affecting these paths |
| 146 parser.add_argument('--apk_32bit', required=True, type=os.path.abspath) |
| 147 parser.add_argument('--apk_64bit', required=True, type=os.path.abspath) |
| 148 parser.add_argument('--out_apk', required=True, type=os.path.abspath) |
| 149 parser.add_argument('--zipalign_path', required=True, type=os.path.abspath) |
| 150 parser.add_argument('--keystore_path', required=True, type=os.path.abspath) |
| 151 parser.add_argument('--key_name', required=True) |
| 152 parser.add_argument('--key_password', required=True) |
| 153 args = parser.parse_args() |
| 154 |
| 155 tmp_dir = tempfile.mkdtemp() |
| 156 tmp_dir_64 = os.path.join(tmp_dir, '64_bit') |
| 157 tmp_dir_32 = os.path.join(tmp_dir, '32_bit') |
| 158 tmp_apk = os.path.join(tmp_dir, 'tmp.apk') |
| 159 signed_tmp_apk = os.path.join(tmp_dir, 'signed.apk') |
| 160 new_apk = args.out_apk |
| 161 |
| 162 # Expected files to copy from 32- to 64-bit APK together with an extra flag |
| 163 # setting the compression level of the file |
| 164 expected_files = {'snapshot_blob_32.bin': ['-0'], |
| 165 'natives_blob_32.bin': ['-0'], |
| 166 'libwebviewchromium.so': []} |
| 167 |
| 168 try: |
| 169 shutil.copyfile(args.apk_64bit, tmp_apk) |
| 170 |
| 171 # need to unpack APKs to compare their contents |
| 172 UnpackApk(args.apk_64bit, tmp_dir_64) |
| 173 UnpackApk(args.apk_32bit, tmp_dir_32) |
| 174 |
| 175 dcmp = filecmp.dircmp( |
| 176 tmp_dir_64, |
| 177 tmp_dir_32, |
| 178 ignore=['META-INF', 'AndroidManifest.xml']) |
| 179 |
| 180 diff_files = GetDiffFiles(dcmp, tmp_dir_32) |
| 181 |
| 182 # Check that diff_files match exactly those files we want to insert into |
| 183 # the 64-bit APK. |
| 184 CheckFilesExpected(diff_files, expected_files) |
| 185 |
| 186 RemoveMetafiles(tmp_apk) |
| 187 |
| 188 AddDiffFiles(diff_files, tmp_dir_32, tmp_apk, expected_files) |
| 189 |
| 190 SignAndAlignApk(tmp_apk, signed_tmp_apk, new_apk, args.zipalign_path, |
| 191 args.keystore_path, args.key_name, args.key_password) |
| 192 |
| 193 except ApkMergeFailure as e: |
| 194 print e |
| 195 return 1 |
| 196 finally: |
| 197 shutil.rmtree(tmp_dir) |
| 198 return 0 |
| 199 |
| 200 |
| 201 if __name__ == '__main__': |
| 202 sys.exit(main()) |
OLD | NEW |