Roll src/third_party/WebKit d9c6159:8139f33 (svn 201974:201975)
[chromium-blink-merge.git] / build / android / gyp / process_resources.py
blobe05e0fdff7255f5a9466909433d419f4c16d8fd8
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
20 import generate_v14_compatible_resources
22 from util import build_utils
24 # Import jinja2 from third_party/jinja2
25 sys.path.insert(1,
26 os.path.join(os.path.dirname(__file__), '../../../third_party'))
27 from jinja2 import Template # pylint: disable=F0401
30 def ParseArgs(args):
31 """Parses command line options.
33 Returns:
34 An options object as from optparse.OptionsParser.parse_args()
35 """
36 parser = optparse.OptionParser()
37 build_utils.AddDepfileOption(parser)
39 parser.add_option('--android-sdk', help='path to the Android SDK folder')
40 parser.add_option('--aapt-path',
41 help='path to the Android aapt tool')
42 parser.add_option('--non-constant-id', action='store_true')
44 parser.add_option('--android-manifest', help='AndroidManifest.xml path')
45 parser.add_option('--custom-package', help='Java package for R.java')
46 parser.add_option(
47 '--shared-resources',
48 action='store_true',
49 help='Make a resource package that can be loaded by a different'
50 'application at runtime to access the package\'s resources.')
52 parser.add_option('--resource-dirs',
53 help='Directories containing resources of this target.')
54 parser.add_option('--dependencies-res-zips',
55 help='Resources from dependents.')
57 parser.add_option('--resource-zip-out',
58 help='Path for output zipped resources.')
60 parser.add_option('--R-dir',
61 help='directory to hold generated R.java.')
62 parser.add_option('--srcjar-out',
63 help='Path to srcjar to contain generated R.java.')
64 parser.add_option('--r-text-out',
65 help='Path to store the R.txt file generated by appt.')
67 parser.add_option('--proguard-file',
68 help='Path to proguard.txt generated file')
70 parser.add_option(
71 '--v14-skip',
72 action="store_true",
73 help='Do not generate nor verify v14 resources')
75 parser.add_option(
76 '--extra-res-packages',
77 help='Additional package names to generate R.java files for')
78 parser.add_option(
79 '--extra-r-text-files',
80 help='For each additional package, the R.txt file should contain a '
81 'list of resources to be included in the R.java file in the format '
82 'generated by aapt')
83 parser.add_option(
84 '--include-all-resources',
85 action='store_true',
86 help='Include every resource ID in every generated R.java file '
87 '(ignoring R.txt).')
89 parser.add_option(
90 '--all-resources-zip-out',
91 help='Path for output of all resources. This includes resources in '
92 'dependencies.')
94 parser.add_option('--stamp', help='File to touch on success')
96 (options, args) = parser.parse_args(args)
98 if args:
99 parser.error('No positional arguments should be given.')
101 # Check that required options have been provided.
102 required_options = (
103 'android_sdk',
104 'aapt_path',
105 'android_manifest',
106 'dependencies_res_zips',
107 'resource_dirs',
108 'resource_zip_out',
110 build_utils.CheckOptions(options, parser, required=required_options)
112 if (options.R_dir is None) == (options.srcjar_out is None):
113 raise Exception('Exactly one of --R-dir or --srcjar-out must be specified.')
115 return options
118 def CreateExtraRJavaFiles(
119 r_dir, extra_packages, extra_r_text_files, shared_resources, include_all):
120 if include_all:
121 java_files = build_utils.FindInDirectory(r_dir, "R.java")
122 if len(java_files) != 1:
123 return
124 r_java_file = java_files[0]
125 r_java_contents = codecs.open(r_java_file, encoding='utf-8').read()
127 for package in extra_packages:
128 package_r_java_dir = os.path.join(r_dir, *package.split('.'))
129 build_utils.MakeDirectory(package_r_java_dir)
130 package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
131 new_r_java = re.sub(r'package [.\w]*;', u'package %s;' % package,
132 r_java_contents)
133 codecs.open(package_r_java_path, 'w', encoding='utf-8').write(new_r_java)
134 else:
135 if len(extra_packages) != len(extra_r_text_files):
136 raise Exception('Need one R.txt file per extra package')
138 all_resources = {}
139 r_txt_file = os.path.join(r_dir, 'R.txt')
140 if not os.path.exists(r_txt_file):
141 return
142 with open(r_txt_file) as f:
143 for line in f:
144 m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line)
145 if not m:
146 raise Exception('Unexpected line in R.txt: %s' % line)
147 java_type, resource_type, name, value = m.groups()
148 all_resources[(resource_type, name)] = (java_type, value)
150 for package, r_text_file in zip(extra_packages, extra_r_text_files):
151 if os.path.exists(r_text_file):
152 package_r_java_dir = os.path.join(r_dir, *package.split('.'))
153 build_utils.MakeDirectory(package_r_java_dir)
154 package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
155 CreateExtraRJavaFile(
156 package, package_r_java_path, r_text_file, all_resources,
157 shared_resources)
160 def CreateExtraRJavaFile(
161 package, r_java_path, r_text_file, all_resources, shared_resources):
162 resources = {}
163 with open(r_text_file) as f:
164 for line in f:
165 m = re.match(r'int(?:\[\])? (\w+) (\w+) ', line)
166 if not m:
167 raise Exception('Unexpected line in R.txt: %s' % line)
168 resource_type, name = m.groups()
169 java_type, value = all_resources[(resource_type, name)]
170 if resource_type not in resources:
171 resources[resource_type] = []
172 resources[resource_type].append((name, java_type, value))
174 template = Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */
176 package {{ package }};
178 public final class R {
179 {% for resource_type in resources %}
180 public static final class {{ resource_type }} {
181 {% for name, java_type, value in resources[resource_type] %}
182 {% if shared_resources %}
183 public static {{ java_type }} {{ name }} = {{ value }};
184 {% else %}
185 public static final {{ java_type }} {{ name }} = {{ value }};
186 {% endif %}
187 {% endfor %}
189 {% endfor %}
190 {% if shared_resources %}
191 public static void onResourcesLoaded(int packageId) {
192 {% for resource_type in resources %}
193 {% for name, java_type, value in resources[resource_type] %}
194 {% if java_type == 'int[]' %}
195 for(int i = 0; i < {{ resource_type }}.{{ name }}.length; ++i) {
196 {{ resource_type }}.{{ name }}[i] =
197 ({{ resource_type }}.{{ name }}[i] & 0x00ffffff)
198 | (packageId << 24);
200 {% else %}
201 {{ resource_type }}.{{ name }} =
202 ({{ resource_type }}.{{ name }} & 0x00ffffff)
203 | (packageId << 24);
204 {% endif %}
205 {% endfor %}
206 {% endfor %}
208 {% endif %}
210 """, trim_blocks=True, lstrip_blocks=True)
212 output = template.render(package=package, resources=resources,
213 shared_resources=shared_resources)
214 with open(r_java_path, 'w') as f:
215 f.write(output)
218 def CrunchDirectory(aapt, input_dir, output_dir):
219 """Crunches the images in input_dir and its subdirectories into output_dir.
221 If an image is already optimized, crunching often increases image size. In
222 this case, the crunched image is overwritten with the original image.
224 aapt_cmd = [aapt,
225 'crunch',
226 '-C', output_dir,
227 '-S', input_dir,
228 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
229 build_utils.CheckOutput(aapt_cmd, stderr_filter=FilterCrunchStderr,
230 fail_func=DidCrunchFail)
232 # Check for images whose size increased during crunching and replace them
233 # with their originals (except for 9-patches, which must be crunched).
234 for dir_, _, files in os.walk(output_dir):
235 for crunched in files:
236 if crunched.endswith('.9.png'):
237 continue
238 if not crunched.endswith('.png'):
239 raise Exception('Unexpected file in crunched dir: ' + crunched)
240 crunched = os.path.join(dir_, crunched)
241 original = os.path.join(input_dir, os.path.relpath(crunched, output_dir))
242 original_size = os.path.getsize(original)
243 crunched_size = os.path.getsize(crunched)
244 if original_size < crunched_size:
245 shutil.copyfile(original, crunched)
248 def FilterCrunchStderr(stderr):
249 """Filters out lines from aapt crunch's stderr that can safely be ignored."""
250 filtered_lines = []
251 for line in stderr.splitlines(True):
252 # Ignore this libpng warning, which is a known non-error condition.
253 # http://crbug.com/364355
254 if ('libpng warning: iCCP: Not recognizing known sRGB profile that has '
255 + 'been edited' in line):
256 continue
257 filtered_lines.append(line)
258 return ''.join(filtered_lines)
261 def DidCrunchFail(returncode, stderr):
262 """Determines whether aapt crunch failed from its return code and output.
264 Because aapt's return code cannot be trusted, any output to stderr is
265 an indication that aapt has failed (http://crbug.com/314885).
267 return returncode != 0 or stderr
270 def ZipResources(resource_dirs, zip_path):
271 # Python zipfile does not provide a way to replace a file (it just writes
272 # another file with the same name). So, first collect all the files to put
273 # in the zip (with proper overriding), and then zip them.
274 files_to_zip = dict()
275 for d in resource_dirs:
276 for root, _, files in os.walk(d):
277 for f in files:
278 archive_path = f
279 parent_dir = os.path.relpath(root, d)
280 if parent_dir != '.':
281 archive_path = os.path.join(parent_dir, f)
282 path = os.path.join(root, f)
283 files_to_zip[archive_path] = path
284 build_utils.DoZip(files_to_zip.iteritems(), zip_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 def path_transform(name, src_zip):
294 return '%d/%s' % (zip_files.index(src_zip), name)
296 build_utils.MergeZips(output_path, zip_files, path_transform=path_transform)
299 def main():
300 args = build_utils.ExpandFileArgs(sys.argv[1:])
302 options = ParseArgs(args)
303 android_jar = os.path.join(options.android_sdk, 'android.jar')
304 aapt = options.aapt_path
306 input_files = []
308 with build_utils.TempDir() as temp_dir:
309 deps_dir = os.path.join(temp_dir, 'deps')
310 build_utils.MakeDirectory(deps_dir)
311 v14_dir = os.path.join(temp_dir, 'v14')
312 build_utils.MakeDirectory(v14_dir)
314 gen_dir = os.path.join(temp_dir, 'gen')
315 build_utils.MakeDirectory(gen_dir)
317 input_resource_dirs = build_utils.ParseGypList(options.resource_dirs)
319 if not options.v14_skip:
320 for resource_dir in input_resource_dirs:
321 generate_v14_compatible_resources.GenerateV14Resources(
322 resource_dir,
323 v14_dir)
325 dep_zips = build_utils.ParseGypList(options.dependencies_res_zips)
326 input_files += dep_zips
327 dep_subdirs = []
328 for z in dep_zips:
329 subdir = os.path.join(deps_dir, os.path.basename(z))
330 if os.path.exists(subdir):
331 raise Exception('Resource zip name conflict: ' + os.path.basename(z))
332 build_utils.ExtractAll(z, path=subdir)
333 dep_subdirs.append(subdir)
335 # Generate R.java. This R.java contains non-final constants and is used only
336 # while compiling the library jar (e.g. chromium_content.jar). When building
337 # an apk, a new R.java file with the correct resource -> ID mappings will be
338 # generated by merging the resources from all libraries and the main apk
339 # project.
340 package_command = [aapt,
341 'package',
342 '-m',
343 '-M', options.android_manifest,
344 '--auto-add-overlay',
345 '-I', android_jar,
346 '--output-text-symbols', gen_dir,
347 '-J', gen_dir,
348 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
350 for d in input_resource_dirs:
351 package_command += ['-S', d]
353 for d in dep_subdirs:
354 package_command += ['-S', d]
356 if options.non_constant_id:
357 package_command.append('--non-constant-id')
358 if options.custom_package:
359 package_command += ['--custom-package', options.custom_package]
360 if options.proguard_file:
361 package_command += ['-G', options.proguard_file]
362 if options.shared_resources:
363 package_command.append('--shared-lib')
364 build_utils.CheckOutput(package_command, print_stderr=False)
366 if options.extra_res_packages:
367 CreateExtraRJavaFiles(
368 gen_dir,
369 build_utils.ParseGypList(options.extra_res_packages),
370 build_utils.ParseGypList(options.extra_r_text_files),
371 options.shared_resources,
372 options.include_all_resources)
374 # This is the list of directories with resources to put in the final .zip
375 # file. The order of these is important so that crunched/v14 resources
376 # override the normal ones.
377 zip_resource_dirs = input_resource_dirs + [v14_dir]
379 base_crunch_dir = os.path.join(temp_dir, 'crunch')
381 # Crunch image resources. This shrinks png files and is necessary for
382 # 9-patch images to display correctly. 'aapt crunch' accepts only a single
383 # directory at a time and deletes everything in the output directory.
384 for idx, input_dir in enumerate(input_resource_dirs):
385 crunch_dir = os.path.join(base_crunch_dir, str(idx))
386 build_utils.MakeDirectory(crunch_dir)
387 zip_resource_dirs.append(crunch_dir)
388 CrunchDirectory(aapt, input_dir, crunch_dir)
390 ZipResources(zip_resource_dirs, options.resource_zip_out)
392 if options.all_resources_zip_out:
393 CombineZips([options.resource_zip_out] + dep_zips,
394 options.all_resources_zip_out)
396 if options.R_dir:
397 build_utils.DeleteDirectory(options.R_dir)
398 shutil.copytree(gen_dir, options.R_dir)
399 else:
400 build_utils.ZipDir(options.srcjar_out, gen_dir)
402 if options.r_text_out:
403 r_text_path = os.path.join(gen_dir, 'R.txt')
404 if os.path.exists(r_text_path):
405 shutil.copyfile(r_text_path, options.r_text_out)
406 else:
407 open(options.r_text_out, 'w').close()
409 if options.depfile:
410 input_files += build_utils.GetPythonDependencies()
411 build_utils.WriteDepfile(options.depfile, input_files)
413 if options.stamp:
414 build_utils.Touch(options.stamp)
417 if __name__ == '__main__':
418 main()