# The APK, as a graph rather than a script. # # Every step the old build-apk.sh ran in sequence is a target here, so a change # to one screen rebuilds the boot image and repackages, and touches neither the # Rust nor the Java. The two native halves come from jolt-native — libvidya out # of its buck2 graph, the glue compiled by the NDK its scripts/ pin — and the # rest is this repo's. # # The machine-specific paths are read from .buckconfig.local, which the `buck` # recipe in the justfile writes. Nothing here is found by looking around the # machine; if a path is missing the recipe says which. load(":defs.bzl", "libvidya") _JOLT_NATIVE = read_root_config("frq", "jolt_native", "") _ANDROID_HOME = read_root_config("frq", "android_home", "") _CHEZ = read_root_config("frq", "chez_android", "") _OPENSSL = read_root_config("frq", "openssl_android", "") _API = "28" _TOOLS = _ANDROID_HOME + "/build-tools/36.0.0" _ANDROID_JAR = _ANDROID_HOME + "/platforms/android-36/android.jar" # `$(...)` in a genrule cmd is a buck macro, not the shell's substitution, so # everything this file runs at command time uses backticks. # This repo's own, not the other checkout's: an APK build needs a clang and an # llvm-objcopy whether or not jolt-native is on the machine. Both resolve the # NDK through scripts/android-ndk.dotslash, downloading it once. _NDK_BIN = "ndk=`$(location //scripts:android-ndk-bin)`" _CC = "$(location //scripts:android-cc)" # An absolute path for $OUT, so a command may cd without losing it. buck has # already made the parent directory. Spelled out step by step because backticks # do not nest. # Empty when a checkout answers for it, so the script's own lookup wins. # The archive's top directory is what strip_prefix took off, so `src` is # directly inside it. Absolute, because the script is handed this and does its # own directory changing. _GLIMMER_VIDYA = "" if read_root_config("frq", "libvidya", "pinned") == "checkout" else \ "GLIMMER_VIDYA=`realpath $(location toolchains//dist:glimmer-vidya)/src` " # jolt_main.c and the ABI's headers. A sibling checkout wins here too, so that # editing the glue and the library together works; otherwise the release. _GLUE = _JOLT_NATIVE if read_root_config("frq", "libvidya", "pinned") == "checkout" else None _GLUE_C = (_GLUE + "/android/jolt_main.c") if _GLUE else "`realpath $(location toolchains//dist:android-glue)/android/jolt_main.c`" _GLUE_INCLUDE = (_GLUE + "/crates/jolt-vidya/include") if _GLUE else "`realpath $(location toolchains//dist:android-glue)/include`" _ABS_OUT = 'd=`dirname "$OUT"` && b=`basename "$OUT"` && d=`cd "$d" && pwd` && out="$d/$b"' # Somewhere to assemble in. Removed by the command that made it, on success; # buck's own scratch space is not this. _TMP = 'tmp=`mktemp -d`' export_file( name = "build-jolt-boot.sh", mode = "reference", ) # --- the UI half ------------------------------------------------------------ # Two ways in, and both make the library's bytes an input rather than a # command buck would cache forever: # # pinned the .so out of jolt-native's release, fetched by digest. What a # build gets by default, and what makes an APK buildable with no # jolt-native checkout and no NDK anywhere on the machine. # checkout the .so a sibling jolt-native just built, staged into this cell # by the `buck` recipe. Anyone working on both repos at once # builds what they are editing. # # The recipe decides which, by whether that checkout exists, and says so here. libvidya(name = "libvidya") # --- the Jolt half ---------------------------------------------------------- # The boot image: frq's Scheme, cross-compiled to arm64 by Chez. The sources # are an input so that editing one rebuilds this; the compile itself reads them # through the deps.edn the script writes. # The sources here are frq's own. The other two roots the image is compiled # from — glimmer, out of the jolt cache, and glimmer-vidya, out of jolt-native # — are outside this cell, and so are Chez's cross boot files and the pinned # jolt itself. None of them can be an input, so the `buck` recipe hashes them # and writes the digest below; naming it in the command is what puts them in # this action's identity. # jolt reaches this action as a manifest and a fetcher rather than as a # program: both are inputs, so nothing here depends on what is installed where # the compile runs, and a remote worker resolves the same pin — by the same # digest — from the same place this machine would. genrule( name = "jolt-boot", out = "boot", srcs = ["//:jolt-sources"], cmd = "# sources outside this cell: " + read_root_config("frq", "boot_stamp", "unknown") + "\n" + "DOTSLASH=$(location toolchains//dist:dotslash)/dotslash " + "JOLT_MANIFEST=$(location //scripts:jolt) " + # The one source root the script cannot find for itself when there # is no jolt-native checkout. An input, so a new release recompiles # the image. _GLIMMER_VIDYA + "$(location :build-jolt-boot.sh) \"$OUT\" >&2", ) # The image travels as a blob in an object file's data section; the # _binary_jolt_boot_{start,end} symbols jolt_main.c reads are named after the # input *path*, which is why this copies the file somewhere it can be called # exactly `jolt.boot` before converting it. genrule( name = "jolt-boot-obj", out = "jolt_boot.o", cmd = _ABS_OUT + " && " + _TMP + " && " + _NDK_BIN + " && " + "cp $(location :jolt-boot)/jolt.boot \"$tmp/jolt.boot\" && " + "( cd \"$tmp\" && \"$ndk\"/llvm-objcopy " + "--input-target=binary --output-target=elf64-littleaarch64 " + "--binary-architecture=aarch64 jolt.boot jolt_boot.o ) && " + "cp \"$tmp/jolt_boot.o\" \"$out\" && rm -rf \"$tmp\"", ) # The glue: jolt-native's android/jolt_main.c over the boot image, linked # against libvidya by name. --no-undefined is what makes a symbol the Scheme # side registers but the ABI no longer exports a build failure here rather than # a crash on the phone. genrule( name = "libjoltapp", out = "libjoltapp.so", cmd = _ABS_OUT + " && lib=`mktemp -d` && " + "cp $(location :libvidya) \"$lib/libvidya.so\" && " + _CC + " -shared -fPIC -O2 -o \"$out\" " + _GLUE_C + " " + "$(location :jolt-boot-obj) " + "-I$(location :jolt-boot) " + "-I" + _GLUE_INCLUDE + " " + "-L\"$lib\" " + _CHEZ + "/tarm64le/boot/tarm64le/libkernel.a " + _CHEZ + "/lz4/lib/liblz4.a " + "-lvidya -landroid -llog -lz -ldl -lm -Wl,--no-undefined && " + "rm -rf \"$lib\"", ) # --- the Java half ---------------------------------------------------------- # One class: the photo chooser's result has to land somewhere, and native code # is not somewhere. android.jar on the class path is where every android.* type # comes from; the JDK's own java.* is what is left. genrule( name = "classes-dex", out = "classes.dex", srcs = glob(["java/**/*.java"]), cmd = _ABS_OUT + " && " + _TMP + " && " + "javac --release 17 --class-path " + _ANDROID_JAR + " -d \"$tmp/classes\" $SRCS && " + "classes=`find \"$tmp/classes\" -name '*.class'` && " + _TOOLS + "/d8 --min-api " + _API + " --output \"$tmp\" $classes && " + "cp \"$tmp/classes.dex\" \"$out\" && rm -rf \"$tmp\"", ) # --- the package ------------------------------------------------------------ # The libraries are stored rather than deflated: the loader maps them straight # out of the APK. The dex is read rather than mapped, so it may as well # compress. OpenSSL travels with the app because the platform's own is not ours # to load — an app's linker namespace refuses /system/lib64/libssl.so, and # without one there is no TLS on the phone at all. genrule( name = "apk-unsigned", out = "frq-unsigned.apk", srcs = ["AndroidManifest.xml"], cmd = _ABS_OUT + " && stage=`mktemp -d` && " + "mkdir -p \"$stage/lib/arm64-v8a\" && " + "cp $(location :libvidya) \"$stage/lib/arm64-v8a/libvidya.so\" && " + "cp $(location :libjoltapp) \"$stage/lib/arm64-v8a/libjoltapp.so\" && " + "cp " + _OPENSSL + "/libssl.so " + _OPENSSL + "/libcrypto.so \"$stage/lib/arm64-v8a/\" && " + "cp $(location :classes-dex) \"$stage/classes.dex\" && " + _TOOLS + "/aapt2 link -o \"$out\" -I " + _ANDROID_JAR + " " + "--manifest $SRCS --min-sdk-version " + _API + " --target-sdk-version 36 " + "--version-code 1 --version-name 0.1.0 >&2 && " + "( cd \"$stage\" && zip -q -0 \"$out\" lib/arm64-v8a/libvidya.so " + "lib/arm64-v8a/libjoltapp.so lib/arm64-v8a/libssl.so lib/arm64-v8a/libcrypto.so && " + "zip -q \"$out\" classes.dex ) && rm -rf \"$stage\"", ) # Aligned and signed with the debug key, which is generated on first use the # way the SDK's own tools generate it. genrule( name = "apk", out = "frq.apk", cmd = _ABS_OUT + " && " + _TMP + " && " + "ks=\"$HOME/.android/debug.keystore\" && " + "if [ ! -f \"$ks\" ]; then mkdir -p \"$HOME/.android\" && " + "keytool -genkeypair -v -keystore \"$ks\" -storepass android -keypass android " + "-alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 " + "-dname 'CN=Android Debug,O=Android,C=US' >&2; fi && " + _TOOLS + "/zipalign -f -p 4 $(location :apk-unsigned) \"$tmp/aligned.apk\" && " + _TOOLS + "/apksigner sign --ks \"$ks\" --ks-key-alias androiddebugkey " + "--ks-pass pass:android --key-pass pass:android --out \"$out\" \"$tmp/aligned.apk\" && " + _TOOLS + "/apksigner verify \"$out\" >/dev/null && rm -rf \"$tmp\"", visibility = ["PUBLIC"], ) # Does the pin resolve where the action runs, and does what it resolves to run # there? Nothing depends on this; it is here to be asked by hand. # # buck2 build //android:jolt-fetch-check --remote-only --materializations=all \ # -c build.execution_platforms=root//platforms:remote -c "parser...->root//platforms:remote" # # It is what found the glibc floor: jolt needs 2.38, and the rbe-ubuntu22-04 # image this platform used to name ships 2.35, so the fetch succeeded and the # binary would not start. The answer was the newer image, not a different pin. genrule( name = "jolt-fetch-check", out = "report", cmd = "jolt=`$(location toolchains//dist:dotslash)/dotslash -- fetch $(location //scripts:jolt)`; " + "{ echo \"fetched: $jolt\"; " + "cat /etc/os-release 2>/dev/null | head -1 || true; " + "ldd --version 2>&1 | head -1 || true; " + "echo '--- run ---'; \"$jolt\" --version; echo \"rc=$?\"; } > \"$OUT\" 2>&1 || true", )