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.
6 """ Merges a 64-bit and a 32-bit APK into a single APK
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
)
25 from util
import build_utils
27 class ApkMergeFailure(Exception):
31 def UnpackApk(file_name
, dst
):
32 zippy
= zipfile
.ZipFile(file_name
)
36 def GetNonDirFiles(top
, base_dir
):
37 """ Return a list containing all (non-directory) files in tree with top as
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
44 if os
.path
.isdir(top
):
46 for dirpath
, _
, filenames
in os
.walk(top
):
47 for filename
in filenames
:
49 os
.path
.relpath(os
.path
.join(dirpath
, filename
), base_dir
))
52 return [os
.path
.relpath(top
, base_dir
)]
55 def GetDiffFiles(dcmp
, base_dir
):
56 """ Return the list of files contained only in the right directory of dcmp.
58 The files returned are represented by relative paths from base_dir.
61 for file_name
in dcmp
.right_only
:
63 GetNonDirFiles(os
.path
.join(dcmp
.right
, file_name
), base_dir
))
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
))
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
))
74 for sub_dcmp
in dcmp
.subdirs
.itervalues():
75 copy_files
.extend(GetDiffFiles(sub_dcmp
, base_dir
))
79 def CheckFilesExpected(actual_files
, expected_files
):
80 """ Check that the lists of actual and expected files are the same. """
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.' %
87 if base_name
in file_set
:
88 raise ApkMergeFailure('Duplicate file %s to add to APK!' %
90 file_set
.add(base_name
)
92 if len(file_set
) != len(expected_files
):
93 raise ApkMergeFailure('Missing expected files to add to APK!')
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). """
99 # Move into 32-bit directory to make sure the files we insert have correct
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
))
111 # Move out of 32-bit directory when done
115 def RemoveMetafiles(tmp_apk
):
116 """ Remove all meta info to avoid certificate clashes """
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
)
123 def SignAndAlignApk(tmp_apk
, signed_tmp_apk
, new_apk
, zipalign_path
,
124 keystore_path
, key_name
, key_password
):
126 finalize_apk
.JarSigner(
132 except build_utils
.CalledProcessError
as e
:
133 raise ApkMergeFailure('Failed to sign APK: ' + e
.output
)
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
)
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()
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
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': []}
169 shutil
.copyfile(args
.apk_64bit
, tmp_apk
)
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
)
175 dcmp
= filecmp
.dircmp(
178 ignore
=['META-INF', 'AndroidManifest.xml'])
180 diff_files
= GetDiffFiles(dcmp
, tmp_dir_32
)
182 # Check that diff_files match exactly those files we want to insert into
184 CheckFilesExpected(diff_files
, expected_files
)
186 RemoveMetafiles(tmp_apk
)
188 AddDiffFiles(diff_files
, tmp_dir_32
, tmp_apk
, expected_files
)
190 SignAndAlignApk(tmp_apk
, signed_tmp_apk
, new_apk
, args
.zipalign_path
,
191 args
.keystore_path
, args
.key_name
, args
.key_password
)
193 except ApkMergeFailure
as e
:
197 shutil
.rmtree(tmp_dir
)
201 if __name__
== '__main__':