• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2020 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Extracts compat_config.xml from built jar files and merges them into a single
19XML file.
20"""
21
22import argparse
23import collections
24import sys
25import xml.etree.ElementTree as ET
26from zipfile import ZipFile
27
28XmlContent = collections.namedtuple('XmlContent', ['xml', 'source'])
29
30def extract_compat_config(jarfile):
31    """
32    Reads all compat_config.xml files from a jarfile.
33
34    Yields: XmlContent for each XML file found.
35    """
36    with ZipFile(jarfile, 'r') as jar:
37        for info in jar.infolist():
38            if info.filename.endswith("_compat_config.xml"):
39                with jar.open(info.filename, 'r') as xml:
40                    yield XmlContent(xml, info.filename)
41
42def change_element_tostring(element):
43    s = "%s(%s)" % (element.attrib['name'], element.attrib['id'])
44    metadata = element.find('meta-data')
45    if metadata is not None:
46        s += " defined in class %s at %s" % (metadata.attrib['definedIn'], metadata.attrib['sourcePosition'])
47    return s
48
49class ChangeDefinition(collections.namedtuple('ChangeDefinition', ['source', 'element'])):
50    def __str__(self):
51        return "  From: %s:\n    %s" % (self.source, change_element_tostring(self.element))
52
53class ConfigMerger(object):
54
55    def __init__(self, detect_conflicts):
56        self.tree = ET.ElementTree()
57        self.tree._setroot(ET.Element("config"))
58        self.detect_conflicts = detect_conflicts
59        self.changes_by_id = dict()
60        self.changes_by_name = dict()
61        self.errors = 0
62        self.write_errors_to = sys.stderr
63
64    def merge(self, xmlFile, source):
65        xml = ET.parse(xmlFile)
66        for child in xml.getroot():
67            if self.detect_conflicts:
68                id = child.attrib['id']
69                name = child.attrib['name']
70                this_change = ChangeDefinition(source, child)
71                if id in self.changes_by_id.keys():
72                    duplicate = self.changes_by_id[id]
73                    self.write_errors_to.write(
74                        "ERROR: Duplicate definitions for compat change with ID %s:\n%s\n%s\n" % (
75                        id, duplicate, this_change))
76                    self.errors += 1
77                if name in self.changes_by_name.keys():
78                    duplicate = self.changes_by_name[name]
79                    self.write_errors_to.write(
80                        "ERROR: Duplicate definitions for compat change with name %s:\n%s\n%s\n" % (
81                        name, duplicate, this_change))
82                    self.errors += 1
83
84                self.changes_by_id[id] = this_change
85                self.changes_by_name[name] = this_change
86            self.tree.getroot().append(child)
87
88    def _check_error(self):
89        if self.errors > 0:
90            raise Exception("Failed due to %d earlier errors" % self.errors)
91
92    def write(self, filename):
93        self._check_error()
94        ET.indent(self.tree)
95        self.tree.write(filename, encoding='utf-8', xml_declaration=True)
96
97    def write_device_config(self, filename):
98        self._check_error()
99        self.strip_config_for_device().write(filename, encoding='utf-8', xml_declaration=True)
100
101    def strip_config_for_device(self):
102        new_tree = ET.ElementTree()
103        new_tree._setroot(ET.Element("config"))
104        for change in self.tree.getroot():
105            new_change = ET.Element("compat-change")
106            new_change.attrib = change.attrib.copy()
107            new_tree.getroot().append(new_change)
108        return new_tree
109
110def main(argv):
111    parser = argparse.ArgumentParser(
112        description="Processes compat config XML files")
113    parser.add_argument("--jar", type=argparse.FileType('rb'), action='append',
114        help="Specifies a jar file to extract compat_config.xml from.")
115    parser.add_argument("--xml", type=argparse.FileType('rb'), action='append',
116        help="Specifies an xml file to read compat_config from.")
117    parser.add_argument("--device-config", dest="device_config", type=argparse.FileType('wb'),
118        help="Specify where to write config for embedding on the device to. "
119        "Meta data not needed on the devivce is stripped from this.")
120    parser.add_argument("--merged-config", dest="merged_config", type=argparse.FileType('wb'),
121        help="Specify where to write merged config to. This will include metadata.")
122    parser.add_argument("--allow-duplicates", dest="allow_duplicates", action='store_true',
123        help="Allow duplicate changed IDs in the merged config.")
124
125    args = parser.parse_args()
126
127    config = ConfigMerger(detect_conflicts = not args.allow_duplicates)
128    if args.jar:
129        for jar in args.jar:
130            for xml_content in extract_compat_config(jar):
131                config.merge(xml_content.xml, "%s:%s" % (jar.name, xml_content.source))
132    if args.xml:
133        for xml in args.xml:
134            config.merge(xml, xml.name)
135
136    if args.device_config:
137        config.write_device_config(args.device_config)
138
139    if args.merged_config:
140        config.write(args.merged_config)
141
142
143
144if __name__ == "__main__":
145    main(sys.argv)
146