#!/usr/bin/env python # # Copyright (C) 2020 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Extracts compat_config.xml from built jar files and merges them into a single XML file. """ import argparse import collections import sys import xml.etree.ElementTree as ET from zipfile import ZipFile XmlContent = collections.namedtuple('XmlContent', ['xml', 'source']) def extract_compat_config(jarfile): """ Reads all compat_config.xml files from a jarfile. Yields: XmlContent for each XML file found. """ with ZipFile(jarfile, 'r') as jar: for info in jar.infolist(): if info.filename.endswith("_compat_config.xml"): with jar.open(info.filename, 'r') as xml: yield XmlContent(xml, info.filename) def change_element_tostring(element): s = "%s(%s)" % (element.attrib['name'], element.attrib['id']) metadata = element.find('meta-data') if metadata is not None: s += " defined in class %s at %s" % (metadata.attrib['definedIn'], metadata.attrib['sourcePosition']) return s class ChangeDefinition(collections.namedtuple('ChangeDefinition', ['source', 'element'])): def __str__(self): return " From: %s:\n %s" % (self.source, change_element_tostring(self.element)) class ConfigMerger(object): def __init__(self, detect_conflicts): self.tree = ET.ElementTree() self.tree._setroot(ET.Element("config")) self.detect_conflicts = detect_conflicts self.changes_by_id = dict() self.changes_by_name = dict() self.errors = 0 self.write_errors_to = sys.stderr def merge(self, xmlFile, source): xml = ET.parse(xmlFile) for child in xml.getroot(): if self.detect_conflicts: id = child.attrib['id'] name = child.attrib['name'] this_change = ChangeDefinition(source, child) if id in self.changes_by_id.keys(): duplicate = self.changes_by_id[id] self.write_errors_to.write( "ERROR: Duplicate definitions for compat change with ID %s:\n%s\n%s\n" % ( id, duplicate, this_change)) self.errors += 1 if name in self.changes_by_name.keys(): duplicate = self.changes_by_name[name] self.write_errors_to.write( "ERROR: Duplicate definitions for compat change with name %s:\n%s\n%s\n" % ( name, duplicate, this_change)) self.errors += 1 self.changes_by_id[id] = this_change self.changes_by_name[name] = this_change self.tree.getroot().append(child) def _check_error(self): if self.errors > 0: raise Exception("Failed due to %d earlier errors" % self.errors) def write(self, filename): self._check_error() self.tree.write(filename, encoding='utf-8', xml_declaration=True) def write_device_config(self, filename): self._check_error() self.strip_config_for_device().write(filename, encoding='utf-8', xml_declaration=True) def strip_config_for_device(self): new_tree = ET.ElementTree() new_tree._setroot(ET.Element("config")) for change in self.tree.getroot(): new_change = ET.Element("compat-change") new_change.attrib = change.attrib.copy() new_tree.getroot().append(new_change) return new_tree def main(argv): parser = argparse.ArgumentParser( description="Processes compat config XML files") parser.add_argument("--jar", type=argparse.FileType('rb'), action='append', help="Specifies a jar file to extract compat_config.xml from.") parser.add_argument("--xml", type=argparse.FileType('rb'), action='append', help="Specifies an xml file to read compat_config from.") parser.add_argument("--device-config", dest="device_config", type=argparse.FileType('wb'), help="Specify where to write config for embedding on the device to. " "Meta data not needed on the devivce is stripped from this.") parser.add_argument("--merged-config", dest="merged_config", type=argparse.FileType('wb'), help="Specify where to write merged config to. This will include metadata.") parser.add_argument("--allow-duplicates", dest="allow_duplicates", action='store_true', help="Allow duplicate changed IDs in the merged config.") args = parser.parse_args() config = ConfigMerger(detect_conflicts = not args.allow_duplicates) if args.jar: for jar in args.jar: for xml_content in extract_compat_config(jar): config.merge(xml_content.xml, "%s:%s" % (jar.name, xml_content.source)) if args.xml: for xml in args.xml: config.merge(xml, xml.name) if args.device_config: config.write_device_config(args.device_config) if args.merged_config: config.write(args.merged_config) if __name__ == "__main__": main(sys.argv)