Home | History | Annotate | Download | only in gyp
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2013 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 """Convert Android xml resources to API 14 compatible.
      8 
      9 There are two reasons that we cannot just use API 17 attributes,
     10 so we are generating another set of resources by this script.
     11 
     12 1. paddingStart attribute can cause a crash on Galaxy Tab 2.
     13 2. There is a bug that paddingStart does not override paddingLeft on
     14    JB-MR1. This is fixed on JB-MR2.
     15 
     16 Therefore, this resource generation script can be removed when
     17 we drop the support for JB-MR1.
     18 
     19 Please refer to http://crbug.com/235118 for the details.
     20 """
     21 
     22 import optparse
     23 import os
     24 import re
     25 import shutil
     26 import sys
     27 import xml.dom.minidom as minidom
     28 
     29 from util import build_utils
     30 
     31 # Note that we are assuming 'android:' is an alias of
     32 # the namespace 'http://schemas.android.com/apk/res/android'.
     33 
     34 GRAVITY_ATTRIBUTES = ('android:gravity', 'android:layout_gravity')
     35 
     36 # Almost all the attributes that has "Start" or "End" in
     37 # its name should be mapped.
     38 ATTRIBUTES_TO_MAP = {'paddingStart' : 'paddingLeft',
     39                      'drawableStart' : 'drawableLeft',
     40                      'layout_alignStart' : 'layout_alignLeft',
     41                      'layout_marginStart' : 'layout_marginLeft',
     42                      'layout_alignParentStart' : 'layout_alignParentLeft',
     43                      'layout_toStartOf' : 'layout_toLeftOf',
     44                      'paddingEnd' : 'paddingRight',
     45                      'drawableEnd' : 'drawableRight',
     46                      'layout_alignEnd' : 'layout_alignRight',
     47                      'layout_marginEnd' : 'layout_marginRight',
     48                      'layout_alignParentEnd' : 'layout_alignParentRight',
     49                      'layout_toEndOf' : 'layout_toRightOf'}
     50 
     51 ATTRIBUTES_TO_MAP = dict(['android:' + k, 'android:' + v] for k, v
     52                          in ATTRIBUTES_TO_MAP.iteritems())
     53 
     54 ATTRIBUTES_TO_MAP_REVERSED = dict([v, k] for k, v
     55                                   in ATTRIBUTES_TO_MAP.iteritems())
     56 
     57 
     58 def IterateXmlElements(node):
     59   """minidom helper function that iterates all the element nodes.
     60   Iteration order is pre-order depth-first."""
     61   if node.nodeType == node.ELEMENT_NODE:
     62     yield node
     63   for child_node in node.childNodes:
     64     for child_node_element in IterateXmlElements(child_node):
     65       yield child_node_element
     66 
     67 
     68 def AssertNotDeprecatedAttribute(name, value, filename):
     69   """Raises an exception if the given attribute is deprecated."""
     70   msg = None
     71   if name in ATTRIBUTES_TO_MAP_REVERSED:
     72     msg = '{0} should use {1} instead of {2}'.format(filename,
     73         ATTRIBUTES_TO_MAP_REVERSED[name], name)
     74   elif name in GRAVITY_ATTRIBUTES and ('left' in value or 'right' in value):
     75     msg = '{0} should use start/end instead of left/right for {1}'.format(
     76         filename, name)
     77 
     78   if msg:
     79     msg += ('\nFor background, see: http://android-developers.blogspot.com/'
     80             '2013/03/native-rtl-support-in-android-42.html\n'
     81             'If you have a legitimate need for this attribute, discuss with '
     82             'kkimlabs (at] chromium.org or newt (at] chromium.org')
     83     raise Exception(msg)
     84 
     85 
     86 def WriteDomToFile(dom, filename):
     87   """Write the given dom to filename."""
     88   build_utils.MakeDirectory(os.path.dirname(filename))
     89   with open(filename, 'w') as f:
     90     dom.writexml(f, '', '  ', '\n', encoding='utf-8')
     91 
     92 
     93 def HasStyleResource(dom):
     94   """Return True if the dom is a style resource, False otherwise."""
     95   root_node = IterateXmlElements(dom).next()
     96   return bool(root_node.nodeName == 'resources' and
     97               list(root_node.getElementsByTagName('style')))
     98 
     99 
    100 def ErrorIfStyleResourceExistsInDir(input_dir):
    101   """If a style resource is in input_dir, raises an exception."""
    102   for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
    103     dom = minidom.parse(input_filename)
    104     if HasStyleResource(dom):
    105       raise Exception('error: style file ' + input_filename +
    106                       ' should be under ' + input_dir +
    107                       '-v17 directory. Please refer to '
    108                       'http://crbug.com/243952 for the details.')
    109 
    110 
    111 def GenerateV14LayoutResourceDom(dom, filename, assert_not_deprecated=True):
    112   """Convert layout resource to API 14 compatible layout resource.
    113 
    114   Args:
    115     dom: Parsed minidom object to be modified.
    116     filename: Filename that the DOM was parsed from.
    117     assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
    118                            cause an exception to be thrown.
    119 
    120   Returns:
    121     True if dom is modified, False otherwise.
    122   """
    123   is_modified = False
    124 
    125   # Iterate all the elements' attributes to find attributes to convert.
    126   for element in IterateXmlElements(dom):
    127     for name, value in list(element.attributes.items()):
    128       # Convert any API 17 Start/End attributes to Left/Right attributes.
    129       # For example, from paddingStart="10dp" to paddingLeft="10dp"
    130       # Note: gravity attributes are not necessary to convert because
    131       # start/end values are backward-compatible. Explained at
    132       # https://plus.sandbox.google.com/+RomanNurik/posts/huuJd8iVVXY?e=Showroom
    133       if name in ATTRIBUTES_TO_MAP:
    134         element.setAttribute(ATTRIBUTES_TO_MAP[name], value)
    135         del element.attributes[name]
    136         is_modified = True
    137       elif assert_not_deprecated:
    138         AssertNotDeprecatedAttribute(name, value, filename)
    139 
    140   return is_modified
    141 
    142 
    143 def GenerateV14StyleResourceDom(dom, filename, assert_not_deprecated=True):
    144   """Convert style resource to API 14 compatible style resource.
    145 
    146   Args:
    147     dom: Parsed minidom object to be modified.
    148     filename: Filename that the DOM was parsed from.
    149     assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
    150                            cause an exception to be thrown.
    151 
    152   Returns:
    153     True if dom is modified, False otherwise.
    154   """
    155   is_modified = False
    156 
    157   for style_element in dom.getElementsByTagName('style'):
    158     for item_element in style_element.getElementsByTagName('item'):
    159       name = item_element.attributes['name'].value
    160       value = item_element.childNodes[0].nodeValue
    161       if name in ATTRIBUTES_TO_MAP:
    162         item_element.attributes['name'].value = ATTRIBUTES_TO_MAP[name]
    163         is_modified = True
    164       elif assert_not_deprecated:
    165         AssertNotDeprecatedAttribute(name, value, filename)
    166 
    167   return is_modified
    168 
    169 
    170 def GenerateV14LayoutResource(input_filename, output_v14_filename,
    171                               output_v17_filename):
    172   """Convert API 17 layout resource to API 14 compatible layout resource.
    173 
    174   It's mostly a simple replacement, s/Start/Left s/End/Right,
    175   on the attribute names.
    176   If the generated resource is identical to the original resource,
    177   don't do anything. If not, write the generated resource to
    178   output_v14_filename, and copy the original resource to output_v17_filename.
    179   """
    180   dom = minidom.parse(input_filename)
    181   is_modified = GenerateV14LayoutResourceDom(dom, input_filename)
    182 
    183   if is_modified:
    184     # Write the generated resource.
    185     WriteDomToFile(dom, output_v14_filename)
    186 
    187     # Copy the original resource.
    188     build_utils.MakeDirectory(os.path.dirname(output_v17_filename))
    189     shutil.copy2(input_filename, output_v17_filename)
    190 
    191 
    192 def GenerateV14StyleResource(input_filename, output_v14_filename):
    193   """Convert API 17 style resources to API 14 compatible style resource.
    194 
    195   Write the generated style resource to output_v14_filename.
    196   It's mostly a simple replacement, s/Start/Left s/End/Right,
    197   on the attribute names.
    198   """
    199   dom = minidom.parse(input_filename)
    200   GenerateV14StyleResourceDom(dom, input_filename)
    201 
    202   # Write the generated resource.
    203   WriteDomToFile(dom, output_v14_filename)
    204 
    205 
    206 def GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, output_v17_dir):
    207   """Convert layout resources to API 14 compatible resources in input_dir."""
    208   for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
    209     rel_filename = os.path.relpath(input_filename, input_dir)
    210     output_v14_filename = os.path.join(output_v14_dir, rel_filename)
    211     output_v17_filename = os.path.join(output_v17_dir, rel_filename)
    212     GenerateV14LayoutResource(input_filename, output_v14_filename,
    213                               output_v17_filename)
    214 
    215 
    216 def GenerateV14StyleResourcesInDir(input_dir, output_v14_dir):
    217   """Convert style resources to API 14 compatible resources in input_dir."""
    218   for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
    219     rel_filename = os.path.relpath(input_filename, input_dir)
    220     output_v14_filename = os.path.join(output_v14_dir, rel_filename)
    221     GenerateV14StyleResource(input_filename, output_v14_filename)
    222 
    223 
    224 def VerifyV14ResourcesInDir(input_dir, resource_type):
    225   """Verify that the resources in input_dir is compatible with v14, i.e., they
    226   don't use attributes that cause crashes on certain devices. Print an error if
    227   they have."""
    228   for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
    229     exception_message = ('error : ' + input_filename + ' has an RTL attribute, '
    230                         'i.e., attribute that has "start" or "end" in its name.'
    231                         ' Pre-v17 resources should not include it because it '
    232                         'can cause crashes on certain devices. Please refer to '
    233                         'http://crbug.com/243952 for the details.')
    234     dom = minidom.parse(input_filename)
    235     if resource_type in ('layout', 'xml'):
    236       if GenerateV14LayoutResourceDom(dom, input_filename, False):
    237         raise Exception(exception_message)
    238     elif resource_type == 'values':
    239       if GenerateV14StyleResourceDom(dom, input_filename, False):
    240         raise Exception(exception_message)
    241 
    242 
    243 def AssertNoDeprecatedAttributesInDir(input_dir, resource_type):
    244   """Raises an exception if resources in input_dir have deprecated attributes,
    245   e.g., paddingLeft, paddingRight"""
    246   for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
    247     dom = minidom.parse(input_filename)
    248     if resource_type in ('layout', 'xml'):
    249       GenerateV14LayoutResourceDom(dom, input_filename)
    250     elif resource_type == 'values':
    251       GenerateV14StyleResourceDom(dom, input_filename)
    252 
    253 
    254 def ParseArgs():
    255   """Parses command line options.
    256 
    257   Returns:
    258     An options object as from optparse.OptionsParser.parse_args()
    259   """
    260   parser = optparse.OptionParser()
    261   parser.add_option('--res-dir',
    262                     help='directory containing resources '
    263                          'used to generate v14 compatible resources')
    264   parser.add_option('--res-v14-compatibility-dir',
    265                     help='output directory into which '
    266                          'v14 compatible resources will be generated')
    267   parser.add_option('--stamp', help='File to touch on success')
    268   parser.add_option('--verify-only', action="store_true", help='Do not generate'
    269       ' v14 resources. Instead, just verify that the resources are already '
    270       "compatible with v14, i.e. they don't use attributes that cause crashes "
    271       'on certain devices.')
    272 
    273   options, args = parser.parse_args()
    274 
    275   if args:
    276     parser.error('No positional arguments should be given.')
    277 
    278   # Check that required options have been provided.
    279   required_options = ('res_dir', 'res_v14_compatibility_dir')
    280   build_utils.CheckOptions(options, parser, required=required_options)
    281   return options
    282 
    283 def GenerateV14Resources(res_dir, res_v14_dir, verify_only):
    284   for name in os.listdir(res_dir):
    285     if not os.path.isdir(os.path.join(res_dir, name)):
    286       continue
    287 
    288     dir_pieces = name.split('-')
    289     resource_type = dir_pieces[0]
    290     qualifiers = dir_pieces[1:]
    291 
    292     api_level_qualifier_index = -1
    293     api_level_qualifier = ''
    294     for index, qualifier in enumerate(qualifiers):
    295       if re.match('v[0-9]+$', qualifier):
    296         api_level_qualifier_index = index
    297         api_level_qualifier = qualifier
    298         break
    299 
    300     # Android pre-v17 API doesn't support RTL. Skip.
    301     if 'ldrtl' in qualifiers:
    302       continue
    303 
    304     input_dir = os.path.abspath(os.path.join(res_dir, name))
    305 
    306     if verify_only:
    307       if not api_level_qualifier or int(api_level_qualifier[1:]) < 17:
    308         VerifyV14ResourcesInDir(input_dir, resource_type)
    309       else:
    310         AssertNoDeprecatedAttributesInDir(input_dir, resource_type)
    311     else:
    312       # We also need to copy the original v17 resource to *-v17 directory
    313       # because the generated v14 resource will hide the original resource.
    314       output_v14_dir = os.path.join(res_v14_dir, name)
    315       output_v17_dir = os.path.join(res_v14_dir, name + '-v17')
    316 
    317       # We only convert layout resources under layout*/, xml*/,
    318       # and style resources under values*/.
    319       if resource_type in ('layout', 'xml'):
    320         if not api_level_qualifier:
    321           GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir,
    322                                           output_v17_dir)
    323       elif resource_type == 'values':
    324         if api_level_qualifier == 'v17':
    325           output_qualifiers = qualifiers[:]
    326           del output_qualifiers[api_level_qualifier_index]
    327           output_v14_dir = os.path.join(res_v14_dir,
    328                                         '-'.join([resource_type] +
    329                                                  output_qualifiers))
    330           GenerateV14StyleResourcesInDir(input_dir, output_v14_dir)
    331         elif not api_level_qualifier:
    332           ErrorIfStyleResourceExistsInDir(input_dir)
    333 
    334 def main():
    335   options = ParseArgs()
    336 
    337   res_v14_dir = options.res_v14_compatibility_dir
    338 
    339   build_utils.DeleteDirectory(res_v14_dir)
    340   build_utils.MakeDirectory(res_v14_dir)
    341 
    342   GenerateV14Resources(options.res_dir, res_v14_dir, options.verify_only)
    343 
    344   if options.stamp:
    345     build_utils.Touch(options.stamp)
    346 
    347 if __name__ == '__main__':
    348   sys.exit(main())
    349 
    350