"""Build the disposable native fixture using an existing Android SDK/JDK only.""" import argparse import os from pathlib import Path import shutil import subprocess import tempfile import zipfile def build(output: Path) -> Path: sdk = Path( os.environ.get("ANDROID_HOME") or os.environ.get("ANDROID_SDK_ROOT") or Path.home() / "AppData/Local/Android/Sdk" ) versions = sorted( (sdk / "build-tools").glob("*"), key=lambda p: tuple(int(v) for v in p.name.split(".") if v.isdigit()), ) platforms = sorted( (sdk / "platforms").glob("android-*"), key=lambda p: int(p.name.split("-")[-1]) ) if not versions or not platforms: raise RuntimeError("An existing Android SDK with platform and build-tools is required.") tools = versions[-1] android = platforms[-1] / "android.jar" executable = ".exe" if os.name == "nt" else "" def run(*args: object) -> None: subprocess.run([str(arg) for arg in args], check=True, timeout=90) java = shutil.which("java") javac = shutil.which("javac") keytool = shutil.which("keytool") if not java or not javac or not keytool: raise RuntimeError("An existing JDK (java, javac and keytool) is required.") output = output.resolve() output.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="artemis-native-fixture-") as temporary: scratch = Path(temporary) classes = scratch / "classes" classes.mkdir() manifest = scratch / "AndroidManifest.xml" manifest.write_text( """ """, encoding="utf-8", ) unsigned = scratch / "unsigned.apk" run( tools / ("aapt2" + executable), "link", "-I", android, "--manifest", manifest, "--min-sdk-version", 26, "--target-sdk-version", 35, "--version-code", 1, "--version-name", "1.0", "-o", unsigned, ) run( javac, "-encoding", "UTF-8", "-source", 8, "-target", 8, "-Xlint:-options", "-cp", android, "-d", classes, Path(__file__).with_name("MainActivity.java"), ) run( java, "-cp", tools / "lib/d8.jar", "com.android.tools.r8.D8", "--lib", android, "--min-api", 26, "--output", scratch, *classes.rglob("*.class"), ) with zipfile.ZipFile(unsigned, "a", compression=zipfile.ZIP_DEFLATED) as archive: archive.write(scratch / "classes.dex", "classes.dex") aligned = scratch / "aligned.apk" run(tools / ("zipalign" + executable), "-f", "-p", 4, unsigned, aligned) key = scratch / "fixture.keystore" # Public throwaway debug signing material, never reused for distribution. run( keytool, "-genkeypair", "-keystore", key, "-storepass", "android", "-keypass", "android", "-alias", "fixture", "-keyalg", "RSA", "-validity", 365, "-dname", "CN=ARTEMIS Test Fixture", "-noprompt", ) run( java, "-jar", tools / "lib/apksigner.jar", "sign", "--ks", key, "--ks-pass", "pass:android", "--key-pass", "pass:android", "--ks-key-alias", "fixture", "--out", output, aligned, ) return output if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) print(build(parser.parse_args().output))