Add ICU message format support
[chromium-blink-merge.git] / build / android / gyp / generate_v14_compatible_resources.py
blob1a9e17b4c8effa01db01d30b61c767b7e69e89ec
1 #!/usr/bin/env python
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.
7 """Convert Android xml resources to API 14 compatible.
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.
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. b/8654490
16 Therefore, this resource generation script can be removed when
17 we drop the support for JB-MR1.
19 Please refer to http://crbug.com/235118 for the details.
20 """
22 import codecs
23 import optparse
24 import os
25 import re
26 import shutil
27 import sys
28 import xml.dom.minidom as minidom
30 from util import build_utils
32 # Note that we are assuming 'android:' is an alias of
33 # the namespace 'http://schemas.android.com/apk/res/android'.
35 GRAVITY_ATTRIBUTES = ('android:gravity', 'android:layout_gravity')
37 # Almost all the attributes that has "Start" or "End" in
38 # its name should be mapped.
39 ATTRIBUTES_TO_MAP = {'paddingStart' : 'paddingLeft',
40 'drawableStart' : 'drawableLeft',
41 'layout_alignStart' : 'layout_alignLeft',
42 'layout_marginStart' : 'layout_marginLeft',
43 'layout_alignParentStart' : 'layout_alignParentLeft',
44 'layout_toStartOf' : 'layout_toLeftOf',
45 'paddingEnd' : 'paddingRight',
46 'drawableEnd' : 'drawableRight',
47 'layout_alignEnd' : 'layout_alignRight',
48 'layout_marginEnd' : 'layout_marginRight',
49 'layout_alignParentEnd' : 'layout_alignParentRight',
50 'layout_toEndOf' : 'layout_toRightOf'}
52 ATTRIBUTES_TO_MAP = dict(['android:' + k, 'android:' + v] for k, v
53 in ATTRIBUTES_TO_MAP.iteritems())
55 ATTRIBUTES_TO_MAP_REVERSED = dict([v, k] for k, v
56 in ATTRIBUTES_TO_MAP.iteritems())
59 def IterateXmlElements(node):
60 """minidom helper function that iterates all the element nodes.
61 Iteration order is pre-order depth-first."""
62 if node.nodeType == node.ELEMENT_NODE:
63 yield node
64 for child_node in node.childNodes:
65 for child_node_element in IterateXmlElements(child_node):
66 yield child_node_element
69 def ParseAndReportErrors(filename):
70 try:
71 return minidom.parse(filename)
72 except Exception:
73 import traceback
74 traceback.print_exc()
75 sys.stderr.write('Failed to parse XML file: %s\n' % filename)
76 sys.exit(1)
79 def AssertNotDeprecatedAttribute(name, value, filename):
80 """Raises an exception if the given attribute is deprecated."""
81 msg = None
82 if name in ATTRIBUTES_TO_MAP_REVERSED:
83 msg = '{0} should use {1} instead of {2}'.format(filename,
84 ATTRIBUTES_TO_MAP_REVERSED[name], name)
85 elif name in GRAVITY_ATTRIBUTES and ('left' in value or 'right' in value):
86 msg = '{0} should use start/end instead of left/right for {1}'.format(
87 filename, name)
89 if msg:
90 msg += ('\nFor background, see: http://android-developers.blogspot.com/'
91 '2013/03/native-rtl-support-in-android-42.html\n'
92 'If you have a legitimate need for this attribute, discuss with '
93 'kkimlabs@chromium.org or newt@chromium.org')
94 raise Exception(msg)
97 def WriteDomToFile(dom, filename):
98 """Write the given dom to filename."""
99 build_utils.MakeDirectory(os.path.dirname(filename))
100 with codecs.open(filename, 'w', 'utf-8') as f:
101 dom.writexml(f, '', ' ', '\n', encoding='utf-8')
104 def HasStyleResource(dom):
105 """Return True if the dom is a style resource, False otherwise."""
106 root_node = IterateXmlElements(dom).next()
107 return bool(root_node.nodeName == 'resources' and
108 list(root_node.getElementsByTagName('style')))
111 def ErrorIfStyleResourceExistsInDir(input_dir):
112 """If a style resource is in input_dir, raises an exception."""
113 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
114 dom = ParseAndReportErrors(input_filename)
115 if HasStyleResource(dom):
116 # Allow style file in third_party to exist in non-v17 directories so long
117 # as they do not contain deprecated attributes.
118 if not 'third_party' in input_dir or (
119 GenerateV14StyleResourceDom(dom, input_filename)):
120 raise Exception('error: style file ' + input_filename +
121 ' should be under ' + input_dir +
122 '-v17 directory. Please refer to '
123 'http://crbug.com/243952 for the details.')
126 def GenerateV14LayoutResourceDom(dom, filename, assert_not_deprecated=True):
127 """Convert layout resource to API 14 compatible layout resource.
129 Args:
130 dom: Parsed minidom object to be modified.
131 filename: Filename that the DOM was parsed from.
132 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
133 cause an exception to be thrown.
135 Returns:
136 True if dom is modified, False otherwise.
138 is_modified = False
140 # Iterate all the elements' attributes to find attributes to convert.
141 for element in IterateXmlElements(dom):
142 for name, value in list(element.attributes.items()):
143 # Convert any API 17 Start/End attributes to Left/Right attributes.
144 # For example, from paddingStart="10dp" to paddingLeft="10dp"
145 # Note: gravity attributes are not necessary to convert because
146 # start/end values are backward-compatible. Explained at
147 # https://plus.sandbox.google.com/+RomanNurik/posts/huuJd8iVVXY?e=Showroom
148 if name in ATTRIBUTES_TO_MAP:
149 element.setAttribute(ATTRIBUTES_TO_MAP[name], value)
150 del element.attributes[name]
151 is_modified = True
152 elif assert_not_deprecated:
153 AssertNotDeprecatedAttribute(name, value, filename)
155 return is_modified
158 def GenerateV14StyleResourceDom(dom, filename, assert_not_deprecated=True):
159 """Convert style resource to API 14 compatible style resource.
161 Args:
162 dom: Parsed minidom object to be modified.
163 filename: Filename that the DOM was parsed from.
164 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
165 cause an exception to be thrown.
167 Returns:
168 True if dom is modified, False otherwise.
170 is_modified = False
172 for style_element in dom.getElementsByTagName('style'):
173 for item_element in style_element.getElementsByTagName('item'):
174 name = item_element.attributes['name'].value
175 value = item_element.childNodes[0].nodeValue
176 if name in ATTRIBUTES_TO_MAP:
177 item_element.attributes['name'].value = ATTRIBUTES_TO_MAP[name]
178 is_modified = True
179 elif assert_not_deprecated:
180 AssertNotDeprecatedAttribute(name, value, filename)
182 return is_modified
185 def GenerateV14LayoutResource(input_filename, output_v14_filename,
186 output_v17_filename):
187 """Convert API 17 layout resource to API 14 compatible layout resource.
189 It's mostly a simple replacement, s/Start/Left s/End/Right,
190 on the attribute names.
191 If the generated resource is identical to the original resource,
192 don't do anything. If not, write the generated resource to
193 output_v14_filename, and copy the original resource to output_v17_filename.
195 dom = ParseAndReportErrors(input_filename)
196 is_modified = GenerateV14LayoutResourceDom(dom, input_filename)
198 if is_modified:
199 # Write the generated resource.
200 WriteDomToFile(dom, output_v14_filename)
202 # Copy the original resource.
203 build_utils.MakeDirectory(os.path.dirname(output_v17_filename))
204 shutil.copy2(input_filename, output_v17_filename)
207 def GenerateV14StyleResource(input_filename, output_v14_filename):
208 """Convert API 17 style resources to API 14 compatible style resource.
210 Write the generated style resource to output_v14_filename.
211 It's mostly a simple replacement, s/Start/Left s/End/Right,
212 on the attribute names.
214 dom = ParseAndReportErrors(input_filename)
215 GenerateV14StyleResourceDom(dom, input_filename)
217 # Write the generated resource.
218 WriteDomToFile(dom, output_v14_filename)
221 def GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, output_v17_dir):
222 """Convert layout resources to API 14 compatible resources in input_dir."""
223 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
224 rel_filename = os.path.relpath(input_filename, input_dir)
225 output_v14_filename = os.path.join(output_v14_dir, rel_filename)
226 output_v17_filename = os.path.join(output_v17_dir, rel_filename)
227 GenerateV14LayoutResource(input_filename, output_v14_filename,
228 output_v17_filename)
231 def GenerateV14StyleResourcesInDir(input_dir, output_v14_dir):
232 """Convert style resources to API 14 compatible resources in input_dir."""
233 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
234 rel_filename = os.path.relpath(input_filename, input_dir)
235 output_v14_filename = os.path.join(output_v14_dir, rel_filename)
236 GenerateV14StyleResource(input_filename, output_v14_filename)
239 def ParseArgs():
240 """Parses command line options.
242 Returns:
243 An options object as from optparse.OptionsParser.parse_args()
245 parser = optparse.OptionParser()
246 parser.add_option('--res-dir',
247 help='directory containing resources '
248 'used to generate v14 compatible resources')
249 parser.add_option('--res-v14-compatibility-dir',
250 help='output directory into which '
251 'v14 compatible resources will be generated')
252 parser.add_option('--stamp', help='File to touch on success')
254 options, args = parser.parse_args()
256 if args:
257 parser.error('No positional arguments should be given.')
259 # Check that required options have been provided.
260 required_options = ('res_dir', 'res_v14_compatibility_dir')
261 build_utils.CheckOptions(options, parser, required=required_options)
262 return options
264 def GenerateV14Resources(res_dir, res_v14_dir):
265 for name in os.listdir(res_dir):
266 if not os.path.isdir(os.path.join(res_dir, name)):
267 continue
269 dir_pieces = name.split('-')
270 resource_type = dir_pieces[0]
271 qualifiers = dir_pieces[1:]
273 api_level_qualifier_index = -1
274 api_level_qualifier = ''
275 for index, qualifier in enumerate(qualifiers):
276 if re.match('v[0-9]+$', qualifier):
277 api_level_qualifier_index = index
278 api_level_qualifier = qualifier
279 break
281 # Android pre-v17 API doesn't support RTL. Skip.
282 if 'ldrtl' in qualifiers:
283 continue
285 input_dir = os.path.abspath(os.path.join(res_dir, name))
287 # We also need to copy the original v17 resource to *-v17 directory
288 # because the generated v14 resource will hide the original resource.
289 output_v14_dir = os.path.join(res_v14_dir, name)
290 output_v17_dir = os.path.join(res_v14_dir, name + '-v17')
292 # We only convert layout resources under layout*/, xml*/,
293 # and style resources under values*/.
294 if resource_type in ('layout', 'xml'):
295 if not api_level_qualifier:
296 GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir,
297 output_v17_dir)
298 elif resource_type == 'values':
299 if api_level_qualifier == 'v17':
300 output_qualifiers = qualifiers[:]
301 del output_qualifiers[api_level_qualifier_index]
302 output_v14_dir = os.path.join(res_v14_dir,
303 '-'.join([resource_type] +
304 output_qualifiers))
305 GenerateV14StyleResourcesInDir(input_dir, output_v14_dir)
306 elif not api_level_qualifier:
307 ErrorIfStyleResourceExistsInDir(input_dir)
309 def main():
310 options = ParseArgs()
312 res_v14_dir = options.res_v14_compatibility_dir
314 build_utils.DeleteDirectory(res_v14_dir)
315 build_utils.MakeDirectory(res_v14_dir)
317 GenerateV14Resources(options.res_dir, res_v14_dir)
319 if options.stamp:
320 build_utils.Touch(options.stamp)
322 if __name__ == '__main__':
323 sys.exit(main())