1# Copyright 2020 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import collections 6import os 7import re 8from xml.etree import ElementTree 9 10from util import build_utils 11from util import resource_utils 12import action_helpers # build_utils adds //build to sys.path. 13 14_TextSymbolEntry = collections.namedtuple( 15 'RTextEntry', ('java_type', 'resource_type', 'name', 'value')) 16 17_DUMMY_RTXT_ID = '0x7f010001' 18_DUMMY_RTXT_INDEX = '1' 19 20 21def _ResourceNameToJavaSymbol(resource_name): 22 return re.sub('[\.:]', '_', resource_name) 23 24 25class RTxtGenerator: 26 def __init__(self, 27 res_dirs, 28 ignore_pattern=resource_utils.AAPT_IGNORE_PATTERN): 29 self.res_dirs = res_dirs 30 self.ignore_pattern = ignore_pattern 31 32 def _ParseDeclareStyleable(self, node): 33 ret = set() 34 stylable_name = _ResourceNameToJavaSymbol(node.attrib['name']) 35 ret.add( 36 _TextSymbolEntry('int[]', 'styleable', stylable_name, 37 '{{{}}}'.format(_DUMMY_RTXT_ID))) 38 for child in node: 39 if child.tag == 'eat-comment': 40 continue 41 if child.tag != 'attr': 42 # This parser expects everything inside <declare-stylable/> to be either 43 # an attr or an eat-comment. If new resource xml files are added that do 44 # not conform to this, this parser needs updating. 45 raise Exception('Unexpected tag {} inside <delcare-stylable/>'.format( 46 child.tag)) 47 entry_name = '{}_{}'.format( 48 stylable_name, _ResourceNameToJavaSymbol(child.attrib['name'])) 49 ret.add( 50 _TextSymbolEntry('int', 'styleable', entry_name, _DUMMY_RTXT_INDEX)) 51 if not child.attrib['name'].startswith('android:'): 52 resource_name = _ResourceNameToJavaSymbol(child.attrib['name']) 53 ret.add(_TextSymbolEntry('int', 'attr', resource_name, _DUMMY_RTXT_ID)) 54 for entry in child: 55 if entry.tag not in ('enum', 'flag'): 56 # This parser expects everything inside <attr/> to be either an 57 # <enum/> or an <flag/>. If new resource xml files are added that do 58 # not conform to this, this parser needs updating. 59 raise Exception('Unexpected tag {} inside <attr/>'.format(entry.tag)) 60 resource_name = _ResourceNameToJavaSymbol(entry.attrib['name']) 61 ret.add(_TextSymbolEntry('int', 'id', resource_name, _DUMMY_RTXT_ID)) 62 return ret 63 64 def _ExtractNewIdsFromNode(self, node): 65 ret = set() 66 # Sometimes there are @+id/ in random attributes (not just in android:id) 67 # and apparently that is valid. See: 68 # https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html 69 for value in node.attrib.values(): 70 if value.startswith('@+id/'): 71 resource_name = value[5:] 72 ret.add(_TextSymbolEntry('int', 'id', resource_name, _DUMMY_RTXT_ID)) 73 for child in node: 74 ret.update(self._ExtractNewIdsFromNode(child)) 75 return ret 76 77 def _ParseXml(self, xml_path): 78 try: 79 return ElementTree.parse(xml_path).getroot() 80 except Exception as e: 81 raise RuntimeError('Failure parsing {}:\n'.format(xml_path)) from e 82 83 def _ExtractNewIdsFromXml(self, xml_path): 84 return self._ExtractNewIdsFromNode(self._ParseXml(xml_path)) 85 86 def _ParseValuesXml(self, xml_path): 87 ret = set() 88 root = self._ParseXml(xml_path) 89 90 assert root.tag == 'resources' 91 for child in root: 92 if child.tag == 'eat-comment': 93 # eat-comment is just a dummy documentation element. 94 continue 95 if child.tag == 'skip': 96 # skip is just a dummy element. 97 continue 98 if child.tag == 'declare-styleable': 99 ret.update(self._ParseDeclareStyleable(child)) 100 else: 101 if child.tag in ('item', 'public'): 102 resource_type = child.attrib['type'] 103 elif child.tag in ('array', 'integer-array', 'string-array'): 104 resource_type = 'array' 105 else: 106 resource_type = child.tag 107 parsed_element = ElementTree.tostring(child, encoding='unicode').strip() 108 assert resource_type in resource_utils.ALL_RESOURCE_TYPES, ( 109 f'Infered resource type ({resource_type}) from xml entry ' 110 f'({parsed_element}) (found in {xml_path}) is not listed in ' 111 'resource_utils.ALL_RESOURCE_TYPES. Teach resources_parser.py how ' 112 'to parse this entry and/or add to the list.') 113 name = _ResourceNameToJavaSymbol(child.attrib['name']) 114 ret.add(_TextSymbolEntry('int', resource_type, name, _DUMMY_RTXT_ID)) 115 return ret 116 117 def _CollectResourcesListFromDirectory(self, res_dir): 118 ret = set() 119 globs = resource_utils._GenerateGlobs(self.ignore_pattern) 120 for root, _, files in os.walk(res_dir): 121 resource_type = os.path.basename(root) 122 if '-' in resource_type: 123 resource_type = resource_type[:resource_type.index('-')] 124 for f in files: 125 if build_utils.MatchesGlob(f, globs): 126 continue 127 if resource_type == 'values': 128 ret.update(self._ParseValuesXml(os.path.join(root, f))) 129 else: 130 if '.' in f: 131 resource_name = f[:f.index('.')] 132 else: 133 resource_name = f 134 ret.add( 135 _TextSymbolEntry('int', resource_type, resource_name, 136 _DUMMY_RTXT_ID)) 137 # Other types not just layouts can contain new ids (eg: Menus and 138 # Drawables). Just in case, look for new ids in all files. 139 if f.endswith('.xml'): 140 ret.update(self._ExtractNewIdsFromXml(os.path.join(root, f))) 141 return ret 142 143 def _CollectResourcesListFromDirectories(self): 144 ret = set() 145 for res_dir in self.res_dirs: 146 ret.update(self._CollectResourcesListFromDirectory(res_dir)) 147 return sorted(ret) 148 149 def WriteRTxtFile(self, rtxt_path): 150 resources = self._CollectResourcesListFromDirectories() 151 with action_helpers.atomic_output(rtxt_path, mode='w') as f: 152 for resource in resources: 153 line = '{0.java_type} {0.resource_type} {0.name} {0.value}\n'.format( 154 resource) 155 f.write(line) 156