1234from pathlib import Path5import sys6import xml.etree.ElementTree as ET78ANDROID_NS = "{http://schemas.android.com/apk/res/android}"9ROOT = Path(__file__).resolve().parents[1]10DEFAULT_INVENTORY = ROOT / "docs" / "android-release-manifest-inventory.txt"11DEFAULT_MERGED = ROOT / "app" / "build" / "intermediates" / "merged_manifest" / "release" / "processReleaseMainManifest" / "AndroidManifest.xml"121314def attrs(node: ET.Element) -> str:15 1617181920 items = sorted(21 (key.rsplit("}", 1)[-1], value) for key, value in node.attrib.items()22 )23 return ";".join(f"{key}={value}" for key, value in items)242526def inventory(path: Path) -> str:27 root = ET.parse(path).getroot()28 lines = []29 for permission in sorted(root.findall("uses-permission"), key=lambda node: node.get(ANDROID_NS + "name", "")):30 lines.append(31 f"permission|{permission.get(ANDROID_NS + 'name', '')}|{attrs(permission)}"32 )33 for feature in sorted(root.findall("uses-feature"), key=lambda node: node.get(ANDROID_NS + "name", "")):34 lines.append(35 f"uses-feature|{feature.get(ANDROID_NS + 'name', '')}|{attrs(feature)}"36 )37 for queries in root.findall("queries"):38 lines.append(f"queries|{attrs(queries)}")39 for child in sorted(queries, key=lambda node: (node.tag, node.get(ANDROID_NS + "name", ""))):40 lines.append(41 f"query|{child.tag}|{child.get(ANDROID_NS + 'name', '')}|{attrs(child)}"42 )43 for data in sorted(child.findall("data"), key=attrs):44 lines.append(f"query-data|{child.tag}|{attrs(data)}")45 application = root.find("application")46 if application is None:47 raise SystemExit("release manifest has no application")48 49 50 51 52 53 lines.append(f"application|{attrs(application)}")54 for child in sorted(application, key=lambda node: (node.tag, node.get(ANDROID_NS + "name", ""))):55 name = child.get(ANDROID_NS + "name", "")56 57 58 59 lines.append(f"component|{child.tag}|{name}|{attrs(child)}")60 for intent_filter in child.findall("intent-filter"):61 actions = sorted(action.get(ANDROID_NS + "name", "") for action in intent_filter.findall("action"))62 categories = sorted(category.get(ANDROID_NS + "name", "") for category in intent_filter.findall("category"))63 lines.append(f"intent-filter|{child.tag}|{name}|actions={','.join(actions)}|categories={','.join(categories)}")64 for data in sorted(intent_filter.findall("data"), key=attrs):65 lines.append(f"intent-filter-data|{child.tag}|{name}|{attrs(data)}")66 return "\n".join(lines) + "\n"676869USAGE = f"usage: {Path(sys.argv[0]).name} check|write [merged-manifest] [inventory]"7071if len(sys.argv) not in (2, 3, 4):72 raise SystemExit(USAGE)73MERGED = Path(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_MERGED74INVENTORY = Path(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_INVENTORY7576if not MERGED.is_file():77 raise SystemExit(f"merged release manifest not found: {MERGED}")7879if sys.argv[1] == "write":80 INVENTORY.write_text(inventory(MERGED))81elif sys.argv[1] == "check":82 if not INVENTORY.is_file():83 raise SystemExit(f"checked-in inventory not found: {INVENTORY}")84 actual = inventory(MERGED)85 expected = INVENTORY.read_text()86 if actual != expected:87 raise SystemExit("merged release manifest differs from checked-in inventory")88else:89 raise SystemExit(f"unknown command: {sys.argv[1]}\n{USAGE}")