| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 1 | # The APK, as a graph rather than a script. |
| 2 | # |
| Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago | 3 | # Every step the old build-apk script ran in sequence is a target here, so a change |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 4 | # to one screen rebuilds the boot image and repackages, and touches neither the |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 5 | # Rust nor the Java. The two native halves and the glue come out of jolt-native's |
| 6 | # release, fetched by digest through scripts/*.dotslash; the rest is this repo's. |
| 7 | # Nothing here is built out of a jolt-native checkout, and none is needed. |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 8 | # |
| 9 | # The machine-specific paths are read from .buckconfig.local, which the `buck` |
| 10 | # recipe in the justfile writes. Nothing here is found by looking around the |
| 11 | # machine; if a path is missing the recipe says which. |
| 12 | _ANDROID_HOME = read_root_config("frq", "android_home", "") |
| 13 | _CHEZ = read_root_config("frq", "chez_android", "") |
| 14 | _OPENSSL = read_root_config("frq", "openssl_android", "") |
| 15 | |
| 16 | _API = "28" |
| 17 | _TOOLS = _ANDROID_HOME + "/build-tools/36.0.0" |
| 18 | _ANDROID_JAR = _ANDROID_HOME + "/platforms/android-36/android.jar" |
| 19 | # `$(...)` in a genrule cmd is a buck macro, not the shell's substitution, so |
| 20 | # everything this file runs at command time uses backticks. |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 21 | # This repo's own: an APK build needs a clang and an llvm-objcopy, and neither |
| 22 | # is jolt-native's to supply. Both resolve the NDK through |
| 23 | # scripts/android-ndk.dotslash, downloading it once. |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 24 | _NDK_BIN = "ndk=`$(location //scripts:android-ndk-bin)`" |
| 25 | _CC = "$(location //scripts:android-cc)" |
| 26 | |
| 27 | # An absolute path for $OUT, so a command may cd without losing it. buck has |
| 28 | # already made the parent directory. Spelled out step by step because backticks |
| 29 | # do not nest. |
| 30 | # The archive's top directory is what strip_prefix took off, so `src` is |
| 31 | # directly inside it. Absolute, because the script is handed this and does its |
| 32 | # own directory changing. |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 33 | _GLIMMER_VIDYA = "GLIMMER_VIDYA=`realpath $(location toolchains//dist:glimmer-vidya)/src` " |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 34 | |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 35 | # jolt_main.c and the ABI's headers, as targets rather than bare paths — see |
| 36 | # the rules below. |
| Make the worktree its own buck root, and the glue an input ae207b8 nandi 18d ago | 37 | _GLUE_C = "`realpath $(location :glue-c)`" |
| 38 | _GLUE_INCLUDE = "`realpath $(location :glue-include)`" |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 39 | |
| 40 | _ABS_OUT = 'd=`dirname "$OUT"` && b=`basename "$OUT"` && d=`cd "$d" && pwd` && out="$d/$b"' |
| 41 | |
| 42 | # Somewhere to assemble in. Removed by the command that made it, on success; |
| 43 | # buck's own scratch space is not this. |
| 44 | _TMP = 'tmp=`mktemp -d`' |
| 45 | |
| Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago | 46 | # A babashka script, run in place: it loads scripts/frq/paths.clj beside it and |
| 47 | # resolves its own interpreter through scripts/bb, the DotSlash pin. Referenced |
| 48 | # rather than copied for exactly that reason — the tree around it is part of it. |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 49 | export_file( |
| Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago | 50 | name = "build-jolt-boot.bb", |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 51 | mode = "reference", |
| 52 | ) |
| 53 | |
| 54 | # --- the UI half ------------------------------------------------------------ |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 55 | # The .so out of jolt-native's release, fetched by digest — which is what makes |
| 56 | # an APK buildable with no jolt-native checkout and no NDK anywhere on the |
| 57 | # machine. A genrule rather than a path, so the library's bytes are an input to |
| 58 | # what reads them: an action that shelled out to build it would have nothing to |
| 59 | # invalidate on and would serve the same stale object forever. |
| 60 | genrule( |
| 61 | name = "libvidya", |
| 62 | out = "libvidya.so", |
| 63 | cmd = "cp $(location toolchains//dist:libvidya-android)/libvidya.so \"$OUT\"", |
| 64 | ) |
| 65 | |
| 66 | # The media plane, out of the same archive. Necessarily the same release: |
| 67 | # libjoltapp links both, and pairing one release's media plane with another's |
| 68 | # UI is a combination nothing has tested. |
| 69 | genrule( |
| 70 | name = "libjoltmoq", |
| 71 | out = "libjoltmoq.so", |
| 72 | cmd = "cp $(location toolchains//dist:libjoltmoq-android)/libjoltmoq.so \"$OUT\"", |
| 73 | ) |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 74 | |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 75 | # jolt_main.c and the ABI's headers, from the glue archive of the same release. |
| 76 | # Targets rather than paths for the reason above — an input is the only thing |
| 77 | # buck can notice a change in. |
| 78 | genrule( |
| 79 | name = "glue-c", |
| 80 | out = "jolt_main.c", |
| 81 | cmd = "cp $(location toolchains//dist:android-glue)/android/jolt_main.c \"$OUT\"", |
| 82 | ) |
| Carry the media plane, and the class that opens the camera 43ada0b nandi 18d ago | 83 | |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 84 | # A directory of headers rather than a filegroup: a filegroup keeps each file |
| 85 | # at its own path inside the output, so `-I` would have to name the staging |
| 86 | # directory again. |
| 87 | genrule( |
| 88 | name = "glue-include", |
| 89 | out = "include", |
| 90 | cmd = "cp -r $(location toolchains//dist:android-glue)/include \"$OUT\"", |
| 91 | ) |
| Make the worktree its own buck root, and the glue an input ae207b8 nandi 18d ago | 92 | |
| Carry the media plane, and the class that opens the camera 43ada0b nandi 18d ago | 93 | # The C++ runtime, out of the same NDK the glue is compiled with. |
| 94 | # |
| 95 | # openh264 is C++, and its build script asks to be linked against |
| 96 | # `libc++_shared.so` by name — so libjoltmoq carries that as a DT_NEEDED. An |
| 97 | # app's linker namespace will not hand out the platform's own copy (there is no |
| 98 | # stable one to hand out), so the APK has to carry it, exactly as it carries |
| 99 | # OpenSSL below and for the same reason. |
| 100 | # |
| 101 | # Only when the media plane is packaged: nothing else here is C++. |
| 102 | genrule( |
| 103 | name = "libcxx", |
| 104 | out = "libc++_shared.so", |
| 105 | cmd = _ABS_OUT + " && " + _NDK_BIN + " && " + |
| 106 | "cp \"$ndk\"/../sysroot/usr/lib/aarch64-linux-android/libc++_shared.so \"$out\"", |
| 107 | ) |
| 108 | |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 109 | # --- the Jolt half ---------------------------------------------------------- |
| 110 | # The boot image: frq's Scheme, cross-compiled to arm64 by Chez. The sources |
| 111 | # are an input so that editing one rebuilds this; the compile itself reads them |
| 112 | # through the deps.edn the script writes. |
| 113 | # The sources here are frq's own. The other two roots the image is compiled |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 114 | # from — glimmer and glimmer-vidya, both out of the jolt cache — are outside |
| 115 | # this cell, and so are Chez's cross boot files and the pinned |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 116 | # jolt itself. None of them can be an input, so the `buck` recipe hashes them |
| 117 | # and writes the digest below; naming it in the command is what puts them in |
| 118 | # this action's identity. |
| 119 | # jolt reaches this action as a manifest and a fetcher rather than as a |
| 120 | # program: both are inputs, so nothing here depends on what is installed where |
| 121 | # the compile runs, and a remote worker resolves the same pin — by the same |
| 122 | # digest — from the same place this machine would. |
| 123 | genrule( |
| 124 | name = "jolt-boot", |
| 125 | out = "boot", |
| 126 | srcs = ["//:jolt-sources"], |
| 127 | cmd = "# sources outside this cell: " + read_root_config("frq", "boot_stamp", "unknown") + "\n" + |
| 128 | "DOTSLASH=$(location toolchains//dist:dotslash)/dotslash " + |
| 129 | "JOLT_MANIFEST=$(location //scripts:jolt) " + |
| Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago | 130 | # The one source root the script cannot find for itself: the jolt |
| 131 | # cache is not an action input, and this is. A new release recompiles |
| 132 | # the image because of it. |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 133 | _GLIMMER_VIDYA + |
| Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago | 134 | "$(location :build-jolt-boot.bb) \"$OUT\" >&2", |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 135 | ) |
| 136 | |
| 137 | # The image travels as a blob in an object file's data section; the |
| 138 | # _binary_jolt_boot_{start,end} symbols jolt_main.c reads are named after the |
| 139 | # input *path*, which is why this copies the file somewhere it can be called |
| 140 | # exactly `jolt.boot` before converting it. |
| 141 | genrule( |
| 142 | name = "jolt-boot-obj", |
| 143 | out = "jolt_boot.o", |
| 144 | cmd = _ABS_OUT + " && " + |
| 145 | _TMP + " && " + _NDK_BIN + " && " + |
| 146 | "cp $(location :jolt-boot)/jolt.boot \"$tmp/jolt.boot\" && " + |
| 147 | "( cd \"$tmp\" && \"$ndk\"/llvm-objcopy " + |
| 148 | "--input-target=binary --output-target=elf64-littleaarch64 " + |
| 149 | "--binary-architecture=aarch64 jolt.boot jolt_boot.o ) && " + |
| 150 | "cp \"$tmp/jolt_boot.o\" \"$out\" && rm -rf \"$tmp\"", |
| 151 | ) |
| 152 | |
| 153 | # The glue: jolt-native's android/jolt_main.c over the boot image, linked |
| 154 | # against libvidya by name. --no-undefined is what makes a symbol the Scheme |
| 155 | # side registers but the ABI no longer exports a build failure here rather than |
| 156 | # a crash on the phone. |
| 157 | genrule( |
| 158 | name = "libjoltapp", |
| 159 | out = "libjoltapp.so", |
| 160 | cmd = _ABS_OUT + " && lib=`mktemp -d` && " + |
| 161 | "cp $(location :libvidya) \"$lib/libvidya.so\" && " + |
| Pin the media plane, now that there is a release to pin 03d5a6b nandi 18d ago | 162 | "cp $(location :libjoltmoq) \"$lib/libjoltmoq.so\" && " + |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 163 | _CC + " -shared -fPIC -O2 -o \"$out\" " + |
| 164 | _GLUE_C + " " + |
| 165 | "$(location :jolt-boot-obj) " + |
| 166 | "-I$(location :jolt-boot) " + |
| 167 | "-I" + _GLUE_INCLUDE + " " + |
| 168 | "-L\"$lib\" " + |
| 169 | _CHEZ + "/tarm64le/boot/tarm64le/libkernel.a " + |
| 170 | _CHEZ + "/lz4/lib/liblz4.a " + |
| Pin the media plane, now that there is a release to pin 03d5a6b nandi 18d ago | 171 | "-lvidya -ljoltmoq -landroid -llog -lz -ldl -lm -Wl,--no-undefined && " + |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 172 | "rm -rf \"$lib\"", |
| 173 | ) |
| 174 | |
| 175 | # --- the Java half ---------------------------------------------------------- |
| Carry the media plane, and the class that opens the camera 43ada0b nandi 18d ago | 176 | # Two classes: the photo chooser's result has to land somewhere and native code |
| 177 | # is not somewhere, and Camera2 has no C API worth the name — CameraCapture |
| 178 | # opens the camera in Java and pushes NV12 planes down to libjoltmoq over JNI. |
| 179 | # android.jar on the class path is where every android.* type comes from; the |
| 180 | # JDK's own java.* is what is left. |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 181 | genrule( |
| 182 | name = "classes-dex", |
| 183 | out = "classes.dex", |
| 184 | srcs = glob(["java/**/*.java"]), |
| 185 | cmd = _ABS_OUT + " && " + _TMP + " && " + |
| 186 | "javac --release 17 --class-path " + _ANDROID_JAR + " -d \"$tmp/classes\" $SRCS && " + |
| 187 | "classes=`find \"$tmp/classes\" -name '*.class'` && " + |
| 188 | _TOOLS + "/d8 --min-api " + _API + " --output \"$tmp\" $classes && " + |
| 189 | "cp \"$tmp/classes.dex\" \"$out\" && rm -rf \"$tmp\"", |
| 190 | ) |
| 191 | |
| 192 | # --- the package ------------------------------------------------------------ |
| 193 | # The libraries are stored rather than deflated: the loader maps them straight |
| 194 | # out of the APK. The dex is read rather than mapped, so it may as well |
| 195 | # compress. OpenSSL travels with the app because the platform's own is not ours |
| 196 | # to load — an app's linker namespace refuses /system/lib64/libssl.so, and |
| 197 | # without one there is no TLS on the phone at all. |
| 198 | genrule( |
| 199 | name = "apk-unsigned", |
| 200 | out = "frq-unsigned.apk", |
| 201 | srcs = ["AndroidManifest.xml"], |
| 202 | cmd = _ABS_OUT + " && stage=`mktemp -d` && " + |
| 203 | "mkdir -p \"$stage/lib/arm64-v8a\" && " + |
| 204 | "cp $(location :libvidya) \"$stage/lib/arm64-v8a/libvidya.so\" && " + |
| 205 | "cp $(location :libjoltapp) \"$stage/lib/arm64-v8a/libjoltapp.so\" && " + |
| Pin the media plane, now that there is a release to pin 03d5a6b nandi 18d ago | 206 | "cp $(location :libjoltmoq) \"$stage/lib/arm64-v8a/libjoltmoq.so\" && " + |
| 207 | "cp $(location :libcxx) \"$stage/lib/arm64-v8a/libc++_shared.so\" && " + |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 208 | "cp " + _OPENSSL + "/libssl.so " + _OPENSSL + "/libcrypto.so \"$stage/lib/arm64-v8a/\" && " + |
| 209 | "cp $(location :classes-dex) \"$stage/classes.dex\" && " + |
| 210 | _TOOLS + "/aapt2 link -o \"$out\" -I " + _ANDROID_JAR + " " + |
| 211 | "--manifest $SRCS --min-sdk-version " + _API + " --target-sdk-version 36 " + |
| 212 | "--version-code 1 --version-name 0.1.0 >&2 && " + |
| 213 | "( cd \"$stage\" && zip -q -0 \"$out\" lib/arm64-v8a/libvidya.so " + |
| Pin the media plane, now that there is a release to pin 03d5a6b nandi 18d ago | 214 | "lib/arm64-v8a/libjoltmoq.so lib/arm64-v8a/libc++_shared.so " + |
| Build the APK with buck2, from pins rather than from a second checkout a71ef8f nandi 18d ago | 215 | "lib/arm64-v8a/libjoltapp.so lib/arm64-v8a/libssl.so lib/arm64-v8a/libcrypto.so && " + |
| 216 | "zip -q \"$out\" classes.dex ) && rm -rf \"$stage\"", |
| 217 | ) |
| 218 | |
| 219 | # Aligned and signed with the debug key, which is generated on first use the |
| 220 | # way the SDK's own tools generate it. |
| 221 | genrule( |
| 222 | name = "apk", |
| 223 | out = "frq.apk", |
| 224 | cmd = _ABS_OUT + " && " + _TMP + " && " + |
| 225 | "ks=\"$HOME/.android/debug.keystore\" && " + |
| 226 | "if [ ! -f \"$ks\" ]; then mkdir -p \"$HOME/.android\" && " + |
| 227 | "keytool -genkeypair -v -keystore \"$ks\" -storepass android -keypass android " + |
| 228 | "-alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 " + |
| 229 | "-dname 'CN=Android Debug,O=Android,C=US' >&2; fi && " + |
| 230 | _TOOLS + "/zipalign -f -p 4 $(location :apk-unsigned) \"$tmp/aligned.apk\" && " + |
| 231 | _TOOLS + "/apksigner sign --ks \"$ks\" --ks-key-alias androiddebugkey " + |
| 232 | "--ks-pass pass:android --key-pass pass:android --out \"$out\" \"$tmp/aligned.apk\" && " + |
| 233 | _TOOLS + "/apksigner verify \"$out\" >/dev/null && rm -rf \"$tmp\"", |
| 234 | visibility = ["PUBLIC"], |
| 235 | ) |
| 236 | |
| 237 | # Does the pin resolve where the action runs, and does what it resolves to run |
| 238 | # there? Nothing depends on this; it is here to be asked by hand. |
| 239 | # |
| 240 | # buck2 build //android:jolt-fetch-check --remote-only --materializations=all \ |
| 241 | # -c build.execution_platforms=root//platforms:remote -c "parser...->root//platforms:remote" |
| 242 | # |
| 243 | # It is what found the glibc floor: jolt needs 2.38, and the rbe-ubuntu22-04 |
| 244 | # image this platform used to name ships 2.35, so the fetch succeeded and the |
| 245 | # binary would not start. The answer was the newer image, not a different pin. |
| 246 | genrule( |
| 247 | name = "jolt-fetch-check", |
| 248 | out = "report", |
| 249 | cmd = "jolt=`$(location toolchains//dist:dotslash)/dotslash -- fetch $(location //scripts:jolt)`; " + |
| 250 | "{ echo \"fetched: $jolt\"; " + |
| 251 | "cat /etc/os-release 2>/dev/null | head -1 || true; " + |
| 252 | "ldd --version 2>&1 | head -1 || true; " + |
| 253 | "echo '--- run ---'; \"$jolt\" --version; echo \"rc=$?\"; } > \"$OUT\" 2>&1 || true", |
| 254 | ) |