Home | History | Annotate | Download | only in gyp
      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 
      9 This will crunch images and generate v14 compatible resources
     10 (see generate_v14_compatible_resources.py).
     11 """
     12 
     13 import optparse
     14 import os
     15 import re
     16 import shutil
     17 import sys
     18 import zipfile
     19 
     20 import generate_v14_compatible_resources
     21 
     22 from util import build_utils
     23 
     24 def ParseArgs(args):
     25   """Parses command line options.
     26 
     27   Returns:
     28     An options object as from optparse.OptionsParser.parse_args()
     29   """
     30   parser = optparse.OptionParser()
     31   build_utils.AddDepfileOption(parser)
     32 
     33   parser.add_option('--android-sdk', help='path to the Android SDK folder')
     34   parser.add_option('--android-sdk-tools',
     35                     help='path to the Android SDK build tools folder')
     36   parser.add_option('--non-constant-id', action='store_true')
     37 
     38   parser.add_option('--android-manifest', help='AndroidManifest.xml path')
     39   parser.add_option('--custom-package', help='Java package for R.java')
     40 
     41   parser.add_option('--resource-dirs',
     42                     help='Directories containing resources of this target.')
     43   parser.add_option('--dependencies-res-zips',
     44                     help='Resources from dependents.')
     45 
     46   parser.add_option('--resource-zip-out',
     47                     help='Path for output zipped resources.')
     48 
     49   parser.add_option('--R-dir',
     50                     help='directory to hold generated R.java.')
     51   parser.add_option('--srcjar-out',
     52                     help='Path to srcjar to contain generated R.java.')
     53 
     54   parser.add_option('--proguard-file',
     55                     help='Path to proguard.txt generated file')
     56 
     57   parser.add_option(
     58       '--v14-verify-only',
     59       action='store_true',
     60       help='Do not generate v14 resources. Instead, just verify that the '
     61       'resources are already compatible with v14, i.e. they don\'t use '
     62       'attributes that cause crashes on certain devices.')
     63 
     64   parser.add_option(
     65       '--extra-res-packages',
     66       help='Additional package names to generate R.java files for')
     67   # TODO(cjhopman): Actually use --extra-r-text-files. We currently include all
     68   # the resources in all R.java files for a particular apk.
     69   parser.add_option(
     70       '--extra-r-text-files',
     71       help='For each additional package, the R.txt file should contain a '
     72       'list of resources to be included in the R.java file in the format '
     73       'generated by aapt')
     74 
     75   parser.add_option(
     76       '--all-resources-zip-out',
     77       help='Path for output of all resources. This includes resources in '
     78       'dependencies.')
     79 
     80   parser.add_option('--stamp', help='File to touch on success')
     81 
     82   (options, args) = parser.parse_args(args)
     83 
     84   if args:
     85     parser.error('No positional arguments should be given.')
     86 
     87   # Check that required options have been provided.
     88   required_options = (
     89       'android_sdk',
     90       'android_sdk_tools',
     91       'android_manifest',
     92       'dependencies_res_zips',
     93       'resource_dirs',
     94       'resource_zip_out',
     95       )
     96   build_utils.CheckOptions(options, parser, required=required_options)
     97 
     98   if (options.R_dir is None) == (options.srcjar_out is None):
     99     raise Exception('Exactly one of --R-dir or --srcjar-out must be specified.')
    100 
    101   return options
    102 
    103 
    104 def CreateExtraRJavaFiles(r_dir, extra_packages):
    105   java_files = build_utils.FindInDirectory(r_dir, "R.java")
    106   if len(java_files) != 1:
    107     return
    108   r_java_file = java_files[0]
    109   r_java_contents = open(r_java_file).read()
    110 
    111   for package in extra_packages:
    112     package_r_java_dir = os.path.join(r_dir, *package.split('.'))
    113     build_utils.MakeDirectory(package_r_java_dir)
    114     package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
    115     open(package_r_java_path, 'w').write(
    116         re.sub(r'package [.\w]*;', 'package %s;' % package, r_java_contents))
    117     # TODO(cjhopman): These extra package's R.java files should be filtered to
    118     # only contain the resources listed in their R.txt files. At this point, we
    119     # have already compiled those other libraries, so doing this would only
    120     # affect how the code in this .apk target could refer to the resources.
    121 
    122 
    123 def DidCrunchFail(returncode, stderr):
    124   """Determines whether aapt crunch failed from its return code and output.
    125 
    126   Because aapt's return code cannot be trusted, any output to stderr is
    127   an indication that aapt has failed (http://crbug.com/314885), except
    128   lines that contain "libpng warning", which is a known non-error condition
    129   (http://crbug.com/364355).
    130   """
    131   if returncode != 0:
    132     return True
    133   for line in stderr.splitlines():
    134     if line and not 'libpng warning' in line:
    135       return True
    136   return False
    137 
    138 
    139 def ZipResources(resource_dirs, zip_path):
    140   # Python zipfile does not provide a way to replace a file (it just writes
    141   # another file with the same name). So, first collect all the files to put
    142   # in the zip (with proper overriding), and then zip them.
    143   files_to_zip = dict()
    144   for d in resource_dirs:
    145     for root, _, files in os.walk(d):
    146       for f in files:
    147         archive_path = os.path.join(os.path.relpath(root, d), f)
    148         path = os.path.join(root, f)
    149         files_to_zip[archive_path] = path
    150   with zipfile.ZipFile(zip_path, 'w') as outzip:
    151     for archive_path, path in files_to_zip.iteritems():
    152       outzip.write(path, archive_path)
    153 
    154 
    155 def CombineZips(zip_files, output_path):
    156   # When packaging resources, if the top-level directories in the zip file are
    157   # of the form 0, 1, ..., then each subdirectory will be passed to aapt as a
    158   # resources directory. While some resources just clobber others (image files,
    159   # etc), other resources (particularly .xml files) need to be more
    160   # intelligently merged. That merging is left up to aapt.
    161   with zipfile.ZipFile(output_path, 'w') as outzip:
    162     for i, z in enumerate(zip_files):
    163       with zipfile.ZipFile(z, 'r') as inzip:
    164         for name in inzip.namelist():
    165           new_name = '%d/%s' % (i, name)
    166           outzip.writestr(new_name, inzip.read(name))
    167 
    168 
    169 def main():
    170   args = build_utils.ExpandFileArgs(sys.argv[1:])
    171 
    172   options = ParseArgs(args)
    173   android_jar = os.path.join(options.android_sdk, 'android.jar')
    174   aapt = os.path.join(options.android_sdk_tools, 'aapt')
    175 
    176   input_files = []
    177 
    178   with build_utils.TempDir() as temp_dir:
    179     deps_dir = os.path.join(temp_dir, 'deps')
    180     build_utils.MakeDirectory(deps_dir)
    181     v14_dir = os.path.join(temp_dir, 'v14')
    182     build_utils.MakeDirectory(v14_dir)
    183 
    184     gen_dir = os.path.join(temp_dir, 'gen')
    185     build_utils.MakeDirectory(gen_dir)
    186 
    187     input_resource_dirs = build_utils.ParseGypList(options.resource_dirs)
    188 
    189     for resource_dir in input_resource_dirs:
    190       generate_v14_compatible_resources.GenerateV14Resources(
    191           resource_dir,
    192           v14_dir,
    193           options.v14_verify_only)
    194 
    195     dep_zips = build_utils.ParseGypList(options.dependencies_res_zips)
    196     input_files += dep_zips
    197     dep_subdirs = []
    198     for z in dep_zips:
    199       subdir = os.path.join(deps_dir, os.path.basename(z))
    200       if os.path.exists(subdir):
    201         raise Exception('Resource zip name conflict: ' + os.path.basename(z))
    202       build_utils.ExtractAll(z, path=subdir)
    203       dep_subdirs.append(subdir)
    204 
    205     # Generate R.java. This R.java contains non-final constants and is used only
    206     # while compiling the library jar (e.g. chromium_content.jar). When building
    207     # an apk, a new R.java file with the correct resource -> ID mappings will be
    208     # generated by merging the resources from all libraries and the main apk
    209     # project.
    210     package_command = [aapt,
    211                        'package',
    212                        '-m',
    213                        '-M', options.android_manifest,
    214                        '--auto-add-overlay',
    215                        '-I', android_jar,
    216                        '--output-text-symbols', gen_dir,
    217                        '-J', gen_dir]
    218 
    219     for d in input_resource_dirs:
    220       package_command += ['-S', d]
    221 
    222     for d in dep_subdirs:
    223       package_command += ['-S', d]
    224 
    225     if options.non_constant_id:
    226       package_command.append('--non-constant-id')
    227     if options.custom_package:
    228       package_command += ['--custom-package', options.custom_package]
    229     if options.proguard_file:
    230       package_command += ['-G', options.proguard_file]
    231     build_utils.CheckOutput(package_command, print_stderr=False)
    232 
    233     if options.extra_res_packages:
    234       CreateExtraRJavaFiles(
    235           gen_dir,
    236           build_utils.ParseGypList(options.extra_res_packages))
    237 
    238     # This is the list of directories with resources to put in the final .zip
    239     # file. The order of these is important so that crunched/v14 resources
    240     # override the normal ones.
    241     zip_resource_dirs = input_resource_dirs + [v14_dir]
    242 
    243     base_crunch_dir = os.path.join(temp_dir, 'crunch')
    244 
    245     # Crunch image resources. This shrinks png files and is necessary for
    246     # 9-patch images to display correctly. 'aapt crunch' accepts only a single
    247     # directory at a time and deletes everything in the output directory.
    248     for idx, d in enumerate(input_resource_dirs):
    249       crunch_dir = os.path.join(base_crunch_dir, str(idx))
    250       build_utils.MakeDirectory(crunch_dir)
    251       zip_resource_dirs.append(crunch_dir)
    252       aapt_cmd = [aapt,
    253                   'crunch',
    254                   '-C', crunch_dir,
    255                   '-S', d]
    256       build_utils.CheckOutput(aapt_cmd, fail_func=DidCrunchFail)
    257 
    258     ZipResources(zip_resource_dirs, options.resource_zip_out)
    259 
    260     if options.all_resources_zip_out:
    261       CombineZips([options.resource_zip_out] + dep_zips,
    262                   options.all_resources_zip_out)
    263 
    264     if options.R_dir:
    265       build_utils.DeleteDirectory(options.R_dir)
    266       shutil.copytree(gen_dir, options.R_dir)
    267     else:
    268       build_utils.ZipDir(options.srcjar_out, gen_dir)
    269 
    270   if options.depfile:
    271     input_files += build_utils.GetPythonDependencies()
    272     build_utils.WriteDepfile(options.depfile, input_files)
    273 
    274   if options.stamp:
    275     build_utils.Touch(options.stamp)
    276 
    277 
    278 if __name__ == '__main__':
    279   main()
    280