aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools/android/merge_manifests.py
blob: 4ef6a632a6dda1bd9844a0c70231cd0a3f963a7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# 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'
  _NODES_TO_COPY_FROM_MERGEE = {
      _MANIFEST: [
          'instrumentation',
          'permission',
          _USES_PERMISSION,
          '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):
        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()