1#!/usr/bin/env python3
2"""Build two test-only APKs with an explicitly installed SDK/JDK, without Gradle."""
3import argparse
4import hashlib
5import json
6from pathlib import Path
7import subprocess
8
9
10def 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
70if __name__ == "__main__":
71 main()