diff --git a/api/Android.bp b/api/Android.bp index 8dff60af8bbd..0acd759bc73e 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -24,9 +24,8 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -python_binary_host { - name: "api_versions_trimmer", - srcs: ["api_versions_trimmer.py"], +python_defaults { + name: "python3_version_defaults", version: { py2: { enabled: false, @@ -38,6 +37,12 @@ python_binary_host { }, } +python_binary_host { + name: "api_versions_trimmer", + srcs: ["api_versions_trimmer.py"], + defaults: ["python3_version_defaults"], +} + python_test_host { name: "api_versions_trimmer_unittests", main: "api_versions_trimmer_unittests.py", @@ -45,17 +50,28 @@ python_test_host { "api_versions_trimmer_unittests.py", "api_versions_trimmer.py", ], + defaults: ["python3_version_defaults"], test_options: { unit_test: true, }, - version: { - py2: { - enabled: false, - }, - py3: { - enabled: true, - embedded_launcher: false, - }, +} + +python_binary_host { + name: "merge_annotation_zips", + srcs: ["merge_annotation_zips.py"], + defaults: ["python3_version_defaults"], +} + +python_test_host { + name: "merge_annotation_zips_test", + main: "merge_annotation_zips_test.py", + srcs: [ + "merge_annotation_zips.py", + "merge_annotation_zips_test.py", + ], + defaults: ["python3_version_defaults"], + test_options: { + unit_test: true, }, } diff --git a/api/merge_annotation_zips.py b/api/merge_annotation_zips.py new file mode 100755 index 000000000000..9c67d7bded76 --- /dev/null +++ b/api/merge_annotation_zips.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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. + +"""Script to merge annotation XML files (created by e.g. metalava).""" + +from pathlib import Path +import sys +import xml.etree.ElementTree as ET +import zipfile + + +def validate_xml_assumptions(root): + """Verify the format of the annotations XML matches expectations""" + prevName = "" + assert root.tag == 'root' + for child in root: + assert child.tag == 'item', 'unexpected tag: %s' % child.tag + assert list(child.attrib.keys()) == ['name'], 'unexpected attribs: %s' % child.attrib.keys() + assert prevName < child.get('name'), 'items unexpectedly not strictly sorted (possibly duplicate entries)' + prevName = child.get('name') + + +def merge_xml(a, b): + """Merge two annotation xml files""" + for xml in [a, b]: + validate_xml_assumptions(xml) + a.extend(b[:]) + a[:] = sorted(a[:], key=lambda x: x.get('name')) + validate_xml_assumptions(a) + + +def merge_zip_file(out_dir, zip_file): + """Merge the content of the zip_file into out_dir""" + for filename in zip_file.namelist(): + path = Path(out_dir, filename) + if path.exists(): + existing_xml = ET.parse(path) + with zip_file.open(filename) as other_file: + other_xml = ET.parse(other_file) + merge_xml(existing_xml.getroot(), other_xml.getroot()) + existing_xml.write(path, encoding='UTF-8', xml_declaration=True) + else: + zip_file.extract(filename, out_dir) + + +def main(): + out_dir = Path(sys.argv[1]) + zip_filenames = sys.argv[2:] + + assert not out_dir.exists() + out_dir.mkdir() + for zip_filename in zip_filenames: + with zipfile.ZipFile(zip_filename) as zip_file: + merge_zip_file(out_dir, zip_file) + + +if __name__ == "__main__": + main() diff --git a/api/merge_annotation_zips_test.py b/api/merge_annotation_zips_test.py new file mode 100644 index 000000000000..26795c47af9e --- /dev/null +++ b/api/merge_annotation_zips_test.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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. + +import io +from pathlib import Path +import tempfile +import unittest +import zipfile + +import merge_annotation_zips + + +zip_a = { + 'android/provider/annotations.xml': + """ + + + + + + + + + +""", + 'android/os/annotations.xml': + """ + + + + + +""" +} + +zip_b = { + 'android/provider/annotations.xml': + """ + + + + + + + + + + + + + +""" +} + +zip_c = { + 'android/app/annotations.xml': + """ + + + + +""" +} + +merged_provider = """ + + + + + + + + + + + + + + + + + + + + + +""" + + + +class MergeAnnotationZipsTest(unittest.TestCase): + + def test_merge_zips(self): + with tempfile.TemporaryDirectory() as out_dir: + for zip_content in [zip_a, zip_b, zip_c]: + f = io.BytesIO() + with zipfile.ZipFile(f, "w") as zip_file: + for filename, content in zip_content.items(): + zip_file.writestr(filename, content) + merge_annotation_zips.merge_zip_file(out_dir, zip_file) + + # Unchanged + self.assertEqual(zip_a['android/os/annotations.xml'], Path(out_dir, 'android/os/annotations.xml').read_text()) + self.assertEqual(zip_c['android/app/annotations.xml'], Path(out_dir, 'android/app/annotations.xml').read_text()) + + # Merged + self.assertEqual(merged_provider, Path(out_dir, 'android/provider/annotations.xml').read_text()) + + +if __name__ == "__main__": + unittest.main(verbosity=2)