1#!/usr/bin/env python 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 self.tree.write(filename, encoding='utf-8', xml_declaration=True) 95 96 def write_device_config(self, filename): 97 self._check_error() 98 self.strip_config_for_device().write(filename, encoding='utf-8', xml_declaration=True) 99 100 def strip_config_for_device(self): 101 new_tree = ET.ElementTree() 102 new_tree._setroot(ET.Element("config")) 103 for change in self.tree.getroot(): 104 new_change = ET.Element("compat-change") 105 new_change.attrib = change.attrib.copy() 106 new_tree.getroot().append(new_change) 107 return new_tree 108 109def main(argv): 110 parser = argparse.ArgumentParser( 111 description="Processes compat config XML files") 112 parser.add_argument("--jar", type=argparse.FileType('rb'), action='append', 113 help="Specifies a jar file to extract compat_config.xml from.") 114 parser.add_argument("--xml", type=argparse.FileType('rb'), action='append', 115 help="Specifies an xml file to read compat_config from.") 116 parser.add_argument("--device-config", dest="device_config", type=argparse.FileType('wb'), 117 help="Specify where to write config for embedding on the device to. " 118 "Meta data not needed on the devivce is stripped from this.") 119 parser.add_argument("--merged-config", dest="merged_config", type=argparse.FileType('wb'), 120 help="Specify where to write merged config to. This will include metadata.") 121 parser.add_argument("--allow-duplicates", dest="allow_duplicates", action='store_true', 122 help="Allow duplicate changed IDs in the merged config.") 123 124 args = parser.parse_args() 125 126 config = ConfigMerger(detect_conflicts = not args.allow_duplicates) 127 if args.jar: 128 for jar in args.jar: 129 for xml_content in extract_compat_config(jar): 130 config.merge(xml_content.xml, "%s:%s" % (jar.name, xml_content.source)) 131 if args.xml: 132 for xml in args.xml: 133 config.merge(xml, xml.name) 134 135 if args.device_config: 136 config.write_device_config(args.device_config) 137 138 if args.merged_config: 139 config.write(args.merged_config) 140 141 142 143if __name__ == "__main__": 144 main(sys.argv) 145