Files
Yuvomi-App/scripts/collect-android-artifacts.py
T
Yuvomi Mobile Contributors f7b9dcd91e build: collect inspected unsigned Android artifacts with checksums
Verify the production manifest, system-only trust, development APK signature and unsigned release outputs before collecting local artifacts. Record source identity and hashes outside Git. Exercised successfully against the built APKs and AAB; collection must follow a clean-source build.
2026-10-21 07:30:51 +02:00

69 lines
3.5 KiB
Python

#!/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"
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"
(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",
"status": "Development debug APK; unsigned release APK/AAB. Not production-ready.",
"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.")