| 1 | #!/usr/bin/env python3 |
| 2 | """Build two test-only APKs with an explicitly installed SDK/JDK, without Gradle.""" |
| 3 | import argparse |
| 4 | import hashlib |
| 5 | import json |
| 6 | from pathlib import Path |
| 7 | import subprocess |
| 8 | |
| 9 | |
| 10 | def main(): |
| 11 | parser = argparse.ArgumentParser(description=__doc__) |
| 12 | parser.add_argument("--sdk", type=Path, required=True) |
| 13 | parser.add_argument("--java-home", type=Path, required=True) |
| 14 | parser.add_argument("--output", type=Path, required=True) |
| 15 | parser.add_argument("--platform", default="android-36") |
| 16 | parser.add_argument("--build-tools", default="36.0.0") |
| 17 | args = parser.parse_args() |
| 18 | source = Path(__file__).resolve().parent |
| 19 | output = args.output.resolve() |
| 20 | output.mkdir(parents=True, exist_ok=False) |
| 21 | java = args.java_home.resolve() / "bin" |
| 22 | tools = args.sdk.resolve() / "build-tools" / args.build_tools |
| 23 | android = args.sdk.resolve() / "platforms" / args.platform / "android.jar" |
| 24 | steps = [] |
| 25 | |
| 26 | def run(command): |
| 27 | command = [str(item) for item in command] |
| 28 | result = subprocess.run(command, capture_output=True, text=True, timeout=120) |
| 29 | steps.append({"command": command, "exitCode": result.returncode, |
| 30 | "stdout": result.stdout, "stderr": result.stderr}) |
| 31 | (output / "build.json").write_text(json.dumps(steps, indent=2) + "\n") |
| 32 | result.check_returncode() |
| 33 | |
| 34 | keystore = output / "test-only.jks" |
| 35 | run([java / "keytool", "-genkeypair", "-keystore", keystore, "-storepass", "android", |
| 36 | "-keypass", "android", "-alias", "lab", "-keyalg", "RSA", "-validity", "30", |
| 37 | "-dname", "CN=Disposable Snowbound Provider Lab"]) |
| 38 | for name in ("provider", "client"): |
| 39 | classes = output / name / "classes" |
| 40 | classes.mkdir(parents=True) |
| 41 | run([java / "javac", "--release", "8", "-classpath", android, |
| 42 | "-d", classes, *sorted((source / name).glob("*.java"))]) |
| 43 | dex = output / name / "dex" |
| 44 | dex.mkdir() |
| 45 | run([java / "java", "-Xmx512m", "-cp", tools / "lib" / "d8.jar", "com.android.tools.r8.D8", |
| 46 | "--min-api", "26", "--lib", android, "--output", dex, *sorted(classes.rglob("*.class"))]) |
| 47 | unsigned = output / name / "unsigned.apk" |
| 48 | run([tools / "aapt2", "link", "-I", android, "--manifest", source / name / "AndroidManifest.xml", |
| 49 | "-o", unsigned]) |
| 50 | import zipfile |
| 51 | with zipfile.ZipFile(unsigned, "a") as archive: |
| 52 | archive.write(dex / "classes.dex", "classes.dex") |
| 53 | aligned = output / name / "aligned.apk" |
| 54 | run([tools / "zipalign", "-p", "4", unsigned, aligned]) |
| 55 | apk = output / (name + ".apk") |
| 56 | run([java / "java", "-Xmx256m", "-jar", tools / "lib" / "apksigner.jar", "sign", |
| 57 | "--ks", keystore, "--ks-pass", "pass:android", "--out", apk, aligned]) |
| 58 | run([java / "java", "-jar", tools / "lib" / "apksigner.jar", "verify", "--verbose", apk]) |
| 59 | manifest = { |
| 60 | "sourceSha256": {str(p.relative_to(source)): hashlib.sha256(p.read_bytes()).hexdigest() |
| 61 | for p in sorted(source.rglob("*")) if p.is_file() and "__pycache__" not in p.parts}, |
| 62 | "apkSha256": {name: hashlib.sha256((output / name).read_bytes()).hexdigest() |
| 63 | for name in ("provider.apk", "client.apk")}, |
| 64 | "runtimeTested": False, |
| 65 | } |
| 66 | (output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") |
| 67 | print(json.dumps(manifest, indent=2)) |
| 68 | |
| 69 | |
| 70 | if __name__ == "__main__": |
| 71 | main() |