Add manifest_check tool

Add a tool that can check that the <uses-library> tags in an
AndroidManifest.xml file match a list provided by the build.

Bug: 132357300
Test: manifest_check_test
Change-Id: If15abf792282bef677469595e80f19923b87ab62
This commit is contained in:
Colin Cross 2019-05-20 13:14:18 -07:00
parent 4af387c20e
commit 7211910fd0
9 changed files with 559 additions and 101 deletions

View file

@ -142,6 +142,7 @@ func init() {
hostBinToolVariableWithPrebuilt("Aapt2Cmd", "prebuilts/sdk/tools", "aapt2") hostBinToolVariableWithPrebuilt("Aapt2Cmd", "prebuilts/sdk/tools", "aapt2")
pctx.HostBinToolVariable("ManifestCheckCmd", "manifest_check")
pctx.HostBinToolVariable("ManifestFixerCmd", "manifest_fixer") pctx.HostBinToolVariable("ManifestFixerCmd", "manifest_fixer")
pctx.HostBinToolVariable("ManifestMergerCmd", "manifest-merger") pctx.HostBinToolVariable("ManifestMergerCmd", "manifest-merger")

View file

@ -73,6 +73,7 @@ func makeVarsProvider(ctx android.MakeVarsContext) {
ctx.Strict("EXTRACT_JAR_PACKAGES", "${ExtractJarPackagesCmd}") ctx.Strict("EXTRACT_JAR_PACKAGES", "${ExtractJarPackagesCmd}")
ctx.Strict("MANIFEST_CHECK", "${ManifestCheckCmd}")
ctx.Strict("MANIFEST_FIXER", "${ManifestFixerCmd}") ctx.Strict("MANIFEST_FIXER", "${ManifestFixerCmd}")
ctx.Strict("ANDROID_MANIFEST_MERGER", "${ManifestMergerCmd}") ctx.Strict("ANDROID_MANIFEST_MERGER", "${ManifestMergerCmd}")

View file

@ -3,6 +3,7 @@ python_binary_host {
main: "manifest_fixer.py", main: "manifest_fixer.py",
srcs: [ srcs: [
"manifest_fixer.py", "manifest_fixer.py",
"manifest.py",
], ],
version: { version: {
py2: { py2: {
@ -20,6 +21,43 @@ python_test_host {
srcs: [ srcs: [
"manifest_fixer_test.py", "manifest_fixer_test.py",
"manifest_fixer.py", "manifest_fixer.py",
"manifest.py",
],
version: {
py2: {
enabled: true,
},
py3: {
enabled: false,
},
},
test_suites: ["general-tests"],
}
python_binary_host {
name: "manifest_check",
main: "manifest_check.py",
srcs: [
"manifest_check.py",
"manifest.py",
],
version: {
py2: {
enabled: true,
},
py3: {
enabled: false,
},
},
}
python_test_host {
name: "manifest_check_test",
main: "manifest_check_test.py",
srcs: [
"manifest_check_test.py",
"manifest_check.py",
"manifest.py",
], ],
version: { version: {
py2: { py2: {

View file

@ -1,8 +1,12 @@
{ {
"presubmit" : [ "presubmit" : [
{
"name": "manifest_check_test",
"host": true
},
{ {
"name": "manifest_fixer_test", "name": "manifest_fixer_test",
"host": true "host": true
} }
] ]
} }

117
scripts/manifest.py Executable file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python
#
# Copyright (C) 2018 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.
#
"""A tool for inserting values from the build system into a manifest."""
from __future__ import print_function
from xml.dom import minidom
android_ns = 'http://schemas.android.com/apk/res/android'
def get_children_with_tag(parent, tag_name):
children = []
for child in parent.childNodes:
if child.nodeType == minidom.Node.ELEMENT_NODE and \
child.tagName == tag_name:
children.append(child)
return children
def find_child_with_attribute(element, tag_name, namespace_uri,
attr_name, value):
for child in get_children_with_tag(element, tag_name):
attr = child.getAttributeNodeNS(namespace_uri, attr_name)
if attr is not None and attr.value == value:
return child
return None
def parse_manifest(doc):
"""Get the manifest element."""
manifest = doc.documentElement
if manifest.tagName != 'manifest':
raise RuntimeError('expected manifest tag at root')
return manifest
def ensure_manifest_android_ns(doc):
"""Make sure the manifest tag defines the android namespace."""
manifest = parse_manifest(doc)
ns = manifest.getAttributeNodeNS(minidom.XMLNS_NAMESPACE, 'android')
if ns is None:
attr = doc.createAttributeNS(minidom.XMLNS_NAMESPACE, 'xmlns:android')
attr.value = android_ns
manifest.setAttributeNode(attr)
elif ns.value != android_ns:
raise RuntimeError('manifest tag has incorrect android namespace ' +
ns.value)
def as_int(s):
try:
i = int(s)
except ValueError:
return s, False
return i, True
def compare_version_gt(a, b):
"""Compare two SDK versions.
Compares a and b, treating codenames like 'Q' as higher
than numerical versions like '28'.
Returns True if a > b
Args:
a: value to compare
b: value to compare
Returns:
True if a is a higher version than b
"""
a, a_is_int = as_int(a.upper())
b, b_is_int = as_int(b.upper())
if a_is_int == b_is_int:
# Both are codenames or both are versions, compare directly
return a > b
else:
# One is a codename, the other is not. Return true if
# b is an integer version
return b_is_int
def get_indent(element, default_level):
indent = ''
if element is not None and element.nodeType == minidom.Node.TEXT_NODE:
text = element.nodeValue
indent = text[:len(text)-len(text.lstrip())]
if not indent or indent == '\n':
# 1 indent = 4 space
indent = '\n' + (' ' * default_level * 4)
return indent
def write_xml(f, doc):
f.write('<?xml version="1.0" encoding="utf-8"?>\n')
for node in doc.childNodes:
f.write(node.toxml(encoding='utf-8') + '\n')

215
scripts/manifest_check.py Executable file
View file

@ -0,0 +1,215 @@
#!/usr/bin/env python
#
# Copyright (C) 2018 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.
#
"""A tool for checking that a manifest agrees with the build system."""
from __future__ import print_function
import argparse
import sys
from xml.dom import minidom
from manifest import android_ns
from manifest import get_children_with_tag
from manifest import parse_manifest
from manifest import write_xml
class ManifestMismatchError(Exception):
pass
def parse_args():
"""Parse commandline arguments."""
parser = argparse.ArgumentParser()
parser.add_argument('--uses-library', dest='uses_libraries',
action='append',
help='specify uses-library entries known to the build system')
parser.add_argument('--optional-uses-library',
dest='optional_uses_libraries',
action='append',
help='specify uses-library entries known to the build system with required:false')
parser.add_argument('--enforce-uses-libraries',
dest='enforce_uses_libraries',
action='store_true',
help='check the uses-library entries known to the build system against the manifest')
parser.add_argument('--extract-target-sdk-version',
dest='extract_target_sdk_version',
action='store_true',
help='print the targetSdkVersion from the manifest')
parser.add_argument('--output', '-o', dest='output', help='output AndroidManifest.xml file')
parser.add_argument('input', help='input AndroidManifest.xml file')
return parser.parse_args()
def enforce_uses_libraries(doc, uses_libraries, optional_uses_libraries):
"""Verify that the <uses-library> tags in the manifest match those provided by the build system.
Args:
doc: The XML document.
uses_libraries: The names of <uses-library> tags known to the build system
optional_uses_libraries: The names of <uses-library> tags with required:fals
known to the build system
Raises:
RuntimeError: Invalid manifest
ManifestMismatchError: Manifest does not match
"""
manifest = parse_manifest(doc)
elems = get_children_with_tag(manifest, 'application')
application = elems[0] if len(elems) == 1 else None
if len(elems) > 1:
raise RuntimeError('found multiple <application> tags')
elif not elems:
if uses_libraries or optional_uses_libraries:
raise ManifestMismatchError('no <application> tag found')
return
verify_uses_library(application, uses_libraries, optional_uses_libraries)
def verify_uses_library(application, uses_libraries, optional_uses_libraries):
"""Verify that the uses-library values known to the build system match the manifest.
Args:
application: the <application> tag in the manifest.
uses_libraries: the names of expected <uses-library> tags.
optional_uses_libraries: the names of expected <uses-library> tags with required="false".
Raises:
ManifestMismatchError: Manifest does not match
"""
if uses_libraries is None:
uses_libraries = []
if optional_uses_libraries is None:
optional_uses_libraries = []
manifest_uses_libraries, manifest_optional_uses_libraries = parse_uses_library(application)
err = []
if manifest_uses_libraries != uses_libraries:
err.append('Expected required <uses-library> tags "%s", got "%s"' %
(', '.join(uses_libraries), ', '.join(manifest_uses_libraries)))
if manifest_optional_uses_libraries != optional_uses_libraries:
err.append('Expected optional <uses-library> tags "%s", got "%s"' %
(', '.join(optional_uses_libraries), ', '.join(manifest_optional_uses_libraries)))
if err:
raise ManifestMismatchError('\n'.join(err))
def parse_uses_library(application):
"""Extract uses-library tags from the manifest.
Args:
application: the <application> tag in the manifest.
"""
libs = get_children_with_tag(application, 'uses-library')
uses_libraries = [uses_library_name(x) for x in libs if uses_library_required(x)]
optional_uses_libraries = [uses_library_name(x) for x in libs if not uses_library_required(x)]
return first_unique_elements(uses_libraries), first_unique_elements(optional_uses_libraries)
def first_unique_elements(l):
result = []
[result.append(x) for x in l if x not in result]
return result
def uses_library_name(lib):
"""Extract the name attribute of a uses-library tag.
Args:
lib: a <uses-library> tag.
"""
name = lib.getAttributeNodeNS(android_ns, 'name')
return name.value if name is not None else ""
def uses_library_required(lib):
"""Extract the required attribute of a uses-library tag.
Args:
lib: a <uses-library> tag.
"""
required = lib.getAttributeNodeNS(android_ns, 'required')
return (required.value == 'true') if required is not None else True
def extract_target_sdk_version(doc):
"""Returns the targetSdkVersion from the manifest.
Args:
doc: The XML document.
Raises:
RuntimeError: invalid manifest
"""
manifest = parse_manifest(doc)
# Get or insert the uses-sdk element
uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
if len(uses_sdk) > 1:
raise RuntimeError('found multiple uses-sdk elements')
elif len(uses_sdk) == 0:
raise RuntimeError('missing uses-sdk element')
uses_sdk = uses_sdk[0]
min_attr = uses_sdk.getAttributeNodeNS(android_ns, 'minSdkVersion')
if min_attr is None:
raise RuntimeError('minSdkVersion is not specified')
target_attr = uses_sdk.getAttributeNodeNS(android_ns, 'targetSdkVersion')
if target_attr is None:
target_attr = min_attr
return target_attr.value
def main():
"""Program entry point."""
try:
args = parse_args()
doc = minidom.parse(args.input)
if args.enforce_uses_libraries:
enforce_uses_libraries(doc,
args.uses_libraries,
args.optional_uses_libraries)
if args.extract_target_sdk_version:
print(extract_target_sdk_version(doc))
if args.output:
with open(args.output, 'wb') as f:
write_xml(f, doc)
# pylint: disable=broad-except
except Exception as err:
print('error: ' + str(err), file=sys.stderr)
sys.exit(-1)
if __name__ == '__main__':
main()

166
scripts/manifest_check_test.py Executable file
View file

@ -0,0 +1,166 @@
#!/usr/bin/env python
#
# Copyright (C) 2018 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.
#
"""Unit tests for manifest_fixer.py."""
import sys
import unittest
from xml.dom import minidom
import manifest_check
sys.dont_write_bytecode = True
def uses_library(name, attr=''):
return '<uses-library android:name="%s"%s />' % (name, attr)
def required(value):
return ' android:required="%s"' % ('true' if value else 'false')
class EnforceUsesLibrariesTest(unittest.TestCase):
"""Unit tests for add_extract_native_libs function."""
def run_test(self, input_manifest, uses_libraries=None, optional_uses_libraries=None):
doc = minidom.parseString(input_manifest)
try:
manifest_check.enforce_uses_libraries(doc, uses_libraries, optional_uses_libraries)
return True
except manifest_check.ManifestMismatchError:
return False
manifest_tmpl = (
'<?xml version="1.0" encoding="utf-8"?>\n'
'<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
' <application>\n'
' %s\n'
' </application>\n'
'</manifest>\n')
def test_uses_library(self):
manifest_input = self.manifest_tmpl % (uses_library('foo'))
matches = self.run_test(manifest_input, uses_libraries=['foo'])
self.assertTrue(matches)
def test_uses_library_required(self):
manifest_input = self.manifest_tmpl % (uses_library('foo', required(True)))
matches = self.run_test(manifest_input, uses_libraries=['foo'])
self.assertTrue(matches)
def test_optional_uses_library(self):
manifest_input = self.manifest_tmpl % (uses_library('foo', required(False)))
matches = self.run_test(manifest_input, optional_uses_libraries=['foo'])
self.assertTrue(matches)
def test_expected_uses_library(self):
manifest_input = self.manifest_tmpl % (uses_library('foo', required(False)))
matches = self.run_test(manifest_input, uses_libraries=['foo'])
self.assertFalse(matches)
def test_expected_optional_uses_library(self):
manifest_input = self.manifest_tmpl % (uses_library('foo'))
matches = self.run_test(manifest_input, optional_uses_libraries=['foo'])
self.assertFalse(matches)
def test_missing_uses_library(self):
manifest_input = self.manifest_tmpl % ('')
matches = self.run_test(manifest_input, uses_libraries=['foo'])
self.assertFalse(matches)
def test_missing_optional_uses_library(self):
manifest_input = self.manifest_tmpl % ('')
matches = self.run_test(manifest_input, optional_uses_libraries=['foo'])
self.assertFalse(matches)
def test_extra_uses_library(self):
manifest_input = self.manifest_tmpl % (uses_library('foo'))
matches = self.run_test(manifest_input)
self.assertFalse(matches)
def test_extra_optional_uses_library(self):
manifest_input = self.manifest_tmpl % (uses_library('foo', required(False)))
matches = self.run_test(manifest_input)
self.assertFalse(matches)
def test_multiple_uses_library(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo'),
uses_library('bar')]))
matches = self.run_test(manifest_input, uses_libraries=['foo', 'bar'])
self.assertTrue(matches)
def test_multiple_optional_uses_library(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo', required(False)),
uses_library('bar', required(False))]))
matches = self.run_test(manifest_input, optional_uses_libraries=['foo', 'bar'])
self.assertTrue(matches)
def test_order_uses_library(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo'),
uses_library('bar')]))
matches = self.run_test(manifest_input, uses_libraries=['bar', 'foo'])
self.assertFalse(matches)
def test_order_optional_uses_library(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo', required(False)),
uses_library('bar', required(False))]))
matches = self.run_test(manifest_input, optional_uses_libraries=['bar', 'foo'])
self.assertFalse(matches)
def test_duplicate_uses_library(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo'),
uses_library('foo')]))
matches = self.run_test(manifest_input, uses_libraries=['foo'])
self.assertTrue(matches)
def test_duplicate_optional_uses_library(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo', required(False)),
uses_library('foo', required(False))]))
matches = self.run_test(manifest_input, optional_uses_libraries=['foo'])
self.assertTrue(matches)
def test_mixed(self):
manifest_input = self.manifest_tmpl % ('\n'.join([uses_library('foo'),
uses_library('bar', required(False))]))
matches = self.run_test(manifest_input, uses_libraries=['foo'],
optional_uses_libraries=['bar'])
self.assertTrue(matches)
class ExtractTargetSdkVersionTest(unittest.TestCase):
def test_target_sdk_version(self):
manifest = (
'<?xml version="1.0" encoding="utf-8"?>\n'
'<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
' <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="29" />\n'
'</manifest>\n')
doc = minidom.parseString(manifest)
target_sdk_version = manifest_check.extract_target_sdk_version(doc)
self.assertEqual(target_sdk_version, '29')
def test_min_sdk_version(self):
manifest = (
'<?xml version="1.0" encoding="utf-8"?>\n'
'<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
' <uses-sdk android:minSdkVersion="28" />\n'
'</manifest>\n')
doc = minidom.parseString(manifest)
target_sdk_version = manifest_check.extract_target_sdk_version(doc)
self.assertEqual(target_sdk_version, '28')
if __name__ == '__main__':
unittest.main(verbosity=2)

View file

@ -17,30 +17,20 @@
"""A tool for inserting values from the build system into a manifest.""" """A tool for inserting values from the build system into a manifest."""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import sys import sys
from xml.dom import minidom from xml.dom import minidom
android_ns = 'http://schemas.android.com/apk/res/android' from manifest import android_ns
from manifest import compare_version_gt
from manifest import ensure_manifest_android_ns
def get_children_with_tag(parent, tag_name): from manifest import find_child_with_attribute
children = [] from manifest import get_children_with_tag
for child in parent.childNodes: from manifest import get_indent
if child.nodeType == minidom.Node.ELEMENT_NODE and \ from manifest import parse_manifest
child.tagName == tag_name: from manifest import write_xml
children.append(child)
return children
def find_child_with_attribute(element, tag_name, namespace_uri,
attr_name, value):
for child in get_children_with_tag(element, tag_name):
attr = child.getAttributeNodeNS(namespace_uri, attr_name)
if attr is not None and attr.value == value:
return child
return None
def parse_args(): def parse_args():
@ -74,76 +64,6 @@ def parse_args():
return parser.parse_args() return parser.parse_args()
def parse_manifest(doc):
"""Get the manifest element."""
manifest = doc.documentElement
if manifest.tagName != 'manifest':
raise RuntimeError('expected manifest tag at root')
return manifest
def ensure_manifest_android_ns(doc):
"""Make sure the manifest tag defines the android namespace."""
manifest = parse_manifest(doc)
ns = manifest.getAttributeNodeNS(minidom.XMLNS_NAMESPACE, 'android')
if ns is None:
attr = doc.createAttributeNS(minidom.XMLNS_NAMESPACE, 'xmlns:android')
attr.value = android_ns
manifest.setAttributeNode(attr)
elif ns.value != android_ns:
raise RuntimeError('manifest tag has incorrect android namespace ' +
ns.value)
def as_int(s):
try:
i = int(s)
except ValueError:
return s, False
return i, True
def compare_version_gt(a, b):
"""Compare two SDK versions.
Compares a and b, treating codenames like 'Q' as higher
than numerical versions like '28'.
Returns True if a > b
Args:
a: value to compare
b: value to compare
Returns:
True if a is a higher version than b
"""
a, a_is_int = as_int(a.upper())
b, b_is_int = as_int(b.upper())
if a_is_int == b_is_int:
# Both are codenames or both are versions, compare directly
return a > b
else:
# One is a codename, the other is not. Return true if
# b is an integer version
return b_is_int
def get_indent(element, default_level):
indent = ''
if element is not None and element.nodeType == minidom.Node.TEXT_NODE:
text = element.nodeValue
indent = text[:len(text)-len(text.lstrip())]
if not indent or indent == '\n':
# 1 indent = 4 space
indent = '\n' + (' ' * default_level * 4)
return indent
def raise_min_sdk_version(doc, min_sdk_version, target_sdk_version, library): def raise_min_sdk_version(doc, min_sdk_version, target_sdk_version, library):
"""Ensure the manifest contains a <uses-sdk> tag with a minSdkVersion. """Ensure the manifest contains a <uses-sdk> tag with a minSdkVersion.
@ -151,6 +71,7 @@ def raise_min_sdk_version(doc, min_sdk_version, target_sdk_version, library):
doc: The XML document. May be modified by this function. doc: The XML document. May be modified by this function.
min_sdk_version: The requested minSdkVersion attribute. min_sdk_version: The requested minSdkVersion attribute.
target_sdk_version: The requested targetSdkVersion attribute. target_sdk_version: The requested targetSdkVersion attribute.
library: True if the manifest is for a library.
Raises: Raises:
RuntimeError: invalid manifest RuntimeError: invalid manifest
""" """
@ -249,6 +170,7 @@ def add_uses_libraries(doc, new_uses_libraries, required):
indent = get_indent(application.previousSibling, 1) indent = get_indent(application.previousSibling, 1)
application.appendChild(doc.createTextNode(indent)) application.appendChild(doc.createTextNode(indent))
def add_uses_non_sdk_api(doc): def add_uses_non_sdk_api(doc):
"""Add android:usesNonSdkApi=true attribute to <application>. """Add android:usesNonSdkApi=true attribute to <application>.
@ -323,12 +245,6 @@ def add_extract_native_libs(doc, extract_native_libs):
(attr.value, value)) (attr.value, value))
def write_xml(f, doc):
f.write('<?xml version="1.0" encoding="utf-8"?>\n')
for node in doc.childNodes:
f.write(node.toxml(encoding='utf-8') + '\n')
def main(): def main():
"""Program entry point.""" """Program entry point."""
try: try:

View file

@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
"""Unit tests for manifest_fixer_test.py.""" """Unit tests for manifest_fixer.py."""
import StringIO import StringIO
import sys import sys
@ -393,10 +393,10 @@ class AddExtractNativeLibsTest(unittest.TestCase):
return output.getvalue() return output.getvalue()
manifest_tmpl = ( manifest_tmpl = (
'<?xml version="1.0" encoding="utf-8"?>\n' '<?xml version="1.0" encoding="utf-8"?>\n'
'<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n' '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
' <application%s/>\n' ' <application%s/>\n'
'</manifest>\n') '</manifest>\n')
def extract_native_libs(self, value): def extract_native_libs(self, value):
return ' android:extractNativeLibs="%s"' % value return ' android:extractNativeLibs="%s"' % value