#!/usr/bin/env python3 """Collect development/unsigned outputs with source identity and inspection evidence.""" import hashlib import json import os from pathlib import Path import re import shutil import subprocess import zipfile ROOT = Path(__file__).resolve().parents[1] BUILD = Path(os.environ.get("YUVOMI_BUILD_DIR", ROOT / "android")) APP = BUILD / "app" if "YUVOMI_BUILD_DIR" in os.environ else BUILD / "app/build" SDK = Path(os.environ["ANDROID_HOME"]) TOOLS = SDK / "build-tools/36.0.0" DESTINATION = ROOT / "artifacts/android" DESTINATION.mkdir(parents=True, exist_ok=True) def tool(name, *arguments): return subprocess.check_output([str(TOOLS / name), *map(str, arguments)], text=True) release = APP / "outputs/apk/release/app-release-unsigned.apk" configuration = (ROOT / "android/app/build.gradle.kts").read_text() version_name = re.search(r'versionName = "([^"]+)"', configuration).group(1) version_code = re.search(r'versionCode = (\d+)', configuration).group(1) assert "native" in version_name, "Collector expects the native preview, not the archived wrapper" manifest = tool("aapt2", "dump", "xmltree", release, "--file", "AndroidManifest.xml") assert 'package="community.yuvomi.mobile"' in manifest assert re.search(r":allowBackup\([^\n]*=false", manifest) assert re.search(r":usesCleartextTraffic\([^\n]*=false", manifest) assert not re.search(r":debuggable\([^\n]*=true", manifest) resources = tool("aapt2", "dump", "resources", release) network = re.search(r"xml/network_security_config\s+\(\) \(file\) (\S+)", resources) assert network, "Missing production network security configuration" policy = tool("aapt2", "dump", "xmltree", release, "--file", network.group(1)) assert "cleartextTrafficPermitted=false" in policy assert policy.count("E: certificates") == 1 and 'src="system"' in policy (DESTINATION / "release-manifest.txt").write_text(manifest) (DESTINATION / "release-network-policy.txt").write_text(policy) debug = APP / "outputs/apk/debug/app-debug.apk" for apk in [debug, release]: identity = tool("aapt2", "dump", "badging", apk).splitlines()[0] assert f"versionCode='{version_code}'" in identity and f"versionName='{version_name}'" in identity, identity (DESTINATION / "debug-signature.txt").write_text(tool("apksigner", "verify", "--verbose", debug)) unsigned = subprocess.run([str(TOOLS / "apksigner"), "verify", str(release)], capture_output=True, text=True) assert unsigned.returncode != 0 and "Missing META-INF/MANIFEST.MF" in unsigned.stderr, unsigned.stderr entries = [] for source in [debug, release, APP / "outputs/bundle/release/app-release.aab"]: with zipfile.ZipFile(source) as archive: for name in archive.namelist(): assert "integration_ca" not in name and not name.endswith((".pem", ".jks", ".keystore", ".p12")), name if source.suffix == ".aab": assert not any(name.upper().startswith("META-INF/") and name.upper().endswith((".RSA", ".DSA", ".EC")) for name in archive.namelist()), "Expected unsigned AAB" destination = DESTINATION / source.name shutil.copyfile(source, destination) entries.append({"file": source.name, "bytes": destination.stat().st_size, "sha256": hashlib.sha256(destination.read_bytes()).hexdigest()}) revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip() dirty = bool(subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True).strip()) (DESTINATION / "SHA256SUMS").write_text("".join(f"{entry['sha256']} {entry['file']}\n" for entry in entries)) (DESTINATION / "build-info.json").write_text(json.dumps({ "source_revision": revision, "worktree_dirty_at_collection": dirty, "upstream_revision": "10bfef7686ed7303b4c74c7fcc55a5eb16356c9d", "version_name": version_name, "version_code": int(version_code), "renderer": "native Jetpack Compose", "status": "Incomplete native preview: development debug APK; unsigned release APK/AAB. No full PWA parity or production approval.", "artifacts": entries, }, indent=2) + "\n") print(f"Inspected and collected {len(entries)} artifacts in {DESTINATION}") print("Build source identity assumes the documented build ran immediately before collection.")