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 in ('eat-comment', 'skip', 'overlayable', 'macro'): 93 # These tags do not create real resources 94 continue 95 if child.tag == 'declare-styleable': 96 ret.update(self._ParseDeclareStyleable(child)) 97 else: 98 if child.tag in ('item', 'public'): 99 resource_type = child.attrib['type'] 100 elif child.tag in ('array', 'integer-array', 'string-array'): 101 resource_type = 'array' 102 else: 103 resource_type = child.tag 104 parsed_element = ElementTree.tostring(child, encoding='unicode').strip() 105 assert resource_type in resource_utils.ALL_RESOURCE_TYPES, ( 106 f'Infered resource type ({resource_type}) from xml entry ' 107 f'({parsed_element}) (found in {xml_path}) is not listed in ' 108 'resource_utils.ALL_RESOURCE_TYPES. Teach resources_parser.py how ' 109 'to parse this entry and then add to the list.') 110 name = _ResourceNameToJavaSymbol(child.attrib['name']) 111 ret.add(_TextSymbolEntry('int', resource_type, name, _DUMMY_RTXT_ID)) 112 return ret 113 114 def _CollectResourcesListFromDirectory(self, res_dir): 115 ret = set() 116 globs = resource_utils._GenerateGlobs(self.ignore_pattern) 117 for root, _, files in os.walk(res_dir): 118 resource_type = os.path.basename(root) 119 if '-' in resource_type: 120 resource_type = resource_type[:resource_type.index('-')] 121 for f in files: 122 if build_utils.MatchesGlob(f, globs): 123 continue 124 if resource_type == 'values': 125 ret.update(self._ParseValuesXml(os.path.join(root, f))) 126 else: 127 if '.' in f: 128 resource_name = f[:f.index('.')] 129 else: 130 resource_name = f 131 ret.add( 132 _TextSymbolEntry('int', resource_type, resource_name, 133 _DUMMY_RTXT_ID)) 134 # Other types not just layouts can contain new ids (eg: Menus and 135 # Drawables). Just in case, look for new ids in all files. 136 if f.endswith('.xml'): 137 ret.update(self._ExtractNewIdsFromXml(os.path.join(root, f))) 138 return ret 139 140 def _CollectResourcesListFromDirectories(self): 141 ret = set() 142 for res_dir in self.res_dirs: 143 ret.update(self._CollectResourcesListFromDirectory(res_dir)) 144 return sorted(ret) 145 146 def WriteRTxtFile(self, rtxt_path): 147 resources = self._CollectResourcesListFromDirectories() 148 with action_helpers.atomic_output(rtxt_path, mode='w') as f: 149 for resource in resources: 150 line = '{0.java_type} {0.resource_type} {0.name} {0.value}\n'.format( 151 resource) 152 f.write(line) 153