• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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