| Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago | 1 | #!/bin/sh |
| 2 | #_( |
| 3 | exec "$(dirname "$0")/../scripts/bb" "$0" "$@" |
| 4 | ) |
| 5 | |
| 6 | ;; Glue the two halves of the frq Android app into an APK. |
| 7 | ;; |
| 8 | ;; libvidya.so the C ABI on Rust/egui, cross-compiled by buck2, and the |
| 9 | ;; NativeActivity's own library (it holds android-activity's |
| 10 | ;; glue, so it owns the event loop) |
| 11 | ;; libjoltapp.so jolt-native's android/jolt_main.c plus frq's Jolt boot |
| 12 | ;; image, dlopened by the above |
| 13 | ;; classes.dex one Java class, and only because a picture chooser answers |
| 14 | ;; through onActivityResult and a NativeActivity has nowhere |
| 15 | ;; to deliver that |
| 16 | ;; libssl.so OpenSSL, because the platform's own is not ours to load: an |
| 17 | ;; libcrypto.so app's linker namespace refuses /system/lib64/libssl.so, and |
| 18 | ;; without one there is no TLS at all on the phone |
| 19 | ;; |
| 20 | ;; Neither half is built here beyond that last link: the UI library comes from |
| 21 | ;; jolt-native's `just ffi-android` and the boot image from build-jolt-boot.bb. |
| 22 | ;; Both native pieces are jolt-native's — only the boot image is frq's. |
| 23 | ;; |
| 24 | ;; build-apk.bb [build|install|run|log] |
| 25 | (require '[babashka.classpath :as cp]) |
| 26 | (cp/add-classpath (str (babashka.fs/path (babashka.fs/parent *file*) ".." "scripts"))) |
| 27 | (require '[frq.paths :as paths] |
| 28 | '[babashka.fs :as fs] |
| 29 | '[babashka.process :as p]) |
| 30 | |
| 31 | (def root (str (fs/canonicalize (fs/path (fs/parent *file*) "..")))) |
| 32 | (def action (or (first *command-line-args*) "build")) |
| 33 | |
| 34 | ;; Where jolt-native is, answered the way the justfile answers it: a sibling |
| 35 | ;; checkout wins, and otherwise it is the clone `just lib` leaves under |
| 36 | ;; .jolt-native. |
| 37 | (def jolt-native (paths/jolt-native root)) |
| 38 | (when-not (fs/directory? jolt-native) |
| 39 | (paths/die (str "no jolt-native at " jolt-native " — run `just lib` to clone it"))) |
| 40 | |
| 41 | (def android-home (paths/env "ANDROID_HOME" (str (fs/path (fs/home) ".local" "share" "android-sdk")))) |
| 42 | (def ndk-home (paths/env "ANDROID_NDK_HOME" (str (fs/path (fs/home) ".local" "share" "android-ndk-r29")))) |
| 43 | (def chez (paths/env "CHEZ_ANDROID" (str (fs/path (fs/home) ".cache" "vidya-chez-android")))) |
| 44 | (def openssl (paths/env "OPENSSL_ANDROID" (str (fs/path (fs/home) ".cache" "frq-openssl-android" "lib")))) |
| 45 | (def build (fs/path root "android" "build")) |
| 46 | (def jolt-build (fs/path build "jolt")) |
| 47 | (def stage (fs/path build "stage")) |
| 48 | (def tools (fs/path android-home "build-tools" "36.0.0")) |
| 49 | (def android-jar (fs/path android-home "platforms" "android-36" "android.jar")) |
| 50 | (def adb (paths/env "ADB" (str (fs/path android-home "platform-tools" "adb")))) |
| 51 | (def ndk-bin (fs/path ndk-home "toolchains" "llvm" "prebuilt" "linux-x86_64" "bin")) |
| 52 | (def package "uk.nandi.frq") |
| 53 | (def activity (str package "/.FrqActivity")) |
| 54 | (def api "28") |
| 55 | (def clang (str (fs/path ndk-bin (str "aarch64-linux-android" api "-clang")))) |
| 56 | (def arm-lib (fs/path stage "lib" "arm64-v8a")) |
| 57 | |
| 58 | (paths/require-paths! "Android tool" |
| 59 | [clang android-jar |
| 60 | (fs/path tools "aapt2") (fs/path tools "zipalign") |
| 61 | (fs/path tools "apksigner") (fs/path tools "d8") |
| 62 | (fs/path openssl "libssl.so") (fs/path openssl "libcrypto.so")]) |
| 63 | |
| 64 | ;; --- the UI half ----------------------------------------------------------- |
| 65 | ;; buck2 fetches its own NDK for this, from the pin in jolt-native's |
| 66 | ;; scripts/android-ndk.dotslash, so the toolchain above is the only one that |
| 67 | ;; has to be installed by hand. |
| 68 | (p/shell {:dir jolt-native :out *err*} "just" "ffi-android") |
| 69 | (def vidya-so (fs/path jolt-native "build" "android" "arm64-v8a" "libvidya.so")) |
| 70 | (paths/require-paths! "library" [vidya-so]) |
| 71 | |
| 72 | ;; --- the Jolt half --------------------------------------------------------- |
| 73 | (p/shell (str (fs/path root "android" "build-jolt-boot.bb")) (str jolt-build)) |
| 74 | ;; The boot image travels as a blob in the object file's data section; the |
| 75 | ;; _binary_jolt_boot_{start,end} symbols jolt_main.c reads come from this. |
| 76 | (p/shell {:dir (str jolt-build)} |
| 77 | (str (fs/path ndk-bin "llvm-objcopy")) |
| 78 | "--input-target=binary" |
| 79 | "--output-target=elf64-littleaarch64" |
| 80 | "--binary-architecture=aarch64" |
| 81 | "jolt.boot" "jolt_boot.o") |
| 82 | |
| 83 | ;; --- the Java half --------------------------------------------------------- |
| 84 | ;; One class: the photo chooser's result has to land somewhere, and native code |
| 85 | ;; is not somewhere. d8 turns it into the classes.dex the runtime loads. |
| 86 | (def java-build (fs/path build "java")) |
| 87 | (fs/delete-tree java-build) |
| 88 | (fs/create-dirs (fs/path java-build "classes")) |
| 89 | ;; android.jar on the class path is where every android.* type comes from; the |
| 90 | ;; JDK's own java.* is what is left, and this class uses nothing of it that |
| 91 | ;; Android does not have. (`-bootclasspath` would be the stricter way to say |
| 92 | ;; that, and javac refuses it for a release this recent.) |
| 93 | (p/shell "javac" "--release" "17" |
| 94 | "--class-path" (str android-jar) |
| 95 | "-d" (str (fs/path java-build "classes")) |
| 96 | (str (fs/path root "android" "java" "uk" "nandi" "frq" "FrqActivity.java"))) |
| 97 | (apply p/shell (str (fs/path tools "d8")) "--min-api" api "--output" (str java-build) |
| 98 | (map str (fs/glob (fs/path java-build "classes") "**.class"))) |
| 99 | |
| 100 | (fs/delete-tree stage) |
| 101 | (fs/create-dirs arm-lib) |
| 102 | (fs/copy (fs/path java-build "classes.dex") (fs/path stage "classes.dex")) |
| 103 | (fs/copy vidya-so (fs/path arm-lib "libvidya.so")) |
| 104 | ;; jolt.mvn-http dlopens these by name at first use; beside the app's own |
| 105 | ;; libraries is where an app's namespace will answer for that name. |
| 106 | (doseq [lib ["libssl.so" "libcrypto.so"]] |
| 107 | (fs/copy (fs/path openssl lib) (fs/path arm-lib lib))) |
| 108 | |
| 109 | (p/shell clang "-shared" "-fPIC" "-O2" |
| 110 | "-o" (str (fs/path arm-lib "libjoltapp.so")) |
| 111 | (str (fs/path jolt-native "android" "jolt_main.c")) |
| 112 | (str (fs/path jolt-build "jolt_boot.o")) |
| 113 | (str "-I" jolt-build) |
| 114 | (str "-I" (fs/path jolt-native "crates" "jolt-vidya" "include")) |
| 115 | (str "-L" arm-lib) |
| 116 | (str (fs/path chez "tarm64le" "boot" "tarm64le" "libkernel.a")) |
| 117 | (str (fs/path chez "lz4" "lib" "liblz4.a")) |
| 118 | "-lvidya" "-landroid" "-llog" "-lz" "-ldl" "-lm" |
| 119 | "-Wl,--no-undefined") |
| 120 | |
| 121 | ;; --- the APK --------------------------------------------------------------- |
| 122 | (def unaligned (fs/path build "frq-unaligned.apk")) |
| 123 | (def aligned (fs/path build "frq-aligned.apk")) |
| 124 | (def apk (fs/path build "frq.apk")) |
| 125 | (run! fs/delete-if-exists [unaligned aligned apk]) |
| 126 | (p/shell (str (fs/path tools "aapt2")) "link" |
| 127 | "-o" (str unaligned) |
| 128 | "-I" (str android-jar) |
| 129 | "--manifest" (str (fs/path root "android" "AndroidManifest.xml")) |
| 130 | "--min-sdk-version" api |
| 131 | "--target-sdk-version" "36" |
| 132 | "--version-code" "1" |
| 133 | "--version-name" "0.1.0") |
| 134 | ;; Stored, not deflated: the loader maps these straight out of the APK. |
| 135 | (p/shell {:dir (str stage)} "zip" "-q" "-0" (str unaligned) |
| 136 | "lib/arm64-v8a/libvidya.so" "lib/arm64-v8a/libjoltapp.so" |
| 137 | "lib/arm64-v8a/libssl.so" "lib/arm64-v8a/libcrypto.so") |
| 138 | ;; The dex is read by the runtime rather than mapped, so it may as well deflate. |
| 139 | (p/shell {:dir (str stage)} "zip" "-q" (str unaligned) "classes.dex") |
| 140 | (p/shell (str (fs/path tools "zipalign")) "-f" "-p" "4" (str unaligned) (str aligned)) |
| 141 | |
| 142 | (def keystore (fs/path (fs/home) ".android" "debug.keystore")) |
| 143 | (when-not (fs/exists? keystore) |
| 144 | (fs/create-dirs (fs/parent keystore)) |
| 145 | (p/shell "keytool" "-genkeypair" "-v" |
| 146 | "-keystore" (str keystore) "-storepass" "android" "-keypass" "android" |
| 147 | "-alias" "androiddebugkey" "-keyalg" "RSA" "-keysize" "2048" |
| 148 | "-validity" "10000" |
| 149 | "-dname" "CN=Android Debug,O=Android,C=US")) |
| 150 | (p/shell (str (fs/path tools "apksigner")) "sign" |
| 151 | "--ks" (str keystore) "--ks-key-alias" "androiddebugkey" |
| 152 | "--ks-pass" "pass:android" "--key-pass" "pass:android" |
| 153 | "--out" (str apk) (str aligned)) |
| 154 | (p/shell {:out :string} (str (fs/path tools "apksigner")) "verify" (str apk)) |
| 155 | |
| 156 | (case action |
| 157 | "build" (println (str apk)) |
| 158 | "install" (p/shell adb "install" "-r" (str apk)) |
| 159 | "run" (do (p/shell adb "install" "-r" (str apk)) |
| 160 | (p/shell adb "shell" "am" "force-stop" package) |
| 161 | (p/shell adb "shell" "am" "start" "-n" activity)) |
| 162 | "log" (p/shell adb "logcat" "-s" "VidyaJolt" "Vidya") |
| 163 | (paths/die "usage: build-apk.bb [build|install|run|log]")) |