"""Contains common helpers for working with Android manifests."""
import hashlib
import os
import re
import shlex
import sys
import xml.dom.minidom as minidom
from xml.etree import ElementTree
from util import build_utils
import action_helpers
ANDROID_NAMESPACE = 'http://schemas.android.com/apk/res/android'
TOOLS_NAMESPACE = 'http://schemas.android.com/tools'
DIST_NAMESPACE = 'http://schemas.android.com/apk/distribution'
EMPTY_ANDROID_MANIFEST_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', 'AndroidManifest.xml'))
_WRAP_CANDIDATES = (
'<manifest',
'<application',
'<activity',
'<provider',
'<receiver',
'<service',
)
_WRAP_LINE_LENGTH = 100
_xml_namespace_initialized = False
def _RegisterElementTreeNamespaces():
global _xml_namespace_initialized
if _xml_namespace_initialized:
return
_xml_namespace_initialized = True
ElementTree.register_namespace('android', ANDROID_NAMESPACE)
ElementTree.register_namespace('tools', TOOLS_NAMESPACE)
ElementTree.register_namespace('dist', DIST_NAMESPACE)
def NamespacedGet(node, key):
return node.get('{%s}%s' % (ANDROID_NAMESPACE, key))
def NamespacedSet(node, key, value):
node.set('{%s}%s' % (ANDROID_NAMESPACE, key), value)
def ParseManifest(path):
"""Parses an AndroidManifest.xml using ElementTree.
Registers required namespaces, creates application node if missing, adds any
missing namespaces for 'android', 'tools' and 'dist'.
Returns tuple of:
doc: Root xml document.
manifest_node: the <manifest> node.
app_node: the <application> node.
"""
_RegisterElementTreeNamespaces()
doc = ElementTree.parse(path)
if doc.getroot().tag == 'manifest':
manifest_node = doc.getroot()
else:
manifest_node = doc.find('manifest')
assert manifest_node is not None, 'Manifest is none for path ' + path
app_node = doc.find('application')
if app_node is None:
app_node = ElementTree.SubElement(manifest_node, 'application')
return doc, manifest_node, app_node
def SaveManifest(doc, path):
with action_helpers.atomic_output(path) as f:
f.write(ElementTree.tostring(doc.getroot(), encoding='UTF-8'))
def GetPackage(manifest_node):
return manifest_node.get('package')
def SetUsesSdk(manifest_node,
target_sdk_version,
min_sdk_version,
max_sdk_version=None):
uses_sdk_node = manifest_node.find('./uses-sdk')
if uses_sdk_node is None:
uses_sdk_node = ElementTree.SubElement(manifest_node, 'uses-sdk')
NamespacedSet(uses_sdk_node, 'targetSdkVersion', target_sdk_version)
NamespacedSet(uses_sdk_node, 'minSdkVersion', min_sdk_version)
if max_sdk_version:
NamespacedSet(uses_sdk_node, 'maxSdkVersion', max_sdk_version)
def SetTargetApiIfUnset(manifest_node, target_sdk_version):
uses_sdk_node = manifest_node.find('./uses-sdk')
if uses_sdk_node is None:
uses_sdk_node = ElementTree.SubElement(manifest_node, 'uses-sdk')
curr_target_sdk_version = NamespacedGet(uses_sdk_node, 'targetSdkVersion')
if curr_target_sdk_version is None:
NamespacedSet(uses_sdk_node, 'targetSdkVersion', target_sdk_version)
return curr_target_sdk_version is None
def _SortAndStripElementTree(root):
def element_sort_key(node):
if node.tag == 'application':
return 'z'
ret = ElementTree.tostring(node)
return re.sub(r' xmlns:.*?".*?"', '', ret.decode('utf8'))
name_attr = '{%s}name' % ANDROID_NAMESPACE
def attribute_sort_key(tup):
return ('', '') if tup[0] == name_attr else tup
def helper(node):
for child in node:
if child.text and child.text.isspace():
child.text = None
helper(child)
node.attrib = dict(sorted(node.attrib.items(), key=attribute_sort_key))
node[:] = sorted(node, key=element_sort_key)
helper(root)
def _SplitElement(line):
"""Parses a one-line xml node into ('<tag', ['a="b"', ...]], '/>')."""
def restore_quotes(value):
return value.replace('=', '="', 1) + '"'
assert line.endswith('>'), line
end_tag = '>'
if line.endswith('/>'):
end_tag = '/>'
line = line[:-len(end_tag)]
parts = shlex.split(line)
start_tag = parts[0]
attrs = parts[1:]
return start_tag, [restore_quotes(x) for x in attrs], end_tag
def _CreateNodeHash(lines):
"""Computes a hash (md5) for the first XML node found in |lines|.
Args:
lines: List of strings containing pretty-printed XML.
Returns:
Positive 32-bit integer hash of the node (including children).
"""
target_indent = lines[0].find('<')
tag_closed = False
for i, l in enumerate(lines[1:]):
cur_indent = l.find('<')
if cur_indent != -1 and cur_indent <= target_indent:
tag_lines = lines[:i + 1]
break
if not tag_closed and 'android:name="' in l:
tag_lines = [l]
break
tag_closed = tag_closed or '>' in l
else:
assert False, 'Did not find end of node:\n' + '\n'.join(lines)
return hashlib.md5(('\n'.join(tag_lines)).encode('utf8')).hexdigest()[:8]
def _IsSelfClosing(lines):
"""Given pretty-printed xml, returns whether first node is self-closing."""
for l in lines:
idx = l.find('>')
if idx != -1:
return l[idx - 1] == '/'
raise RuntimeError('Did not find end of tag:\n%s' % '\n'.join(lines))
def _AddDiffTags(lines):
hash_stack = []
for i, l in enumerate(lines):
stripped = l.lstrip()
if l[0] != ' ' or stripped[0] != '<':
continue
if l[-2:] == '/>':
continue
if stripped.lstrip('</').startswith('application'):
continue
if stripped[1] != '/':
cur_hash = _CreateNodeHash(lines[i:])
if not _IsSelfClosing(lines[i:]):
hash_stack.append(cur_hash)
else:
cur_hash = hash_stack.pop()
lines[i] += ' # DIFF-ANCHOR: {}'.format(cur_hash)
assert not hash_stack, 'hash_stack was not empty:\n' + '\n'.join(hash_stack)
def NormalizeManifest(manifest_contents, version_code_offset,
library_version_offset):
_RegisterElementTreeNamespaces()
root = ElementTree.fromstring(manifest_contents)
package = GetPackage(root)
app_node = root.find('application')
if app_node is not None:
debuggable_name = '{%s}debuggable' % ANDROID_NAMESPACE
if debuggable_name in app_node.attrib:
del app_node.attrib[debuggable_name]
version_code = NamespacedGet(root, 'versionCode')
if version_code and version_code_offset:
version_code = int(version_code) - int(version_code_offset)
NamespacedSet(root, 'versionCode', f'OFFSET={version_code}')
version_name = NamespacedGet(root, 'versionName')
if version_name:
version_name = re.sub(r'\d+', '#', version_name)
NamespacedSet(root, 'versionName', version_name)
for node in app_node:
if node.tag in ['uses-static-library', 'static-library']:
version = NamespacedGet(node, 'version')
if version and library_version_offset:
version = int(version) - int(library_version_offset)
NamespacedSet(node, 'version', f'OFFSET={version}')
def blur_package_name(node):
for key in node.keys():
node.set(key, node.get(key).replace(package, '$PACKAGE'))
for child in node:
blur_package_name(child)
for child in root:
blur_package_name(child)
_SortAndStripElementTree(root)
dom = minidom.parseString(ElementTree.tostring(root))
out_lines = []
for l in dom.toprettyxml(indent=' ').splitlines():
if not l or l.isspace():
continue
if len(l) > _WRAP_LINE_LENGTH and any(x in l for x in _WRAP_CANDIDATES):
indent = ' ' * l.find('<')
start_tag, attrs, end_tag = _SplitElement(l)
out_lines.append('{}{}'.format(indent, start_tag))
for attribute in attrs:
out_lines.append('{} {}'.format(indent, attribute))
out_lines[-1] += '>'
if end_tag == '/>':
out_lines.append('{}{}>'.format(indent, start_tag.replace('<', '</')))
else:
out_lines.append(l)
_AddDiffTags(out_lines)
return '\n'.join(out_lines) + '\n'