| # Copyright 2015 The Bazel Authors. All rights reserved. |
| # |
| # 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. |
| |
| """Merges two android manifest xml files.""" |
| |
| import re |
| import sys |
| import xml.dom.minidom |
| |
| from tools.android import android_permissions |
| from third_party.py import gflags |
| |
| FLAGS = gflags.FLAGS |
| EXCLUDE_ALL_ARG = 'all' |
| |
| gflags.DEFINE_multistring( |
| 'exclude_permission', None, |
| 'Permissions to be excluded, e.g.: "android.permission.READ_LOGS".' |
| 'This is a multistring, so multiple of those flags can be provided.' |
| 'Pass "%s" to exclude all permissions contributed by mergees.' |
| % EXCLUDE_ALL_ARG) |
| gflags.DEFINE_multistring( |
| 'mergee', None, |
| 'Mergee manifest that will be merged to merger manifest.' |
| 'This is a multistring, so multiple of those flags can be provided.') |
| gflags.DEFINE_string('merger', None, |
| 'Merger AndroidManifest file to be merged.') |
| gflags.DEFINE_string('output', None, 'Output file with merged manifests.') |
| |
| USAGE = """Error, invalid arguments. |
| Usage: merge_manifests.py --merger=<merger> --mergee=<mergee1> --mergee=<merge2> |
| --exclude_permission=[Exclude permissions from mergee] --output=<output> |
| Examples: |
| merge_manifests.py --merger=manifest.xml --mergee=manifest2.xml |
| --mergee=manifest3.xml --exclude_permission=android.permission.READ_LOGS |
| --output=AndroidManifest.xml |
| |
| merge_manifests.py --merger=manifest.xml --mergee=manifest2.xml |
| --mergee=manifest3.xml --exclude_permission=%s |
| --output=AndroidManifest.xml |
| """ % EXCLUDE_ALL_ARG |
| |
| |
| class UndefinedPlaceholderException(Exception): |
| """Exception thrown when encountering a placeholder without a replacement. |
| """ |
| pass |
| |
| |
| class MalformedManifestException(Exception): |
| """Exception thrown when encountering a fatally malformed manifest. |
| """ |
| pass |
| |
| |
| class MergeManifests(object): |
| """A utility class for merging two android manifest.xml files. |
| |
| This is useful when including another app as android library. |
| """ |
| _ACTIVITY = 'activity' |
| _ANDROID_NAME = 'android:name' |
| _ANDROID_LABEL = 'android:label' |
| _INTENT_FILTER = 'intent-filter' |
| _MANIFEST = 'manifest' |
| _USES_PERMISSION = 'uses-permission' |
| _USES_PERMISSION_SDK_23 = 'uses-permission-sdk-23' |
| _NODES_TO_COPY_FROM_MERGEE = { |
| _MANIFEST: [ |
| 'instrumentation', |
| 'permission', |
| _USES_PERMISSION, |
| _USES_PERMISSION_SDK_23, |
| 'uses-feature', |
| 'permission-group', |
| ], |
| 'application': [ |
| 'activity', |
| 'activity-alias', |
| 'provider', |
| 'receiver', |
| 'service', |
| 'uses-library', |
| 'meta-data', |
| ], |
| } |
| _NODES_TO_REMOVE_FROM_MERGER = [] |
| _PACKAGE = 'package' |
| |
| def __init__(self, merger, mergees, exclude_permissions=None): |
| """Constructs and initializes the MergeManifests object. |
| |
| Args: |
| merger: First (merger) AndroidManifest.xml string. |
| mergees: mergee AndroidManifest.xml strings, a list. |
| exclude_permissions: Permissions to be excludeed from merging, |
| string list. "all" means don't include any permissions. |
| """ |
| self._merger = merger |
| self._mergees = mergees |
| self._exclude_permissions = exclude_permissions |
| self._merger_dom = xml.dom.minidom.parseString(self._merger[0]) |
| |
| def _ApplyExcludePermissions(self, dom): |
| """Apply exclude filters. |
| |
| Args: |
| dom: Document dom object from which to exclude permissions. |
| """ |
| if self._exclude_permissions: |
| exclude_all_permissions = EXCLUDE_ALL_ARG in self._exclude_permissions |
| for element in (dom.getElementsByTagName(self._USES_PERMISSION) + |
| dom.getElementsByTagName(self._USES_PERMISSION_SDK_23)): |
| if element.hasAttribute(self._ANDROID_NAME): |
| attrib = element.getAttribute(self._ANDROID_NAME) |
| if exclude_all_permissions or attrib in self._exclude_permissions: |
| element.parentNode.removeChild(element) |
| |
| def _ExpandPackageName(self, node): |
| """Set the package name if it is in a short form. |
| |
| Filtering logic for what elements have package expansion: |
| If the name starts with a dot, always prefix it with the package. |
| If the name has a dot anywhere else, do not prefix it. |
| If the name has no dot at all, also prefix it with the package. |
| |
| The massageManifest function shows where this rule is applied: |
| |
| In the application element, on the name and backupAgent attributes. |
| In the activity, service, receiver, provider, and activity-alias elements, |
| on the name attribute. |
| In the activity-alias element, on the targetActivity attribute. |
| |
| Args: |
| node: Xml Node for which to expand package name. |
| """ |
| package_name = node.getElementsByTagName(self._MANIFEST).item( |
| 0).getAttribute(self._PACKAGE) |
| |
| if not package_name: |
| return |
| |
| for element in node.getElementsByTagName('*'): |
| if element.nodeName not in [ |
| 'activity', |
| 'activity-alias', |
| 'application', |
| 'service', |
| 'receiver', |
| 'provider', |
| ]: |
| continue |
| |
| self._ExpandPackageNameHelper(package_name, element, self._ANDROID_NAME) |
| |
| if element.nodeName == 'activity': |
| self._ExpandPackageNameHelper(package_name, element, |
| 'android:parentActivityName') |
| |
| if element.nodeName == 'activity-alias': |
| self._ExpandPackageNameHelper(package_name, element, |
| 'android:targetActivity') |
| continue |
| |
| if element.nodeName == 'application': |
| self._ExpandPackageNameHelper(package_name, element, |
| 'android:backupAgent') |
| |
| def _ExpandPackageNameHelper(self, package_name, element, attribute_name): |
| if element.hasAttribute(attribute_name): |
| class_name = element.getAttribute(attribute_name) |
| |
| if class_name.startswith('.'): |
| pass |
| elif '.' not in class_name: |
| class_name = '.' + class_name |
| else: |
| return |
| |
| element.setAttribute(attribute_name, package_name + class_name) |
| |
| def _RemoveFromMerger(self): |
| """Remove from merger.""" |
| for tag_name in self._NODES_TO_REMOVE_FROM_MERGER: |
| elements = self._merger_dom.getElementsByTagName(tag_name) |
| for element in elements: |
| element.parentNode.removeChild(element) |
| |
| def _RemoveAndroidLabel(self, node): |
| """Remove android:label. |
| |
| We do this because it is not required by merger manifest, |
| and it might contain @string references that will not allow compilation. |
| |
| Args: |
| node: Node for which to remove Android labels. |
| """ |
| if node.hasAttribute(self._ANDROID_LABEL): |
| node.removeAttribute(self._ANDROID_LABEL) |
| |
| def _IsDuplicate(self, node_to_copy, node): |
| """Is element a duplicate?""" |
| for merger_node in self._merger_dom.getElementsByTagName(node_to_copy): |
| if (merger_node.getAttribute(self._ANDROID_NAME) == |
| node.getAttribute(self._ANDROID_NAME)): |
| return True |
| return False |
| |
| def _RemoveIntentFilters(self, node): |
| """Remove intent-filter in activity element. |
| |
| So there are no duplicate apps. |
| |
| Args: |
| node: Node for which to remove intent filters. |
| """ |
| intent_filters = node.getElementsByTagName(self._INTENT_FILTER) |
| if intent_filters.length > 0: |
| for sub_node in intent_filters: |
| node.removeChild(sub_node) |
| |
| def _FindElementComment(self, node): |
| """Find element's comment. |
| |
| Assumes that element's comment can be just above the element. |
| Searches previous siblings and looks for the first non text element |
| that is of a nodeType of comment node. |
| |
| Args: |
| node: Node for which to find a comment. |
| Returns: |
| Elements's comment node, None if not found. |
| """ |
| while node.previousSibling: |
| node = node.previousSibling |
| if node.nodeType is node.COMMENT_NODE: |
| return node |
| if node.nodeType is not node.TEXT_NODE: |
| return None |
| return None |
| |
| def _ReplaceArgumentPlaceholders(self, dom): |
| """Replaces argument placeholders with their values. |
| |
| Modifies the attribute values of the input node. |
| |
| Args: |
| dom: Xml node that should get placeholders replaced. |
| """ |
| |
| placeholders = { |
| 'packageName': self._merger_dom.getElementsByTagName( |
| self._MANIFEST).item(0).getAttribute(self._PACKAGE), |
| } |
| |
| for element in dom.getElementsByTagName('*'): |
| for i in range(element.attributes.length): |
| attr = element.attributes.item(i) |
| attr.value = self._ReplaceArgumentHelper(placeholders, attr.value) |
| |
| def _ReplaceArgumentHelper(self, placeholders, attr): |
| """Replaces argument placeholders within a single string. |
| |
| Args: |
| placeholders: A dict mapping between placeholder names and their |
| replacement values. |
| attr: A string in which to replace argument placeholders. |
| |
| Returns: |
| A string with placeholders replaced, or the same string if no placeholders |
| were found. |
| """ |
| match_placeholder = '\\${([a-zA-Z]*)}' |
| |
| # Returns the replacement string for found matches. |
| def PlaceholderReplacer(matchobj): |
| found_placeholder = matchobj.group(1) |
| if found_placeholder not in placeholders: |
| raise UndefinedPlaceholderException( |
| 'Undefined placeholder when substituting arguments: ' |
| + found_placeholder) |
| return placeholders[found_placeholder] |
| |
| attr = re.sub(match_placeholder, PlaceholderReplacer, attr) |
| |
| return attr |
| |
| def _SortAliases(self): |
| applications = self._merger_dom.getElementsByTagName('application') |
| if not applications: |
| return |
| for alias in applications[0].getElementsByTagName('activity-alias'): |
| comment_node = self._FindElementComment(alias) |
| while comment_node is not None: |
| applications[0].appendChild(comment_node) |
| comment_node = self._FindElementComment(alias) |
| applications[0].appendChild(alias) |
| |
| def _FindMergerParent(self, tag_to_copy, destination_tag_name, mergee_dom): |
| """Finds merger parent node, or appends mergee equivalent node if none.""" |
| # Merger parent element to which to add merged elements. |
| if self._merger_dom.getElementsByTagName(destination_tag_name): |
| return self._merger_dom.getElementsByTagName(destination_tag_name)[0] |
| else: |
| mergee_element = mergee_dom.getElementsByTagName(destination_tag_name)[0] |
| # find the parent |
| parents = self._merger_dom.getElementsByTagName( |
| mergee_element.parentNode.tagName) |
| if not parents: |
| raise MalformedManifestException( |
| 'Malformed manifest has tag %s but no parent tag %s', |
| (tag_to_copy, destination_tag_name)) |
| # append the mergee child as the first child. |
| return parents[0].insertBefore(mergee_element, parents[0].firstChild) |
| |
| def Merge(self): |
| """Takes two manifests, and merges them together to produce a third.""" |
| self._RemoveFromMerger() |
| self._ExpandPackageName(self._merger_dom) |
| |
| for dom, filename in self._mergees: |
| mergee_dom = xml.dom.minidom.parseString(dom) |
| self._ReplaceArgumentPlaceholders(mergee_dom) |
| self._ExpandPackageName(mergee_dom) |
| self._ApplyExcludePermissions(mergee_dom) |
| |
| for destination, values in sorted( |
| self._NODES_TO_COPY_FROM_MERGEE.iteritems()): |
| for node_to_copy in values: |
| for node in mergee_dom.getElementsByTagName(node_to_copy): |
| if self._IsDuplicate(node_to_copy, node): |
| continue |
| |
| merger_parent = self._FindMergerParent(node_to_copy, |
| destination, |
| mergee_dom) |
| |
| # Append the merge comment. |
| merger_parent.appendChild( |
| self._merger_dom.createComment(' Merged from file: %s ' % |
| filename)) |
| |
| # Append mergee's comment, if present. |
| comment_node = self._FindElementComment(node) |
| if comment_node: |
| merger_parent.appendChild(comment_node) |
| |
| # Append element from mergee to merger. |
| merger_parent.appendChild(node) |
| |
| # Insert top level comment about the merge. |
| top_comment = ( |
| ' *** WARNING *** DO NOT EDIT! THIS IS GENERATED MANIFEST BY ' |
| 'MERGE_MANIFEST TOOL.\n' |
| ' Merger manifest:\n %s\n' % self._merger[1] + |
| ' Mergee manifests:\n%s' % '\n'.join( |
| [' %s' % mergee[1] for mergee in self._mergees]) + |
| '\n ') |
| manifest_element = self._merger_dom.getElementsByTagName('manifest')[0] |
| manifest_element.insertBefore(self._merger_dom.createComment(top_comment), |
| manifest_element.firstChild) |
| |
| self._SortAliases() |
| return self._merger_dom.toprettyxml(indent=' ') |
| |
| |
| def _ReadFiles(files): |
| results = [] |
| for file_name in files: |
| results.append(_ReadFile(file_name)) |
| return results |
| |
| |
| def _ReadFile(file_name): |
| with open(file_name, 'r') as my_file: |
| return (my_file.read(), file_name,) |
| |
| |
| def _ValidateAndWarnPermissions(exclude_permissions): |
| unknown_permissions = ( |
| set(exclude_permissions) |
| - set([EXCLUDE_ALL_ARG]) |
| - android_permissions.PERMISSIONS) |
| return '\n'.join([ |
| 'WARNING:\n\t Specified permission "%s" is not a standard permission. ' |
| 'Is it a typo?' % perm for perm in unknown_permissions]) |
| |
| |
| def main(): |
| if not FLAGS.merger: |
| raise RuntimeError('Missing merger value.\n' + USAGE) |
| if len(FLAGS.mergee) < 1: |
| raise RuntimeError('Missing mergee value.\n' + USAGE) |
| if not FLAGS.output: |
| raise RuntimeError('Missing output value.\n' + USAGE) |
| if FLAGS.exclude_permission: |
| warning = _ValidateAndWarnPermissions(FLAGS.exclude_permission) |
| if warning: |
| print warning |
| |
| merged_manifests = MergeManifests(_ReadFile(FLAGS.merger), |
| _ReadFiles(FLAGS.mergee), |
| FLAGS.exclude_permission |
| ).Merge() |
| |
| with open(FLAGS.output, 'w') as out_file: |
| for line in merged_manifests.split('\n'): |
| if not line.strip(): |
| continue |
| out_file.write(line + '\n') |
| |
| if __name__ == '__main__': |
| FLAGS(sys.argv) |
| main() |