Roll src/third_party/WebKit e0eac24:489c548 (svn 193311:193320)
[chromium-blink-merge.git] / build / android / gyp / generate_v14_compatible_resources.py
blob78181704874b376bcc23e0b6114e500168384726
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 optparse
23 import os
24 import re
25 import shutil
26 import sys
27 import xml.dom.minidom as minidom
29 from util import build_utils
31 # Note that we are assuming 'android:' is an alias of
32 # the namespace 'http://schemas.android.com/apk/res/android'.
34 GRAVITY_ATTRIBUTES = ('android:gravity', 'android:layout_gravity')
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'}
51 ATTRIBUTES_TO_MAP = dict(['android:' + k, 'android:' + v] for k, v
52 in ATTRIBUTES_TO_MAP.iteritems())
54 ATTRIBUTES_TO_MAP_REVERSED = dict([v, k] for k, v
55 in ATTRIBUTES_TO_MAP.iteritems())
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
68 def ParseAndReportErrors(filename):
69 try:
70 return minidom.parse(filename)
71 except Exception:
72 import traceback
73 traceback.print_exc()
74 sys.stderr.write('Failed to parse XML file: %s\n' % filename)
75 sys.exit(1)
78 def AssertNotDeprecatedAttribute(name, value, filename):
79 """Raises an exception if the given attribute is deprecated."""
80 msg = None
81 if name in ATTRIBUTES_TO_MAP_REVERSED:
82 msg = '{0} should use {1} instead of {2}'.format(filename,
83 ATTRIBUTES_TO_MAP_REVERSED[name], name)
84 elif name in GRAVITY_ATTRIBUTES and ('left' in value or 'right' in value):
85 msg = '{0} should use start/end instead of left/right for {1}'.format(
86 filename, name)
88 if msg:
89 msg += ('\nFor background, see: http://android-developers.blogspot.com/'
90 '2013/03/native-rtl-support-in-android-42.html\n'
91 'If you have a legitimate need for this attribute, discuss with '
92 'kkimlabs@chromium.org or newt@chromium.org')
93 raise Exception(msg)
96 def WriteDomToFile(dom, filename):
97 """Write the given dom to filename."""
98 build_utils.MakeDirectory(os.path.dirname(filename))
99 with open(filename, 'w') as f:
100 dom.writexml(f, '', ' ', '\n', encoding='utf-8')
103 def HasStyleResource(dom):
104 """Return True if the dom is a style resource, False otherwise."""
105 root_node = IterateXmlElements(dom).next()
106 return bool(root_node.nodeName == 'resources' and
107 list(root_node.getElementsByTagName('style')))
110 def ErrorIfStyleResourceExistsInDir(input_dir):
111 """If a style resource is in input_dir, raises an exception."""
112 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
113 dom = ParseAndReportErrors(input_filename)
114 if HasStyleResource(dom):
115 raise Exception('error: style file ' + input_filename +
116 ' should be under ' + input_dir +
117 '-v17 directory. Please refer to '
118 'http://crbug.com/243952 for the details.')
121 def GenerateV14LayoutResourceDom(dom, filename, assert_not_deprecated=True):
122 """Convert layout resource to API 14 compatible layout resource.
124 Args:
125 dom: Parsed minidom object to be modified.
126 filename: Filename that the DOM was parsed from.
127 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
128 cause an exception to be thrown.
130 Returns:
131 True if dom is modified, False otherwise.
133 is_modified = False
135 # Iterate all the elements' attributes to find attributes to convert.
136 for element in IterateXmlElements(dom):
137 for name, value in list(element.attributes.items()):
138 # Convert any API 17 Start/End attributes to Left/Right attributes.
139 # For example, from paddingStart="10dp" to paddingLeft="10dp"
140 # Note: gravity attributes are not necessary to convert because
141 # start/end values are backward-compatible. Explained at
142 # https://plus.sandbox.google.com/+RomanNurik/posts/huuJd8iVVXY?e=Showroom
143 if name in ATTRIBUTES_TO_MAP:
144 element.setAttribute(ATTRIBUTES_TO_MAP[name], value)
145 del element.attributes[name]
146 is_modified = True
147 elif assert_not_deprecated:
148 AssertNotDeprecatedAttribute(name, value, filename)
150 return is_modified
153 def GenerateV14StyleResourceDom(dom, filename, assert_not_deprecated=True):
154 """Convert style resource to API 14 compatible style resource.
156 Args:
157 dom: Parsed minidom object to be modified.
158 filename: Filename that the DOM was parsed from.
159 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
160 cause an exception to be thrown.
162 Returns:
163 True if dom is modified, False otherwise.
165 is_modified = False
167 for style_element in dom.getElementsByTagName('style'):
168 for item_element in style_element.getElementsByTagName('item'):
169 name = item_element.attributes['name'].value
170 value = item_element.childNodes[0].nodeValue
171 if name in ATTRIBUTES_TO_MAP:
172 item_element.attributes['name'].value = ATTRIBUTES_TO_MAP[name]
173 is_modified = True
174 elif assert_not_deprecated:
175 AssertNotDeprecatedAttribute(name, value, filename)
177 return is_modified
180 def GenerateV14LayoutResource(input_filename, output_v14_filename,
181 output_v17_filename):
182 """Convert API 17 layout resource to API 14 compatible layout resource.
184 It's mostly a simple replacement, s/Start/Left s/End/Right,
185 on the attribute names.
186 If the generated resource is identical to the original resource,
187 don't do anything. If not, write the generated resource to
188 output_v14_filename, and copy the original resource to output_v17_filename.
190 dom = ParseAndReportErrors(input_filename)
191 is_modified = GenerateV14LayoutResourceDom(dom, input_filename)
193 if is_modified:
194 # Write the generated resource.
195 WriteDomToFile(dom, output_v14_filename)
197 # Copy the original resource.
198 build_utils.MakeDirectory(os.path.dirname(output_v17_filename))
199 shutil.copy2(input_filename, output_v17_filename)
202 def GenerateV14StyleResource(input_filename, output_v14_filename):
203 """Convert API 17 style resources to API 14 compatible style resource.
205 Write the generated style resource to output_v14_filename.
206 It's mostly a simple replacement, s/Start/Left s/End/Right,
207 on the attribute names.
209 dom = ParseAndReportErrors(input_filename)
210 GenerateV14StyleResourceDom(dom, input_filename)
212 # Write the generated resource.
213 WriteDomToFile(dom, output_v14_filename)
216 def GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, output_v17_dir):
217 """Convert layout 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 output_v17_filename = os.path.join(output_v17_dir, rel_filename)
222 GenerateV14LayoutResource(input_filename, output_v14_filename,
223 output_v17_filename)
226 def GenerateV14StyleResourcesInDir(input_dir, output_v14_dir):
227 """Convert style resources to API 14 compatible resources in input_dir."""
228 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
229 rel_filename = os.path.relpath(input_filename, input_dir)
230 output_v14_filename = os.path.join(output_v14_dir, rel_filename)
231 GenerateV14StyleResource(input_filename, output_v14_filename)
234 def VerifyV14ResourcesInDir(input_dir, resource_type):
235 """Verify that the resources in input_dir is compatible with v14, i.e., they
236 don't use attributes that cause crashes on certain devices. Print an error if
237 they have."""
238 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
239 exception_message = ('error : ' + input_filename + ' has an RTL attribute, '
240 'i.e., attribute that has "start" or "end" in its name.'
241 ' Pre-v17 resources should not include it because it '
242 'can cause crashes on certain devices. Please refer to '
243 'http://crbug.com/243952 for the details.')
244 dom = ParseAndReportErrors(input_filename)
245 if resource_type in ('layout', 'xml'):
246 if GenerateV14LayoutResourceDom(dom, input_filename, False):
247 raise Exception(exception_message)
248 elif resource_type == 'values':
249 if GenerateV14StyleResourceDom(dom, input_filename, False):
250 raise Exception(exception_message)
253 def AssertNoDeprecatedAttributesInDir(input_dir, resource_type):
254 """Raises an exception if resources in input_dir have deprecated attributes,
255 e.g., paddingLeft, paddingRight"""
256 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'):
257 dom = ParseAndReportErrors(input_filename)
258 if resource_type in ('layout', 'xml'):
259 GenerateV14LayoutResourceDom(dom, input_filename)
260 elif resource_type == 'values':
261 GenerateV14StyleResourceDom(dom, input_filename)
264 def ParseArgs():
265 """Parses command line options.
267 Returns:
268 An options object as from optparse.OptionsParser.parse_args()
270 parser = optparse.OptionParser()
271 parser.add_option('--res-dir',
272 help='directory containing resources '
273 'used to generate v14 compatible resources')
274 parser.add_option('--res-v14-compatibility-dir',
275 help='output directory into which '
276 'v14 compatible resources will be generated')
277 parser.add_option('--stamp', help='File to touch on success')
278 parser.add_option('--verify-only', action="store_true", help='Do not generate'
279 ' v14 resources. Instead, just verify that the resources are already '
280 "compatible with v14, i.e. they don't use attributes that cause crashes "
281 'on certain devices.')
283 options, args = parser.parse_args()
285 if args:
286 parser.error('No positional arguments should be given.')
288 # Check that required options have been provided.
289 required_options = ('res_dir', 'res_v14_compatibility_dir')
290 build_utils.CheckOptions(options, parser, required=required_options)
291 return options
293 def GenerateV14Resources(res_dir, res_v14_dir, verify_only):
294 for name in os.listdir(res_dir):
295 if not os.path.isdir(os.path.join(res_dir, name)):
296 continue
298 dir_pieces = name.split('-')
299 resource_type = dir_pieces[0]
300 qualifiers = dir_pieces[1:]
302 api_level_qualifier_index = -1
303 api_level_qualifier = ''
304 for index, qualifier in enumerate(qualifiers):
305 if re.match('v[0-9]+$', qualifier):
306 api_level_qualifier_index = index
307 api_level_qualifier = qualifier
308 break
310 # Android pre-v17 API doesn't support RTL. Skip.
311 if 'ldrtl' in qualifiers:
312 continue
314 input_dir = os.path.abspath(os.path.join(res_dir, name))
316 if verify_only:
317 if not api_level_qualifier or int(api_level_qualifier[1:]) < 17:
318 VerifyV14ResourcesInDir(input_dir, resource_type)
319 else:
320 AssertNoDeprecatedAttributesInDir(input_dir, resource_type)
321 else:
322 # We also need to copy the original v17 resource to *-v17 directory
323 # because the generated v14 resource will hide the original resource.
324 output_v14_dir = os.path.join(res_v14_dir, name)
325 output_v17_dir = os.path.join(res_v14_dir, name + '-v17')
327 # We only convert layout resources under layout*/, xml*/,
328 # and style resources under values*/.
329 if resource_type in ('layout', 'xml'):
330 if not api_level_qualifier:
331 GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir,
332 output_v17_dir)
333 elif resource_type == 'values':
334 if api_level_qualifier == 'v17':
335 output_qualifiers = qualifiers[:]
336 del output_qualifiers[api_level_qualifier_index]
337 output_v14_dir = os.path.join(res_v14_dir,
338 '-'.join([resource_type] +
339 output_qualifiers))
340 GenerateV14StyleResourcesInDir(input_dir, output_v14_dir)
341 elif not api_level_qualifier:
342 ErrorIfStyleResourceExistsInDir(input_dir)
344 def main():
345 options = ParseArgs()
347 res_v14_dir = options.res_v14_compatibility_dir
349 build_utils.DeleteDirectory(res_v14_dir)
350 build_utils.MakeDirectory(res_v14_dir)
352 GenerateV14Resources(options.res_dir, res_v14_dir, options.verify_only)
354 if options.stamp:
355 build_utils.Touch(options.stamp)
357 if __name__ == '__main__':
358 sys.exit(main())