blob: 08a253795433cd1f1f1329f8b31bbc0dae5f4263 [file] [log] [blame]
Ben Murdoch097c5b22016-05-18 11:27:45 +01001#!/usr/bin/env python
2#
3# Copyright 2014 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7# pylint: disable=C0301
8"""Package resources into an apk.
9
10See https://android.googlesource.com/platform/tools/base/+/master/legacy/ant-tasks/src/main/java/com/android/ant/AaptExecTask.java
11and
12https://android.googlesource.com/platform/sdk/+/master/files/ant/build.xml
13"""
14# pylint: enable=C0301
15
16import optparse
17import os
18import re
19import shutil
20import sys
21import zipfile
22
23from util import build_utils
24
25
26# List is generated from the chrome_apk.apk_intermediates.ap_ via:
27# unzip -l $FILE_AP_ | cut -c31- | grep res/draw | cut -d'/' -f 2 | sort \
28# | uniq | grep -- -tvdpi- | cut -c10-
29# and then manually sorted.
30# Note that we can't just do a cross-product of dimentions because the filenames
31# become too big and aapt fails to create the files.
32# This leaves all default drawables (mdpi) in the main apk. Android gets upset
33# though if any drawables are missing from the default drawables/ directory.
34DENSITY_SPLITS = {
35 'hdpi': (
36 'hdpi-v4', # Order matters for output file names.
37 'ldrtl-hdpi-v4',
38 'sw600dp-hdpi-v13',
39 'ldrtl-hdpi-v17',
40 'ldrtl-sw600dp-hdpi-v17',
41 'hdpi-v21',
42 ),
43 'xhdpi': (
44 'xhdpi-v4',
45 'ldrtl-xhdpi-v4',
46 'sw600dp-xhdpi-v13',
47 'ldrtl-xhdpi-v17',
48 'ldrtl-sw600dp-xhdpi-v17',
49 'xhdpi-v21',
50 ),
51 'xxhdpi': (
52 'xxhdpi-v4',
53 'ldrtl-xxhdpi-v4',
54 'sw600dp-xxhdpi-v13',
55 'ldrtl-xxhdpi-v17',
56 'ldrtl-sw600dp-xxhdpi-v17',
57 'xxhdpi-v21',
58 ),
59 'xxxhdpi': (
60 'xxxhdpi-v4',
61 'ldrtl-xxxhdpi-v4',
62 'sw600dp-xxxhdpi-v13',
63 'ldrtl-xxxhdpi-v17',
64 'ldrtl-sw600dp-xxxhdpi-v17',
65 'xxxhdpi-v21',
66 ),
67 'tvdpi': (
68 'tvdpi-v4',
69 'sw600dp-tvdpi-v13',
70 'ldrtl-sw600dp-tvdpi-v17',
71 ),
72}
73
74
75def _ParseArgs(args):
76 """Parses command line options.
77
78 Returns:
79 An options object as from optparse.OptionsParser.parse_args()
80 """
81 parser = optparse.OptionParser()
82 build_utils.AddDepfileOption(parser)
83 parser.add_option('--android-sdk-jar',
84 help='path to the Android SDK jar.')
85 parser.add_option('--aapt-path',
86 help='path to the Android aapt tool')
87
88 parser.add_option('--configuration-name',
89 help='Gyp\'s configuration name (Debug or Release).')
90
91 parser.add_option('--android-manifest', help='AndroidManifest.xml path')
92 parser.add_option('--version-code', help='Version code for apk.')
93 parser.add_option('--version-name', help='Version name for apk.')
94 parser.add_option(
95 '--shared-resources',
96 action='store_true',
97 help='Make a resource package that can be loaded by a different'
98 'application at runtime to access the package\'s resources.')
99 parser.add_option(
100 '--app-as-shared-lib',
101 action='store_true',
102 help='Make a resource package that can be loaded as shared library')
103 parser.add_option('--resource-zips',
104 default='[]',
105 help='zip files containing resources to be packaged')
106 parser.add_option('--asset-dir',
107 help='directories containing assets to be packaged')
108 parser.add_option('--no-compress', help='disables compression for the '
109 'given comma separated list of extensions')
110 parser.add_option(
111 '--create-density-splits',
112 action='store_true',
113 help='Enables density splits')
114 parser.add_option('--language-splits',
115 default='[]',
116 help='GYP list of languages to create splits for')
117
118 parser.add_option('--apk-path',
119 help='Path to output (partial) apk.')
120
121 options, positional_args = parser.parse_args(args)
122
123 if positional_args:
124 parser.error('No positional arguments should be given.')
125
126 # Check that required options have been provided.
127 required_options = ('android_sdk_jar', 'aapt_path', 'configuration_name',
128 'android_manifest', 'version_code', 'version_name',
129 'apk_path')
130
131 build_utils.CheckOptions(options, parser, required=required_options)
132
133 options.resource_zips = build_utils.ParseGypList(options.resource_zips)
134 options.language_splits = build_utils.ParseGypList(options.language_splits)
135 return options
136
137
138def MoveImagesToNonMdpiFolders(res_root):
139 """Move images from drawable-*-mdpi-* folders to drawable-* folders.
140
141 Why? http://crbug.com/289843
142 """
143 for src_dir_name in os.listdir(res_root):
144 src_components = src_dir_name.split('-')
145 if src_components[0] != 'drawable' or 'mdpi' not in src_components:
146 continue
147 src_dir = os.path.join(res_root, src_dir_name)
148 if not os.path.isdir(src_dir):
149 continue
150 dst_components = [c for c in src_components if c != 'mdpi']
151 assert dst_components != src_components
152 dst_dir_name = '-'.join(dst_components)
153 dst_dir = os.path.join(res_root, dst_dir_name)
154 build_utils.MakeDirectory(dst_dir)
155 for src_file_name in os.listdir(src_dir):
156 if not src_file_name.endswith('.png'):
157 continue
158 src_file = os.path.join(src_dir, src_file_name)
159 dst_file = os.path.join(dst_dir, src_file_name)
160 assert not os.path.lexists(dst_file)
161 shutil.move(src_file, dst_file)
162
163
164def PackageArgsForExtractedZip(d):
165 """Returns the aapt args for an extracted resources zip.
166
167 A resources zip either contains the resources for a single target or for
168 multiple targets. If it is multiple targets merged into one, the actual
169 resource directories will be contained in the subdirectories 0, 1, 2, ...
170 """
171 subdirs = [os.path.join(d, s) for s in os.listdir(d)]
172 subdirs = [s for s in subdirs if os.path.isdir(s)]
173 is_multi = '0' in [os.path.basename(s) for s in subdirs]
174 if is_multi:
175 res_dirs = sorted(subdirs, key=lambda p : int(os.path.basename(p)))
176 else:
177 res_dirs = [d]
178 package_command = []
179 for d in res_dirs:
180 MoveImagesToNonMdpiFolders(d)
181 package_command += ['-S', d]
182 return package_command
183
184
185def _GenerateDensitySplitPaths(apk_path):
186 for density, config in DENSITY_SPLITS.iteritems():
187 src_path = '%s_%s' % (apk_path, '_'.join(config))
188 dst_path = '%s_%s' % (apk_path, density)
189 yield src_path, dst_path
190
191
192def _GenerateLanguageSplitOutputPaths(apk_path, languages):
193 for lang in languages:
194 yield '%s_%s' % (apk_path, lang)
195
196
197def RenameDensitySplits(apk_path):
198 """Renames all density splits to have shorter / predictable names."""
199 for src_path, dst_path in _GenerateDensitySplitPaths(apk_path):
200 shutil.move(src_path, dst_path)
201
202
203def CheckForMissedConfigs(apk_path, check_density, languages):
204 """Raises an exception if apk_path contains any unexpected configs."""
205 triggers = []
206 if check_density:
207 triggers.extend(re.compile('-%s' % density) for density in DENSITY_SPLITS)
208 if languages:
209 triggers.extend(re.compile(r'-%s\b' % lang) for lang in languages)
210 with zipfile.ZipFile(apk_path) as main_apk_zip:
211 for name in main_apk_zip.namelist():
212 for trigger in triggers:
213 if trigger.search(name) and not 'mipmap-' in name:
214 raise Exception(('Found config in main apk that should have been ' +
215 'put into a split: %s\nYou need to update ' +
216 'package_resources.py to include this new ' +
217 'config (trigger=%s)') % (name, trigger.pattern))
218
219
220def _ConstructMostAaptArgs(options):
221 package_command = [
222 options.aapt_path,
223 'package',
224 '--version-code', options.version_code,
225 '--version-name', options.version_name,
226 '-M', options.android_manifest,
227 '--no-crunch',
228 '-f',
229 '--auto-add-overlay',
230 '--no-version-vectors',
231 '-I', options.android_sdk_jar,
232 '-F', options.apk_path,
233 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN,
234 ]
235
236 if options.no_compress:
237 for ext in options.no_compress.split(','):
238 package_command += ['-0', ext]
239
240 if options.shared_resources:
241 package_command.append('--shared-lib')
242
243 if options.app_as_shared_lib:
244 package_command.append('--app-as-shared-lib')
245
246 if options.asset_dir and os.path.exists(options.asset_dir):
247 package_command += ['-A', options.asset_dir]
248
249 if options.create_density_splits:
250 for config in DENSITY_SPLITS.itervalues():
251 package_command.extend(('--split', ','.join(config)))
252
253 if options.language_splits:
254 for lang in options.language_splits:
255 package_command.extend(('--split', lang))
256
257 if 'Debug' in options.configuration_name:
258 package_command += ['--debug-mode']
259
260 return package_command
261
262
263def _OnStaleMd5(package_command, options):
264 with build_utils.TempDir() as temp_dir:
265 if options.resource_zips:
266 dep_zips = options.resource_zips
267 for z in dep_zips:
268 subdir = os.path.join(temp_dir, os.path.basename(z))
269 if os.path.exists(subdir):
270 raise Exception('Resource zip name conflict: ' + os.path.basename(z))
271 build_utils.ExtractAll(z, path=subdir)
272 package_command += PackageArgsForExtractedZip(subdir)
273
274 build_utils.CheckOutput(
275 package_command, print_stdout=False, print_stderr=False)
276
277 if options.create_density_splits or options.language_splits:
278 CheckForMissedConfigs(options.apk_path, options.create_density_splits,
279 options.language_splits)
280
281 if options.create_density_splits:
282 RenameDensitySplits(options.apk_path)
283
284
285def main(args):
286 args = build_utils.ExpandFileArgs(args)
287 options = _ParseArgs(args)
288
289 package_command = _ConstructMostAaptArgs(options)
290
291 output_paths = [ options.apk_path ]
292
293 if options.create_density_splits:
294 for _, dst_path in _GenerateDensitySplitPaths(options.apk_path):
295 output_paths.append(dst_path)
296 output_paths.extend(
297 _GenerateLanguageSplitOutputPaths(options.apk_path,
298 options.language_splits))
299
300 input_paths = [ options.android_manifest ] + options.resource_zips
301
302 input_strings = []
303 input_strings.extend(package_command)
304
305 # The md5_check.py doesn't count file path in md5 intentionally,
306 # in order to repackage resources when assets' name changed, we need
307 # to put assets into input_strings, as we know the assets path isn't
308 # changed among each build if there is no asset change.
309 if options.asset_dir and os.path.exists(options.asset_dir):
310 asset_paths = []
311 for root, _, filenames in os.walk(options.asset_dir):
312 asset_paths.extend(os.path.join(root, f) for f in filenames)
313 input_paths.extend(asset_paths)
314 input_strings.extend(sorted(asset_paths))
315
316 build_utils.CallAndWriteDepfileIfStale(
317 lambda: _OnStaleMd5(package_command, options),
318 options,
319 input_paths=input_paths,
320 input_strings=input_strings,
321 output_paths=output_paths)
322
323
324if __name__ == '__main__':
325 main(sys.argv[1:])