Make aapt and aidl paths flexible.
[chromium-blink-merge.git] / build / android / gyp / process_resources.py
blobd227954ae96af22f748c6ab35b16780a5e1fa522
1 #!/usr/bin/env python
3 # Copyright (c) 2012 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.
7 """Process Android resources to generate R.java, and prepare for packaging.
9 This will crunch images and generate v14 compatible resources
10 (see generate_v14_compatible_resources.py).
11 """
13 import codecs
14 import optparse
15 import os
16 import re
17 import shutil
18 import sys
19 import zipfile
21 import generate_v14_compatible_resources
23 from util import build_utils
25 # Import jinja2 from third_party/jinja2
26 sys.path.insert(1,
27 os.path.join(os.path.dirname(__file__), '../../../third_party'))
28 from jinja2 import Template # pylint: disable=F0401
31 def ParseArgs(args):
32 """Parses command line options.
34 Returns:
35 An options object as from optparse.OptionsParser.parse_args()
36 """
37 parser = optparse.OptionParser()
38 build_utils.AddDepfileOption(parser)
40 parser.add_option('--android-sdk', help='path to the Android SDK folder')
41 parser.add_option('--aapt-path',
42 help='path to the Android aapt tool')
43 parser.add_option('--non-constant-id', action='store_true')
45 parser.add_option('--android-manifest', help='AndroidManifest.xml path')
46 parser.add_option('--custom-package', help='Java package for R.java')
47 parser.add_option(
48 '--shared-resources',
49 action='store_true',
50 help='Make a resource package that can be loaded by a different'
51 'application at runtime to access the package\'s resources.')
53 parser.add_option('--resource-dirs',
54 help='Directories containing resources of this target.')
55 parser.add_option('--dependencies-res-zips',
56 help='Resources from dependents.')
58 parser.add_option('--resource-zip-out',
59 help='Path for output zipped resources.')
61 parser.add_option('--R-dir',
62 help='directory to hold generated R.java.')
63 parser.add_option('--srcjar-out',
64 help='Path to srcjar to contain generated R.java.')
65 parser.add_option('--r-text-out',
66 help='Path to store the R.txt file generated by appt.')
68 parser.add_option('--proguard-file',
69 help='Path to proguard.txt generated file')
71 parser.add_option(
72 '--v14-skip',
73 action="store_true",
74 help='Do not generate nor verify v14 resources')
76 parser.add_option(
77 '--extra-res-packages',
78 help='Additional package names to generate R.java files for')
79 parser.add_option(
80 '--extra-r-text-files',
81 help='For each additional package, the R.txt file should contain a '
82 'list of resources to be included in the R.java file in the format '
83 'generated by aapt')
84 parser.add_option(
85 '--include-all-resources',
86 action='store_true',
87 help='Include every resource ID in every generated R.java file '
88 '(ignoring R.txt).')
90 parser.add_option(
91 '--all-resources-zip-out',
92 help='Path for output of all resources. This includes resources in '
93 'dependencies.')
95 parser.add_option('--stamp', help='File to touch on success')
97 (options, args) = parser.parse_args(args)
99 if args:
100 parser.error('No positional arguments should be given.')
102 # Check that required options have been provided.
103 required_options = (
104 'android_sdk',
105 'aapt_path',
106 'android_manifest',
107 'dependencies_res_zips',
108 'resource_dirs',
109 'resource_zip_out',
111 build_utils.CheckOptions(options, parser, required=required_options)
113 if (options.R_dir is None) == (options.srcjar_out is None):
114 raise Exception('Exactly one of --R-dir or --srcjar-out must be specified.')
116 return options
119 def CreateExtraRJavaFiles(
120 r_dir, extra_packages, extra_r_text_files, shared_resources, include_all):
121 if include_all:
122 java_files = build_utils.FindInDirectory(r_dir, "R.java")
123 if len(java_files) != 1:
124 return
125 r_java_file = java_files[0]
126 r_java_contents = codecs.open(r_java_file, encoding='utf-8').read()
128 for package in extra_packages:
129 package_r_java_dir = os.path.join(r_dir, *package.split('.'))
130 build_utils.MakeDirectory(package_r_java_dir)
131 package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
132 new_r_java = re.sub(r'package [.\w]*;', u'package %s;' % package,
133 r_java_contents)
134 codecs.open(package_r_java_path, 'w', encoding='utf-8').write(new_r_java)
135 else:
136 if len(extra_packages) != len(extra_r_text_files):
137 raise Exception('Need one R.txt file per extra package')
139 all_resources = {}
140 r_txt_file = os.path.join(r_dir, 'R.txt')
141 if not os.path.exists(r_txt_file):
142 return
143 with open(r_txt_file) as f:
144 for line in f:
145 m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line)
146 if not m:
147 raise Exception('Unexpected line in R.txt: %s' % line)
148 java_type, resource_type, name, value = m.groups()
149 all_resources[(resource_type, name)] = (java_type, value)
151 for package, r_text_file in zip(extra_packages, extra_r_text_files):
152 if os.path.exists(r_text_file):
153 package_r_java_dir = os.path.join(r_dir, *package.split('.'))
154 build_utils.MakeDirectory(package_r_java_dir)
155 package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
156 CreateExtraRJavaFile(
157 package, package_r_java_path, r_text_file, all_resources,
158 shared_resources)
161 def CreateExtraRJavaFile(
162 package, r_java_path, r_text_file, all_resources, shared_resources):
163 resources = {}
164 with open(r_text_file) as f:
165 for line in f:
166 m = re.match(r'int(?:\[\])? (\w+) (\w+) ', line)
167 if not m:
168 raise Exception('Unexpected line in R.txt: %s' % line)
169 resource_type, name = m.groups()
170 java_type, value = all_resources[(resource_type, name)]
171 if resource_type not in resources:
172 resources[resource_type] = []
173 resources[resource_type].append((name, java_type, value))
175 template = Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */
177 package {{ package }};
179 public final class R {
180 {% for resource_type in resources %}
181 public static final class {{ resource_type }} {
182 {% for name, java_type, value in resources[resource_type] %}
183 {% if shared_resources %}
184 public static {{ java_type }} {{ name }} = {{ value }};
185 {% else %}
186 public static final {{ java_type }} {{ name }} = {{ value }};
187 {% endif %}
188 {% endfor %}
190 {% endfor %}
191 {% if shared_resources %}
192 public static void onResourcesLoaded(int packageId) {
193 {% for resource_type in resources %}
194 {% for name, java_type, value in resources[resource_type] %}
195 {% if java_type == 'int[]' %}
196 for(int i = 0; i < {{ resource_type }}.{{ name }}.length; ++i) {
197 {{ resource_type }}.{{ name }}[i] =
198 ({{ resource_type }}.{{ name }}[i] & 0x00ffffff)
199 | (packageId << 24);
201 {% else %}
202 {{ resource_type }}.{{ name }} =
203 ({{ resource_type }}.{{ name }} & 0x00ffffff)
204 | (packageId << 24);
205 {% endif %}
206 {% endfor %}
207 {% endfor %}
209 {% endif %}
211 """, trim_blocks=True, lstrip_blocks=True)
213 output = template.render(package=package, resources=resources,
214 shared_resources=shared_resources)
215 with open(r_java_path, 'w') as f:
216 f.write(output)
219 def CrunchDirectory(aapt, input_dir, output_dir):
220 """Crunches the images in input_dir and its subdirectories into output_dir.
222 If an image is already optimized, crunching often increases image size. In
223 this case, the crunched image is overwritten with the original image.
225 aapt_cmd = [aapt,
226 'crunch',
227 '-C', output_dir,
228 '-S', input_dir,
229 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
230 build_utils.CheckOutput(aapt_cmd, stderr_filter=FilterCrunchStderr,
231 fail_func=DidCrunchFail)
233 # Check for images whose size increased during crunching and replace them
234 # with their originals (except for 9-patches, which must be crunched).
235 for dir_, _, files in os.walk(output_dir):
236 for crunched in files:
237 if crunched.endswith('.9.png'):
238 continue
239 if not crunched.endswith('.png'):
240 raise Exception('Unexpected file in crunched dir: ' + crunched)
241 crunched = os.path.join(dir_, crunched)
242 original = os.path.join(input_dir, os.path.relpath(crunched, output_dir))
243 original_size = os.path.getsize(original)
244 crunched_size = os.path.getsize(crunched)
245 if original_size < crunched_size:
246 shutil.copyfile(original, crunched)
249 def FilterCrunchStderr(stderr):
250 """Filters out lines from aapt crunch's stderr that can safely be ignored."""
251 filtered_lines = []
252 for line in stderr.splitlines(True):
253 # Ignore this libpng warning, which is a known non-error condition.
254 # http://crbug.com/364355
255 if ('libpng warning: iCCP: Not recognizing known sRGB profile that has '
256 + 'been edited' in line):
257 continue
258 filtered_lines.append(line)
259 return ''.join(filtered_lines)
262 def DidCrunchFail(returncode, stderr):
263 """Determines whether aapt crunch failed from its return code and output.
265 Because aapt's return code cannot be trusted, any output to stderr is
266 an indication that aapt has failed (http://crbug.com/314885).
268 return returncode != 0 or stderr
271 def ZipResources(resource_dirs, zip_path):
272 # Python zipfile does not provide a way to replace a file (it just writes
273 # another file with the same name). So, first collect all the files to put
274 # in the zip (with proper overriding), and then zip them.
275 files_to_zip = dict()
276 for d in resource_dirs:
277 for root, _, files in os.walk(d):
278 for f in files:
279 archive_path = os.path.join(os.path.relpath(root, d), f)
280 path = os.path.join(root, f)
281 files_to_zip[archive_path] = path
282 with zipfile.ZipFile(zip_path, 'w') as outzip:
283 for archive_path, path in files_to_zip.iteritems():
284 outzip.write(path, archive_path)
287 def CombineZips(zip_files, output_path):
288 # When packaging resources, if the top-level directories in the zip file are
289 # of the form 0, 1, ..., then each subdirectory will be passed to aapt as a
290 # resources directory. While some resources just clobber others (image files,
291 # etc), other resources (particularly .xml files) need to be more
292 # intelligently merged. That merging is left up to aapt.
293 with zipfile.ZipFile(output_path, 'w') as outzip:
294 for i, z in enumerate(zip_files):
295 with zipfile.ZipFile(z, 'r') as inzip:
296 for name in inzip.namelist():
297 new_name = '%d/%s' % (i, name)
298 outzip.writestr(new_name, inzip.read(name))
301 def main():
302 args = build_utils.ExpandFileArgs(sys.argv[1:])
304 options = ParseArgs(args)
305 android_jar = os.path.join(options.android_sdk, 'android.jar')
306 aapt = options.aapt_path
308 input_files = []
310 with build_utils.TempDir() as temp_dir:
311 deps_dir = os.path.join(temp_dir, 'deps')
312 build_utils.MakeDirectory(deps_dir)
313 v14_dir = os.path.join(temp_dir, 'v14')
314 build_utils.MakeDirectory(v14_dir)
316 gen_dir = os.path.join(temp_dir, 'gen')
317 build_utils.MakeDirectory(gen_dir)
319 input_resource_dirs = build_utils.ParseGypList(options.resource_dirs)
321 if not options.v14_skip:
322 for resource_dir in input_resource_dirs:
323 generate_v14_compatible_resources.GenerateV14Resources(
324 resource_dir,
325 v14_dir)
327 dep_zips = build_utils.ParseGypList(options.dependencies_res_zips)
328 input_files += dep_zips
329 dep_subdirs = []
330 for z in dep_zips:
331 subdir = os.path.join(deps_dir, os.path.basename(z))
332 if os.path.exists(subdir):
333 raise Exception('Resource zip name conflict: ' + os.path.basename(z))
334 build_utils.ExtractAll(z, path=subdir)
335 dep_subdirs.append(subdir)
337 # Generate R.java. This R.java contains non-final constants and is used only
338 # while compiling the library jar (e.g. chromium_content.jar). When building
339 # an apk, a new R.java file with the correct resource -> ID mappings will be
340 # generated by merging the resources from all libraries and the main apk
341 # project.
342 package_command = [aapt,
343 'package',
344 '-m',
345 '-M', options.android_manifest,
346 '--auto-add-overlay',
347 '-I', android_jar,
348 '--output-text-symbols', gen_dir,
349 '-J', gen_dir,
350 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
352 for d in input_resource_dirs:
353 package_command += ['-S', d]
355 for d in dep_subdirs:
356 package_command += ['-S', d]
358 if options.non_constant_id:
359 package_command.append('--non-constant-id')
360 if options.custom_package:
361 package_command += ['--custom-package', options.custom_package]
362 if options.proguard_file:
363 package_command += ['-G', options.proguard_file]
364 if options.shared_resources:
365 package_command.append('--shared-lib')
366 build_utils.CheckOutput(package_command, print_stderr=False)
368 if options.extra_res_packages:
369 CreateExtraRJavaFiles(
370 gen_dir,
371 build_utils.ParseGypList(options.extra_res_packages),
372 build_utils.ParseGypList(options.extra_r_text_files),
373 options.shared_resources,
374 options.include_all_resources)
376 # This is the list of directories with resources to put in the final .zip
377 # file. The order of these is important so that crunched/v14 resources
378 # override the normal ones.
379 zip_resource_dirs = input_resource_dirs + [v14_dir]
381 base_crunch_dir = os.path.join(temp_dir, 'crunch')
383 # Crunch image resources. This shrinks png files and is necessary for
384 # 9-patch images to display correctly. 'aapt crunch' accepts only a single
385 # directory at a time and deletes everything in the output directory.
386 for idx, input_dir in enumerate(input_resource_dirs):
387 crunch_dir = os.path.join(base_crunch_dir, str(idx))
388 build_utils.MakeDirectory(crunch_dir)
389 zip_resource_dirs.append(crunch_dir)
390 CrunchDirectory(aapt, input_dir, crunch_dir)
392 ZipResources(zip_resource_dirs, options.resource_zip_out)
394 if options.all_resources_zip_out:
395 CombineZips([options.resource_zip_out] + dep_zips,
396 options.all_resources_zip_out)
398 if options.R_dir:
399 build_utils.DeleteDirectory(options.R_dir)
400 shutil.copytree(gen_dir, options.R_dir)
401 else:
402 build_utils.ZipDir(options.srcjar_out, gen_dir)
404 if options.r_text_out:
405 r_text_path = os.path.join(gen_dir, 'R.txt')
406 if os.path.exists(r_text_path):
407 shutil.copyfile(r_text_path, options.r_text_out)
408 else:
409 open(options.r_text_out, 'w').close()
411 if options.depfile:
412 input_files += build_utils.GetPythonDependencies()
413 build_utils.WriteDepfile(options.depfile, input_files)
415 if options.stamp:
416 build_utils.Touch(options.stamp)
419 if __name__ == '__main__':
420 main()