• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
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.
6
7"""Process Android resources to generate R.java, and prepare for packaging.
8
9This will crunch images and generate v14 compatible resources
10(see generate_v14_compatible_resources.py).
11"""
12
13import codecs
14import collections
15import optparse
16import os
17import re
18import shutil
19import sys
20
21import generate_v14_compatible_resources
22
23from util import build_utils
24
25# Import jinja2 from third_party/jinja2
26sys.path.insert(1,
27    os.path.join(os.path.dirname(__file__), '../../../third_party'))
28from jinja2 import Template # pylint: disable=F0401
29
30
31# Represents a line from a R.txt file.
32TextSymbolsEntry = collections.namedtuple('RTextEntry',
33    ('java_type', 'resource_type', 'name', 'value'))
34
35
36def _ParseArgs(args):
37  """Parses command line options.
38
39  Returns:
40    An options object as from optparse.OptionsParser.parse_args()
41  """
42  parser = optparse.OptionParser()
43  build_utils.AddDepfileOption(parser)
44
45  parser.add_option('--android-sdk-jar',
46                    help='the path to android jar file.')
47  parser.add_option('--aapt-path',
48                    help='path to the Android aapt tool')
49  parser.add_option('--non-constant-id', action='store_true')
50
51  parser.add_option('--android-manifest', help='AndroidManifest.xml path')
52  parser.add_option('--custom-package', help='Java package for R.java')
53  parser.add_option(
54      '--shared-resources',
55      action='store_true',
56      help='Make a resource package that can be loaded by a different'
57      'application at runtime to access the package\'s resources.')
58  parser.add_option(
59      '--app-as-shared-lib',
60      action='store_true',
61      help='Make a resource package that can be loaded as shared library.')
62
63  parser.add_option('--resource-dirs',
64                    help='Directories containing resources of this target.')
65  parser.add_option('--dependencies-res-zips',
66                    help='Resources from dependents.')
67
68  parser.add_option('--resource-zip-out',
69                    help='Path for output zipped resources.')
70
71  parser.add_option('--R-dir',
72                    help='directory to hold generated R.java.')
73  parser.add_option('--srcjar-out',
74                    help='Path to srcjar to contain generated R.java.')
75  parser.add_option('--r-text-out',
76                    help='Path to store the R.txt file generated by appt.')
77
78  parser.add_option('--proguard-file',
79                    help='Path to proguard.txt generated file')
80
81  parser.add_option(
82      '--v14-skip',
83      action="store_true",
84      help='Do not generate nor verify v14 resources')
85
86  parser.add_option(
87      '--extra-res-packages',
88      help='Additional package names to generate R.java files for')
89  parser.add_option(
90      '--extra-r-text-files',
91      help='For each additional package, the R.txt file should contain a '
92      'list of resources to be included in the R.java file in the format '
93      'generated by aapt')
94  parser.add_option(
95      '--include-all-resources',
96      action='store_true',
97      help='Include every resource ID in every generated R.java file '
98      '(ignoring R.txt).')
99
100  parser.add_option(
101      '--all-resources-zip-out',
102      help='Path for output of all resources. This includes resources in '
103      'dependencies.')
104
105  parser.add_option('--stamp', help='File to touch on success')
106
107  options, positional_args = parser.parse_args(args)
108
109  if positional_args:
110    parser.error('No positional arguments should be given.')
111
112  # Check that required options have been provided.
113  required_options = (
114      'android_sdk_jar',
115      'aapt_path',
116      'android_manifest',
117      'dependencies_res_zips',
118      'resource_dirs',
119      'resource_zip_out',
120      )
121  build_utils.CheckOptions(options, parser, required=required_options)
122
123  if (options.R_dir is None) == (options.srcjar_out is None):
124    raise Exception('Exactly one of --R-dir or --srcjar-out must be specified.')
125
126  options.resource_dirs = build_utils.ParseGypList(options.resource_dirs)
127  options.dependencies_res_zips = (
128      build_utils.ParseGypList(options.dependencies_res_zips))
129
130  # Don't use [] as default value since some script explicitly pass "".
131  if options.extra_res_packages:
132    options.extra_res_packages = (
133        build_utils.ParseGypList(options.extra_res_packages))
134  else:
135    options.extra_res_packages = []
136
137  if options.extra_r_text_files:
138    options.extra_r_text_files = (
139        build_utils.ParseGypList(options.extra_r_text_files))
140  else:
141    options.extra_r_text_files = []
142
143  return options
144
145
146def CreateExtraRJavaFiles(
147      r_dir, extra_packages, extra_r_text_files, shared_resources, include_all):
148  if include_all:
149    java_files = build_utils.FindInDirectory(r_dir, "R.java")
150    if len(java_files) != 1:
151      return
152    r_java_file = java_files[0]
153    r_java_contents = codecs.open(r_java_file, encoding='utf-8').read()
154
155    for package in extra_packages:
156      package_r_java_dir = os.path.join(r_dir, *package.split('.'))
157      build_utils.MakeDirectory(package_r_java_dir)
158      package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
159      new_r_java = re.sub(r'package [.\w]*;', u'package %s;' % package,
160                          r_java_contents)
161      codecs.open(package_r_java_path, 'w', encoding='utf-8').write(new_r_java)
162  else:
163    if len(extra_packages) != len(extra_r_text_files):
164      raise Exception('Need one R.txt file per extra package')
165
166    r_txt_file = os.path.join(r_dir, 'R.txt')
167    if not os.path.exists(r_txt_file):
168      return
169
170    # Map of (resource_type, name) -> Entry.
171    # Contains the correct values for resources.
172    all_resources = {}
173    for entry in _ParseTextSymbolsFile(r_txt_file):
174      all_resources[(entry.resource_type, entry.name)] = entry
175
176    # Map of package_name->resource_type->entry
177    resources_by_package = (
178        collections.defaultdict(lambda: collections.defaultdict(list)))
179    # Build the R.java files using each package's R.txt file, but replacing
180    # each entry's placeholder value with correct values from all_resources.
181    for package, r_text_file in zip(extra_packages, extra_r_text_files):
182      if not os.path.exists(r_text_file):
183        continue
184      if package in resources_by_package:
185        raise Exception(('Package name "%s" appeared twice. All '
186                         'android_resources() targets must use unique package '
187                         'names, or no package name at all.') % package)
188      resources_by_type = resources_by_package[package]
189      # The sub-R.txt files have the wrong values at this point. Read them to
190      # figure out which entries belong to them, but use the values from the
191      # main R.txt file.
192      for entry in _ParseTextSymbolsFile(r_text_file):
193        entry = all_resources[(entry.resource_type, entry.name)]
194        resources_by_type[entry.resource_type].append(entry)
195
196    for package, resources_by_type in resources_by_package.iteritems():
197      package_r_java_dir = os.path.join(r_dir, *package.split('.'))
198      build_utils.MakeDirectory(package_r_java_dir)
199      package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
200      java_file_contents = _CreateExtraRJavaFile(
201          package, resources_by_type, shared_resources)
202      with open(package_r_java_path, 'w') as f:
203        f.write(java_file_contents)
204
205
206def _ParseTextSymbolsFile(path):
207  """Given an R.txt file, returns a list of TextSymbolsEntry."""
208  ret = []
209  with open(path) as f:
210    for line in f:
211      m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line)
212      if not m:
213        raise Exception('Unexpected line in R.txt: %s' % line)
214      java_type, resource_type, name, value = m.groups()
215      ret.append(TextSymbolsEntry(java_type, resource_type, name, value))
216  return ret
217
218
219def _CreateExtraRJavaFile(package, resources_by_type, shared_resources):
220  """Generates the contents of a R.java file."""
221  template = Template("""/* AUTO-GENERATED FILE.  DO NOT MODIFY. */
222
223package {{ package }};
224
225public final class R {
226    {% for resource_type in resources %}
227    public static final class {{ resource_type }} {
228        {% for e in resources[resource_type] %}
229        {% if shared_resources %}
230        public static {{ e.java_type }} {{ e.name }} = {{ e.value }};
231        {% else %}
232        public static final {{ e.java_type }} {{ e.name }} = {{ e.value }};
233        {% endif %}
234        {% endfor %}
235    }
236    {% endfor %}
237    {% if shared_resources %}
238    public static void onResourcesLoaded(int packageId) {
239        {% for resource_type in resources %}
240        {% for e in resources[resource_type] %}
241        {% if e.java_type == 'int[]' %}
242        for(int i = 0; i < {{ e.resource_type }}.{{ e.name }}.length; ++i) {
243            {{ e.resource_type }}.{{ e.name }}[i] =
244                    ({{ e.resource_type }}.{{ e.name }}[i] & 0x00ffffff)
245                    | (packageId << 24);
246        }
247        {% else %}
248        {{ e.resource_type }}.{{ e.name }} =
249                ({{ e.resource_type }}.{{ e.name }} & 0x00ffffff)
250                | (packageId << 24);
251        {% endif %}
252        {% endfor %}
253        {% endfor %}
254    }
255    {% endif %}
256}
257""", trim_blocks=True, lstrip_blocks=True)
258
259  return template.render(package=package, resources=resources_by_type,
260                         shared_resources=shared_resources)
261
262
263def CrunchDirectory(aapt, input_dir, output_dir):
264  """Crunches the images in input_dir and its subdirectories into output_dir.
265
266  If an image is already optimized, crunching often increases image size. In
267  this case, the crunched image is overwritten with the original image.
268  """
269  aapt_cmd = [aapt,
270              'crunch',
271              '-C', output_dir,
272              '-S', input_dir,
273              '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
274  build_utils.CheckOutput(aapt_cmd, stderr_filter=FilterCrunchStderr,
275                          fail_func=DidCrunchFail)
276
277  # Check for images whose size increased during crunching and replace them
278  # with their originals (except for 9-patches, which must be crunched).
279  for dir_, _, files in os.walk(output_dir):
280    for crunched in files:
281      if crunched.endswith('.9.png'):
282        continue
283      if not crunched.endswith('.png'):
284        raise Exception('Unexpected file in crunched dir: ' + crunched)
285      crunched = os.path.join(dir_, crunched)
286      original = os.path.join(input_dir, os.path.relpath(crunched, output_dir))
287      original_size = os.path.getsize(original)
288      crunched_size = os.path.getsize(crunched)
289      if original_size < crunched_size:
290        shutil.copyfile(original, crunched)
291
292
293def FilterCrunchStderr(stderr):
294  """Filters out lines from aapt crunch's stderr that can safely be ignored."""
295  filtered_lines = []
296  for line in stderr.splitlines(True):
297    # Ignore this libpng warning, which is a known non-error condition.
298    # http://crbug.com/364355
299    if ('libpng warning: iCCP: Not recognizing known sRGB profile that has '
300        + 'been edited' in line):
301      continue
302    filtered_lines.append(line)
303  return ''.join(filtered_lines)
304
305
306def DidCrunchFail(returncode, stderr):
307  """Determines whether aapt crunch failed from its return code and output.
308
309  Because aapt's return code cannot be trusted, any output to stderr is
310  an indication that aapt has failed (http://crbug.com/314885).
311  """
312  return returncode != 0 or stderr
313
314
315def ZipResources(resource_dirs, zip_path):
316  # Python zipfile does not provide a way to replace a file (it just writes
317  # another file with the same name). So, first collect all the files to put
318  # in the zip (with proper overriding), and then zip them.
319  files_to_zip = dict()
320  for d in resource_dirs:
321    for root, _, files in os.walk(d):
322      for f in files:
323        archive_path = f
324        parent_dir = os.path.relpath(root, d)
325        if parent_dir != '.':
326          archive_path = os.path.join(parent_dir, f)
327        path = os.path.join(root, f)
328        files_to_zip[archive_path] = path
329  build_utils.DoZip(files_to_zip.iteritems(), zip_path)
330
331
332def CombineZips(zip_files, output_path):
333  # When packaging resources, if the top-level directories in the zip file are
334  # of the form 0, 1, ..., then each subdirectory will be passed to aapt as a
335  # resources directory. While some resources just clobber others (image files,
336  # etc), other resources (particularly .xml files) need to be more
337  # intelligently merged. That merging is left up to aapt.
338  def path_transform(name, src_zip):
339    return '%d/%s' % (zip_files.index(src_zip), name)
340
341  build_utils.MergeZips(output_path, zip_files, path_transform=path_transform)
342
343
344def _OnStaleMd5(options):
345  aapt = options.aapt_path
346  with build_utils.TempDir() as temp_dir:
347    deps_dir = os.path.join(temp_dir, 'deps')
348    build_utils.MakeDirectory(deps_dir)
349    v14_dir = os.path.join(temp_dir, 'v14')
350    build_utils.MakeDirectory(v14_dir)
351
352    gen_dir = os.path.join(temp_dir, 'gen')
353    build_utils.MakeDirectory(gen_dir)
354
355    input_resource_dirs = options.resource_dirs
356
357    if not options.v14_skip:
358      for resource_dir in input_resource_dirs:
359        generate_v14_compatible_resources.GenerateV14Resources(
360            resource_dir,
361            v14_dir)
362
363    dep_zips = options.dependencies_res_zips
364    dep_subdirs = []
365    for z in dep_zips:
366      subdir = os.path.join(deps_dir, os.path.basename(z))
367      if os.path.exists(subdir):
368        raise Exception('Resource zip name conflict: ' + os.path.basename(z))
369      build_utils.ExtractAll(z, path=subdir)
370      dep_subdirs.append(subdir)
371
372    # Generate R.java. This R.java contains non-final constants and is used only
373    # while compiling the library jar (e.g. chromium_content.jar). When building
374    # an apk, a new R.java file with the correct resource -> ID mappings will be
375    # generated by merging the resources from all libraries and the main apk
376    # project.
377    package_command = [aapt,
378                       'package',
379                       '-m',
380                       '-M', options.android_manifest,
381                       '--auto-add-overlay',
382                       '--no-version-vectors',
383                       '-I', options.android_sdk_jar,
384                       '--output-text-symbols', gen_dir,
385                       '-J', gen_dir,
386                       '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
387
388    for d in input_resource_dirs:
389      package_command += ['-S', d]
390
391    for d in dep_subdirs:
392      package_command += ['-S', d]
393
394    if options.non_constant_id:
395      package_command.append('--non-constant-id')
396    if options.custom_package:
397      package_command += ['--custom-package', options.custom_package]
398    if options.proguard_file:
399      package_command += ['-G', options.proguard_file]
400    if options.shared_resources:
401      package_command.append('--shared-lib')
402    if options.app_as_shared_lib:
403      package_command.append('--app-as-shared-lib')
404    build_utils.CheckOutput(package_command, print_stderr=False)
405
406    if options.extra_res_packages:
407      CreateExtraRJavaFiles(
408          gen_dir,
409          options.extra_res_packages,
410          options.extra_r_text_files,
411          options.shared_resources or options.app_as_shared_lib,
412          options.include_all_resources)
413
414    # This is the list of directories with resources to put in the final .zip
415    # file. The order of these is important so that crunched/v14 resources
416    # override the normal ones.
417    zip_resource_dirs = input_resource_dirs + [v14_dir]
418
419    base_crunch_dir = os.path.join(temp_dir, 'crunch')
420
421    # Crunch image resources. This shrinks png files and is necessary for
422    # 9-patch images to display correctly. 'aapt crunch' accepts only a single
423    # directory at a time and deletes everything in the output directory.
424    for idx, input_dir in enumerate(input_resource_dirs):
425      crunch_dir = os.path.join(base_crunch_dir, str(idx))
426      build_utils.MakeDirectory(crunch_dir)
427      zip_resource_dirs.append(crunch_dir)
428      CrunchDirectory(aapt, input_dir, crunch_dir)
429
430    ZipResources(zip_resource_dirs, options.resource_zip_out)
431
432    if options.all_resources_zip_out:
433      CombineZips([options.resource_zip_out] + dep_zips,
434                  options.all_resources_zip_out)
435
436    if options.R_dir:
437      build_utils.DeleteDirectory(options.R_dir)
438      shutil.copytree(gen_dir, options.R_dir)
439    else:
440      build_utils.ZipDir(options.srcjar_out, gen_dir)
441
442    if options.r_text_out:
443      r_text_path = os.path.join(gen_dir, 'R.txt')
444      if os.path.exists(r_text_path):
445        shutil.copyfile(r_text_path, options.r_text_out)
446      else:
447        open(options.r_text_out, 'w').close()
448
449
450def main(args):
451  args = build_utils.ExpandFileArgs(args)
452  options = _ParseArgs(args)
453
454  possible_output_paths = [
455    options.resource_zip_out,
456    options.all_resources_zip_out,
457    options.proguard_file,
458    options.r_text_out,
459    options.srcjar_out,
460  ]
461  output_paths = [x for x in possible_output_paths if x]
462
463  # List python deps in input_strings rather than input_paths since the contents
464  # of them does not change what gets written to the depsfile.
465  input_strings = options.extra_res_packages + [
466    options.app_as_shared_lib,
467    options.custom_package,
468    options.include_all_resources,
469    options.non_constant_id,
470    options.shared_resources,
471    options.v14_skip,
472  ]
473
474  input_paths = [
475    options.aapt_path,
476    options.android_manifest,
477    options.android_sdk_jar,
478  ]
479  input_paths.extend(options.dependencies_res_zips)
480  input_paths.extend(p for p in options.extra_r_text_files if os.path.exists(p))
481
482  resource_names = []
483  for resource_dir in options.resource_dirs:
484    for resource_file in build_utils.FindInDirectory(resource_dir, '*'):
485      input_paths.append(resource_file)
486      resource_names.append(os.path.relpath(resource_file, resource_dir))
487
488  # Resource filenames matter to the output, so add them to strings as well.
489  # This matters if a file is renamed but not changed (http://crbug.com/597126).
490  input_strings.extend(sorted(resource_names))
491
492  build_utils.CallAndWriteDepfileIfStale(
493      lambda: _OnStaleMd5(options),
494      options,
495      input_paths=input_paths,
496      input_strings=input_strings,
497      output_paths=output_paths,
498      # TODO(agrieve): Remove R_dir when it's no longer used (used only by GYP).
499      force=options.R_dir)
500
501
502if __name__ == '__main__':
503  main(sys.argv[1:])
504