Home | History | Annotate | Download | only in gyp
      1 #!/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 
     10 See https://android.googlesource.com/platform/tools/base/+/master/legacy/ant-tasks/src/main/java/com/android/ant/AaptExecTask.java
     11 and
     12 https://android.googlesource.com/platform/sdk/+/master/files/ant/build.xml
     13 """
     14 # pylint: enable=C0301
     15 
     16 import optparse
     17 import os
     18 import re
     19 import shutil
     20 import sys
     21 import zipfile
     22 
     23 from 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.
     34 DENSITY_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 
     75 def _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 
    138 def 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 
    164 def 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 
    185 def _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 
    192 def _GenerateLanguageSplitOutputPaths(apk_path, languages):
    193   for lang in languages:
    194     yield '%s_%s' % (apk_path, lang)
    195 
    196 
    197 def 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 
    203 def 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 
    220 def _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 
    263 def _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 
    285 def 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 
    324 if __name__ == '__main__':
    325   main(sys.argv[1:])
    326