xref: /aosp_15_r20/external/cronet/build/android/gyp/util/resources_parser.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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