nandi/frqpublic Fork 0
6c745dfdacf9ce4b652034872fbebd67d16a66e5
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

android.nix · 381 lines · 15.7 KBNix Blame HistoryRaw
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago1# The APK, as derivations rather than as a buck2 graph.
2#
Ask for the camera and the microphone from the app 6c745df nandi 18d ago3# android/BUCK builds the same things, and does it better for a person at
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago4# a terminal: it is incremental, and it lets a sibling jolt-native checkout win
5# over the pinned release so that editing the Rust rebuilds the APK. Nothing
6# here replaces that. What this adds is the other build — from nothing, on a
7# machine with no Android SDK, no NDK, no hand-built Chez cross target and no
8# ~/.cache at all:
9#
10# nix build .#apk
11#
12# On a machine with a remote builder configured, prefer
13#
14# nix build .#apk --store ssh-ng://eu.nixbuild.net --eval-store auto
15#
16# rather than letting `builders` do it. With `builders`, nix copies the output
17# of every remotely-built derivation back, and androidenv's NDK is both
18# `preferLocalBuild` and absent from cache.nixos.org — so the 3.1 GB unpacked
19# toolchain is built here and uploaded. With the remote as the *store* the
20# whole graph stays there, only .drv files go up, and the builder fetches
21# Google's zip over its own link.
22#
23# Every path .buckconfig.local answers for is answered here by the store
24# instead. The steps are in the same order and do the same work; where a
25# genrule read `read_root_config`, a derivation takes an argument.
26{ pkgs, lib, self, chez-src, jolt-native, glimmer, joltAndroid, androidSdk, ndk }:
27
28let
29 # What the APK targets, in the three spellings the tools want it in.
30 apiLevel = "28";
31 targetSdk = "36";
32 abi = "arm64-v8a";
33 package = "uk.nandi.frq";
34 version = "0.1.0";
35
36 sdk = "${androidSdk}/libexec/android-sdk";
37 buildTools = "${sdk}/build-tools/36.0.0";
38 androidJar = "${sdk}/platforms/android-${targetSdk}/android.jar";
39
40 # The NDK's clang finds its sysroot, resource directory and the rest of LLVM
41 # relative to itself, so it is named by path rather than copied anywhere.
42 # androidenv installs the tree under libexec/android-sdk and leaves
43 # `ndk-bundle` pointing at the versioned directory beside it.
44 ndkRoot = "${ndk}/libexec/android-sdk/ndk-bundle";
45 ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin";
46 cc = "${ndkBin}/aarch64-linux-android${apiLevel}-clang";
47
Ask for the camera and the microphone from the app 6c745df nandi 18d ago48 # What comes out of jolt-native's releases, by the digests scripts/*.dotslash
49 # pin. Same bytes buck fetches; DotSlash's `digest` is over the archive,
50 # which is what fetchurl hashes too.
51 #
52 # One archive, two libraries: libvidya (the retained-tree UI) and libjoltmoq
53 # (the AV media plane). They are built together and only make sense together
54 # — libjoltapp links both — so there is one pin for the pair rather than two
55 # that could drift apart.
56 nativeRelease = pkgs.fetchurl {
57 url = "https://gitlab.com/nandithebull/jolt-native/-/releases/v0.1.3/downloads/jolt-native-android-arm64-v0.1.3.tar.gz";
58 sha256 = "4519745bae6db9a791a71b38a26478879e246166802b0a48dbf7d45e4d45a108";
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago59 };
60
61 glue = pkgs.fetchurl {
Ask for the camera and the microphone from the app 6c745df nandi 18d ago62 url = "https://gitlab.com/nandithebull/jolt-native/-/releases/v0.1.3/downloads/jolt-native-android-glue-v0.1.3.tar.gz";
63 sha256 = "83313eda124f2a0cfff6827cf4473600c1f71db2f208d654b97068a85af38da5";
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago64 };
65
Ask for the camera and the microphone from the app 6c745df nandi 18d ago66 # Unpacked once, so the consumers below name files rather than repeat the tar.
67 nativeLibs = pkgs.runCommand "jolt-native-android" { } ''
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago68 mkdir -p "$out"
Ask for the camera and the microphone from the app 6c745df nandi 18d ago69 tar -xzf ${nativeRelease} -C "$out"
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago70 '';
71
72 glueSrc = pkgs.runCommand "jolt-android-glue" { } ''
73 mkdir -p "$out"
74 tar -xzf ${glue} -C "$out" --strip-components=1
75 '';
76
Ask for the camera and the microphone from the app 6c745df nandi 18d ago77 # The C++ runtime, out of the same NDK the glue is compiled with.
78 #
79 # openh264 is C++, and its build script asks to be linked against
80 # `libc++_shared.so` by name — so libjoltmoq carries that as a DT_NEEDED. An
81 # app's linker namespace will not hand out the platform's own copy (there is
82 # no stable one to hand out), so the APK carries it, exactly as it carries
83 # OpenSSL below and for the same reason.
84 libcxx = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so";
85
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago86 # --- Chez's arm64 cross target ------------------------------------------
87 # The one piece with no nixpkgs equivalent: `pkgs.chez` builds a Scheme for
88 # this machine, and what the boot image needs is Chez's `tarm64le` workarea —
89 # the target boot files, the cross compiler's xpatch, and the arm64
90 # libkernel.a that libjoltapp links.
91 #
92 # Three builds in one derivation, because each needs the one before it:
93 #
94 # ta6le a host Scheme, which is what cross-compiles anything
95 # boot XM=... the target's boot files and xpatch, made by that host
96 # tarm64le the target kernel, compiled by the NDK
97 #
98 # The flags are the ones the hand-built tree under ~/.cache/vidya-chez-android
99 # was configured with, read back out of its Mf-config. zlib is Android's own
100 # (`-lz`, which Bionic has); lz4 is the in-tree submodule, built for arm64
101 # here because a cross configure links it rather than building it.
102 chezAndroid = pkgs.stdenv.mkDerivation {
103 pname = "chez-scheme-android";
104 version = "10.4.1";
105 src = chez-src;
106
107 strictDeps = true;
108 nativeBuildInputs = with pkgs; [ gnumake which ];
109
110 dontConfigure = true;
111
112 buildPhase = ''
113 runHook preBuild
114
115 # Both workareas turn off the expression editor's two dependencies, which
116 # is what ~/.cache/vidya-chez-android was configured with (its Mf-config
117 # has empty cursesLib/ncursesLib, and disablex11=yes on the target). The
118 # host Scheme here is only ever a cross compiler, and Bionic has no
119 # curses.h at all so on the target it is not a preference but a
120 # requirement.
121 ./configure -m=ta6le --disable-x11 --disable-curses CC_FOR_BUILD="$CC"
122 make -j"$NIX_BUILD_CORES"
123 make boot XM=tarm64le -j"$NIX_BUILD_CORES"
124
125 # lz4 for the phone, not for this machine: the host build above left an
126 # x86_64 liblz4.a in the same place, and the cross link needs it gone.
127 make -C lz4/lib clean
128 make -C lz4/lib liblz4.a -j"$NIX_BUILD_CORES" \
129 CC=${cc} AR=${ndkBin}/llvm-ar
130
131 # --disable-auto-flags stops configure appending -lrt and -lpthread, which
132 # is what its unix branch does for a glibc host and what Bionic has no
133 # separate libraries for both live in libc there. Everything it would
134 # otherwise add is passed explicitly below, matching the Mf-config of
135 # the tree this was reconstructed from.
136 ./configure -m=tarm64le --cross --disable-x11 --disable-curses \
137 --disable-auto-flags \
138 LIBS="-ldl -lm" \
139 CC=${cc} \
140 AR=${ndkBin}/llvm-ar \
141 CC_FOR_BUILD="$CC" \
142 ZLIB=-lz \
143 LZ4="$PWD/lz4/lib/liblz4.a" \
144 CPPFLAGS="-I$PWD/lz4/lib" \
145 CFLAGS="-O2 -D_REENTRANT -pthread -fPIC"
146 make -j"$NIX_BUILD_CORES"
147
148 runHook postBuild
149 '';
150
151 # The whole workarea, at the paths CHEZ_ANDROID means: the host Scheme
152 # loads xpatch out of xc-tarm64le/s, and the link below reads two archives
153 # from elsewhere in the tree. Pruning it would only be guessing at which
154 # of those the cross compiler still opens.
155 installPhase = ''
156 runHook preInstall
157 mkdir -p "$out"
158 cp -r . "$out/"
159 runHook postInstall
160 '';
161
162 # A Scheme built for this machine and a kernel built for another one; the
163 # usual fixups have an opinion about both, and neither wants it.
164 dontStrip = true;
165 dontPatchELF = true;
166 };
167
168 hostScheme = "${chezAndroid}/ta6le/bin/ta6le/scheme";
169 targetBoot = "${chezAndroid}/boot/tarm64le";
170 xpatch = "${chezAndroid}/xc-tarm64le/s/xpatch";
171
172 # --- the Jolt half --------------------------------------------------------
173 # frq's Scheme, cross-compiled to an arm64 boot image. android/build-jolt-boot.bb
174 # does this by hand-writing a deps.edn of :paths and then driving Chez; so
175 # does this, for the same reason — there is no dependency resolution inside a
176 # cross compile, so every source root deps.edn would have resolved is named.
177 #
178 # The jolt that runs it is the fork, not upstream and not the one the desktop
179 # package builds: upstream reads the socket address out of `struct addrinfo`
180 # at glibc's offset, which on Bionic is `ai_canonname`, and an APK built with
181 # it cannot open a TLS connection at all.
182 joltBoot = pkgs.stdenv.mkDerivation {
183 pname = "frq-jolt-boot";
184 inherit version;
185
186 dontUnpack = true;
187 strictDeps = true;
188 nativeBuildInputs = [ joltAndroid ];
189
190 buildPhase = ''
191 runHook preBuild
192
193 export HOME="$TMPDIR"
194 mkdir -p project cross
195
196 cat > project/deps.edn <<EOF
197 {:paths ["${self}/src" "${glimmer}/src" "${jolt-native}/jolt/glimmer-vidya/src"]}
198 EOF
199
200 # The flat build, which is the one shape make-boot-file can take.
201 ( cd project && JOLT_NO_FLAT_SPLIT=1 jolt build -m frq.app -o app )
202
203 cat > cross/compile.ss <<EOF
204 (import (chezscheme))
205 (load "${xpatch}")
206 (optimize-level 2)
207 (generate-inspector-information #f)
208 (compile-file "$PWD/project/app.build/flat.ss" "$PWD/cross/flat.so")
209 (make-boot-file "$PWD/jolt.boot" '()
210 "${targetBoot}/petite.boot"
211 "${targetBoot}/scheme.boot"
212 "$PWD/cross/flat.so")
213 EOF
214
215 SCHEMEHEAPDIRS="${chezAndroid}/ta6le/boot/ta6le" \
216 ${hostScheme} --script cross/compile.ss
217
218 runHook postBuild
219 '';
220
221 # scheme.h travels with the image because jolt_main.c includes it.
222 installPhase = ''
223 runHook preInstall
224 mkdir -p "$out"
225 cp jolt.boot "$out/jolt.boot"
226 cp ${targetBoot}/scheme.h "$out/scheme.h"
227 runHook postInstall
228 '';
229 };
230
231 # The image travels as a blob in an object file's data section. The
232 # _binary_jolt_boot_{start,end} symbols jolt_main.c reads are named after the
233 # input *path*, so this copies the file somewhere it is called exactly
234 # `jolt.boot` before converting it.
235 joltBootObj = pkgs.runCommand "jolt-boot-obj" { } ''
236 cp ${joltBoot}/jolt.boot jolt.boot
237 ${ndkBin}/llvm-objcopy \
238 --input-target=binary --output-target=elf64-littleaarch64 \
239 --binary-architecture=aarch64 jolt.boot "$out"
240 '';
241
242 # The glue: jolt-native's jolt_main.c over the boot image, linked against
243 # libvidya by name. --no-undefined is what makes a symbol the Scheme side
244 # registers but the ABI no longer exports a build failure here rather than a
245 # crash on the phone.
246 libjoltapp = pkgs.runCommand "libjoltapp.so" { } ''
247 mkdir -p lib
Ask for the camera and the microphone from the app 6c745df nandi 18d ago248 cp ${nativeLibs}/libvidya.so lib/libvidya.so
249 cp ${nativeLibs}/libjoltmoq.so lib/libjoltmoq.so
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago250
251 ${cc} -shared -fPIC -O2 -o "$out" \
252 ${glueSrc}/android/jolt_main.c \
253 ${joltBootObj} \
254 -I${joltBoot} \
255 -I${glueSrc}/include \
256 -Llib \
257 ${chezAndroid}/tarm64le/boot/tarm64le/libkernel.a \
258 ${chezAndroid}/lz4/lib/liblz4.a \
Ask for the camera and the microphone from the app 6c745df nandi 18d ago259 -lvidya -ljoltmoq -landroid -llog -lz -ldl -lm -Wl,--no-undefined
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago260 '';
261
262 # --- the Java half --------------------------------------------------------
263 # One class: the photo chooser's result has to land somewhere, and native
264 # code is not somewhere.
265 classesDex = pkgs.runCommand "classes.dex"
266 {
267 nativeBuildInputs = [ pkgs.jdk17 ];
268 } ''
269 mkdir -p classes out
270 # -encoding, because a build sandbox has no locale and javac then reads
271 # the source as US-ASCII on which the comments' em dashes are errors.
272 javac --release 17 -encoding UTF-8 --class-path ${androidJar} -d classes \
273 $(find ${self}/android/java -name '*.java')
274 ${buildTools}/d8 --min-api ${apiLevel} --output out $(find classes -name '*.class')
275 cp out/classes.dex "$out"
276 '';
277
278 # --- the package ----------------------------------------------------------
279 # OpenSSL travels with the app because the platform's own is not ours to
280 # load: an app's linker namespace refuses /system/lib64/libssl.so, and
281 # without one there is no TLS on the phone at all.
282 # Built by the NDK rather than by pkgsCross.aarch64-android: that cross
283 # stdenv cannot build its own compiler-rt on this nixpkgs — os_version_check.c
284 # includes <pthread.h> and the sysroot it is handed has no such header — and
285 # an APK has no use for a second toolchain anyway. OpenSSL's own android-arm64
286 # target wants the NDK's llvm on PATH and takes the API level from the flag.
287 # The version is the one ~/.cache/frq-openssl-android was built from.
288 opensslAndroid = pkgs.stdenv.mkDerivation {
289 pname = "openssl-android";
290 version = "3.5.4";
291
292 src = pkgs.fetchurl {
293 url = "https://github.com/openssl/openssl/releases/download/openssl-3.5.4/openssl-3.5.4.tar.gz";
294 sha256 = "16ay6ppxsky3qhg6573370iz93kihfwx9n5ipmlnjcam97w12wwn";
295 };
296
297 strictDeps = true;
298 nativeBuildInputs = with pkgs; [ perl ];
299
300 configurePhase = ''
301 runHook preConfigure
302 export ANDROID_NDK_ROOT="${ndkRoot}"
303 export PATH="${ndkBin}:$PATH"
304 # Through perl rather than as a program: its shebang is /usr/bin/env,
305 # which a build sandbox does not have.
306 perl ./Configure android-arm64 -D__ANDROID_API__=${apiLevel} \
307 shared no-tests no-docs \
308 --prefix="$out" --openssldir="$out/etc/ssl"
309 runHook postConfigure
310 '';
311
312 # install_sw, not install: the rest of an OpenSSL install is for a machine
313 # that runs it, and this one only ships two .so files into an APK.
314 installTargets = [ "install_sw" ];
315
316 dontStrip = true;
317 dontPatchELF = true;
318 };
319
320 # The libraries are stored rather than deflated: the loader maps them
321 # straight out of the APK. The dex is read rather than mapped, so it may as
322 # well compress.
323 apkUnsigned = pkgs.runCommand "frq-unsigned.apk"
324 {
325 nativeBuildInputs = [ pkgs.zip ];
326 } ''
327 mkdir -p stage/lib/${abi}
Ask for the camera and the microphone from the app 6c745df nandi 18d ago328 cp ${nativeLibs}/libvidya.so stage/lib/${abi}/libvidya.so
329 cp ${nativeLibs}/libjoltmoq.so stage/lib/${abi}/libjoltmoq.so
330 cp ${libcxx} stage/lib/${abi}/libc++_shared.so
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago331 cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so
332 cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so
333 cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so
334 cp ${classesDex} stage/classes.dex
335 chmod -R u+w stage
336
337 ${buildTools}/aapt2 link -o "$out" -I ${androidJar} \
338 --manifest ${self}/android/AndroidManifest.xml \
339 --min-sdk-version ${apiLevel} --target-sdk-version ${targetSdk} \
340 --version-code 1 --version-name ${version}
341
342 ( cd stage && \
Ask for the camera and the microphone from the app 6c745df nandi 18d ago343 zip -q -0 "$out" lib/${abi}/libvidya.so lib/${abi}/libjoltmoq.so \
344 lib/${abi}/libc++_shared.so lib/${abi}/libjoltapp.so \
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago345 lib/${abi}/libssl.so lib/${abi}/libcrypto.so && \
346 zip -q "$out" classes.dex )
347 '';
348
349 # Aligned and signed with a debug key. The key is generated here rather than
350 # read from ~/.android, which is the one place this build is deliberately
351 # not the buck one: a keystore outside the store would make the output
352 # depend on the machine, and a release key has no business in the store at
353 # all. So this output is installable and not reproducible — keytool stamps
354 # the certificate with the time — and anything meant for a store should be
355 # signed from .#apk-unsigned instead.
356 apk = pkgs.runCommand "frq-${version}.apk"
357 {
358 nativeBuildInputs = [ pkgs.jdk17 ];
359 meta = {
360 description = "frq for Android, debug-signed";
361 platforms = [ "x86_64-linux" ];
362 };
363 } ''
364 export HOME="$TMPDIR"
365 keytool -genkeypair -keystore debug.keystore \
366 -storepass android -keypass android -alias androiddebugkey \
367 -keyalg RSA -keysize 2048 -validity 10000 \
368 -dname 'CN=Android Debug,O=Android,C=US'
369
370 ${buildTools}/zipalign -f -p 4 ${apkUnsigned} aligned.apk
371 ${buildTools}/apksigner sign --ks debug.keystore \
372 --ks-key-alias androiddebugkey \
373 --ks-pass pass:android --key-pass pass:android \
374 --out "$out" aligned.apk
375 ${buildTools}/apksigner verify "$out"
376 '';
377in
378{
379 inherit chezAndroid joltBoot libjoltapp classesDex apk;
380 apk-unsigned = apkUnsigned;
381}