From 7211910fd0d158cd2a4e7d610f1e32b8c9d25817 Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Mon, 20 May 2019 13:14:18 -0700 Subject: Add manifest_check tool Add a tool that can check that the tags in an AndroidManifest.xml file match a list provided by the build. Bug: 132357300 Test: manifest_check_test Change-Id: If15abf792282bef677469595e80f19923b87ab62 --- scripts/Android.bp | 38 ++++++++ scripts/TEST_MAPPING | 6 +- scripts/manifest.py | 117 ++++++++++++++++++++++ scripts/manifest_check.py | 215 +++++++++++++++++++++++++++++++++++++++++ scripts/manifest_check_test.py | 166 +++++++++++++++++++++++++++++++ scripts/manifest_fixer.py | 106 +++----------------- scripts/manifest_fixer_test.py | 10 +- 7 files changed, 557 insertions(+), 101 deletions(-) create mode 100755 scripts/manifest.py create mode 100755 scripts/manifest_check.py create mode 100755 scripts/manifest_check_test.py (limited to 'scripts') diff --git a/scripts/Android.bp b/scripts/Android.bp index 35b57718..31f59227 100644 --- a/scripts/Android.bp +++ b/scripts/Android.bp @@ -3,6 +3,7 @@ python_binary_host { main: "manifest_fixer.py", srcs: [ "manifest_fixer.py", + "manifest.py", ], version: { py2: { @@ -20,6 +21,43 @@ python_test_host { srcs: [ "manifest_fixer_test.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: { py2: { diff --git a/scripts/TEST_MAPPING b/scripts/TEST_MAPPING index cafecdee..1b0a2298 100644 --- a/scripts/TEST_MAPPING +++ b/scripts/TEST_MAPPING @@ -1,8 +1,12 @@ { "presubmit" : [ + { + "name": "manifest_check_test", + "host": true + }, { "name": "manifest_fixer_test", "host": true - } + } ] } diff --git a/scripts/manifest.py b/scripts/manifest.py new file mode 100755 index 00000000..4c75f8bf --- /dev/null +++ b/scripts/manifest.py @@ -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('\n') + for node in doc.childNodes: + f.write(node.toxml(encoding='utf-8') + '\n') diff --git a/scripts/manifest_check.py b/scripts/manifest_check.py new file mode 100755 index 00000000..9122da1f --- /dev/null +++ b/scripts/manifest_check.py @@ -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 tags in the manifest match those provided by the build system. + + Args: + doc: The XML document. + uses_libraries: The names of tags known to the build system + optional_uses_libraries: The names of 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 tags') + elif not elems: + if uses_libraries or optional_uses_libraries: + raise ManifestMismatchError('no 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 tag in the manifest. + uses_libraries: the names of expected tags. + optional_uses_libraries: the names of expected 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 tags "%s", got "%s"' % + (', '.join(uses_libraries), ', '.join(manifest_uses_libraries))) + + if manifest_optional_uses_libraries != optional_uses_libraries: + err.append('Expected optional 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 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 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 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() diff --git a/scripts/manifest_check_test.py b/scripts/manifest_check_test.py new file mode 100755 index 00000000..7baad5d3 --- /dev/null +++ b/scripts/manifest_check_test.py @@ -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 '' % (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 = ( + '\n' + '\n' + ' \n' + ' %s\n' + ' \n' + '\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 = ( + '\n' + '\n' + ' \n' + '\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 = ( + '\n' + '\n' + ' \n' + '\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) diff --git a/scripts/manifest_fixer.py b/scripts/manifest_fixer.py index 83868e60..bb148513 100755 --- a/scripts/manifest_fixer.py +++ b/scripts/manifest_fixer.py @@ -17,30 +17,20 @@ """A tool for inserting values from the build system into a manifest.""" from __future__ import print_function + import argparse import sys 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 +from manifest import android_ns +from manifest import compare_version_gt +from manifest import ensure_manifest_android_ns +from manifest import find_child_with_attribute +from manifest import get_children_with_tag +from manifest import get_indent +from manifest import parse_manifest +from manifest import write_xml def parse_args(): @@ -74,76 +64,6 @@ def 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): """Ensure the manifest contains a 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. min_sdk_version: The requested minSdkVersion attribute. target_sdk_version: The requested targetSdkVersion attribute. + library: True if the manifest is for a library. Raises: RuntimeError: invalid manifest """ @@ -249,6 +170,7 @@ def add_uses_libraries(doc, new_uses_libraries, required): indent = get_indent(application.previousSibling, 1) application.appendChild(doc.createTextNode(indent)) + def add_uses_non_sdk_api(doc): """Add android:usesNonSdkApi=true attribute to . @@ -323,12 +245,6 @@ def add_extract_native_libs(doc, extract_native_libs): (attr.value, value)) -def write_xml(f, doc): - f.write('\n') - for node in doc.childNodes: - f.write(node.toxml(encoding='utf-8') + '\n') - - def main(): """Program entry point.""" try: diff --git a/scripts/manifest_fixer_test.py b/scripts/manifest_fixer_test.py index 7fbbc17b..20354218 100755 --- a/scripts/manifest_fixer_test.py +++ b/scripts/manifest_fixer_test.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Unit tests for manifest_fixer_test.py.""" +"""Unit tests for manifest_fixer.py.""" import StringIO import sys @@ -393,10 +393,10 @@ class AddExtractNativeLibsTest(unittest.TestCase): return output.getvalue() manifest_tmpl = ( - '\n' - '\n' - ' \n' - '\n') + '\n' + '\n' + ' \n' + '\n') def extract_native_libs(self, value): return ' android:extractNativeLibs="%s"' % value -- cgit v1.2.3