nandi/frqpublic Fork 0
a008d3bb4cef6d5905699f39dd51f4665d8a2199
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 · 364 lines · 14.6 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#
3# android/BUCK builds the same five things, and does it better for a person at
Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago4# a terminal: it is incremental, and it fetches jolt-native's release by digest
5# rather than rebuilding the world. Nothing here replaces that. What this adds
6# is the other build — from nothing, on a
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago7# 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
48 # The two halves that come out of jolt-native's releases, by the digests
49 # scripts/*.dotslash pins. Same bytes buck fetches; DotSlash's `digest` is
50 # over the archive, which is what fetchurl hashes too.
51 libvidya = pkgs.fetchurl {
52 url = "https://gitlab.com/-/project/85910092/uploads/a7264f20582e6d42d45b17fe626ba471/jolt-native-android-arm64-v0.1.1.tar.gz";
53 sha256 = "bb85c57ea263b9bfe113927c6f6b0c8c6414b97056bcb0281b8444187616a233";
54 };
55
56 glue = pkgs.fetchurl {
57 url = "https://gitlab.com/-/project/85910092/uploads/16b1a3ea32dac6737fc21aec701d7b0c/jolt-native-android-glue-v0.1.1.tar.gz";
58 sha256 = "7f4c179d72a3660ce8e80c3cf52a788ea33d7ef97d6f624310e61e7f4f98851b";
59 };
60
61 # Unpacked once, so the three consumers below name files rather than repeat
62 # the tar.
63 vidyaLib = pkgs.runCommand "libvidya-android" { } ''
64 mkdir -p "$out"
65 tar -xzf ${libvidya} -C "$out"
66 '';
67
68 glueSrc = pkgs.runCommand "jolt-android-glue" { } ''
69 mkdir -p "$out"
70 tar -xzf ${glue} -C "$out" --strip-components=1
71 '';
72
73 # --- Chez's arm64 cross target ------------------------------------------
74 # The one piece with no nixpkgs equivalent: `pkgs.chez` builds a Scheme for
75 # this machine, and what the boot image needs is Chez's `tarm64le` workarea —
76 # the target boot files, the cross compiler's xpatch, and the arm64
77 # libkernel.a that libjoltapp links.
78 #
79 # Three builds in one derivation, because each needs the one before it:
80 #
81 # ta6le a host Scheme, which is what cross-compiles anything
82 # boot XM=... the target's boot files and xpatch, made by that host
83 # tarm64le the target kernel, compiled by the NDK
84 #
85 # The flags are the ones the hand-built tree under ~/.cache/vidya-chez-android
86 # was configured with, read back out of its Mf-config. zlib is Android's own
87 # (`-lz`, which Bionic has); lz4 is the in-tree submodule, built for arm64
88 # here because a cross configure links it rather than building it.
89 chezAndroid = pkgs.stdenv.mkDerivation {
90 pname = "chez-scheme-android";
91 version = "10.4.1";
92 src = chez-src;
93
94 strictDeps = true;
95 nativeBuildInputs = with pkgs; [ gnumake which ];
96
97 dontConfigure = true;
98
99 buildPhase = ''
100 runHook preBuild
101
102 # Both workareas turn off the expression editor's two dependencies, which
103 # is what ~/.cache/vidya-chez-android was configured with (its Mf-config
104 # has empty cursesLib/ncursesLib, and disablex11=yes on the target). The
105 # host Scheme here is only ever a cross compiler, and Bionic has no
106 # curses.h at all so on the target it is not a preference but a
107 # requirement.
108 ./configure -m=ta6le --disable-x11 --disable-curses CC_FOR_BUILD="$CC"
109 make -j"$NIX_BUILD_CORES"
110 make boot XM=tarm64le -j"$NIX_BUILD_CORES"
111
112 # lz4 for the phone, not for this machine: the host build above left an
113 # x86_64 liblz4.a in the same place, and the cross link needs it gone.
114 make -C lz4/lib clean
115 make -C lz4/lib liblz4.a -j"$NIX_BUILD_CORES" \
116 CC=${cc} AR=${ndkBin}/llvm-ar
117
118 # --disable-auto-flags stops configure appending -lrt and -lpthread, which
119 # is what its unix branch does for a glibc host and what Bionic has no
120 # separate libraries for both live in libc there. Everything it would
121 # otherwise add is passed explicitly below, matching the Mf-config of
122 # the tree this was reconstructed from.
123 ./configure -m=tarm64le --cross --disable-x11 --disable-curses \
124 --disable-auto-flags \
125 LIBS="-ldl -lm" \
126 CC=${cc} \
127 AR=${ndkBin}/llvm-ar \
128 CC_FOR_BUILD="$CC" \
129 ZLIB=-lz \
130 LZ4="$PWD/lz4/lib/liblz4.a" \
131 CPPFLAGS="-I$PWD/lz4/lib" \
132 CFLAGS="-O2 -D_REENTRANT -pthread -fPIC"
133 make -j"$NIX_BUILD_CORES"
134
135 runHook postBuild
136 '';
137
138 # The whole workarea, at the paths CHEZ_ANDROID means: the host Scheme
139 # loads xpatch out of xc-tarm64le/s, and the link below reads two archives
140 # from elsewhere in the tree. Pruning it would only be guessing at which
141 # of those the cross compiler still opens.
142 installPhase = ''
143 runHook preInstall
144 mkdir -p "$out"
145 cp -r . "$out/"
146 runHook postInstall
147 '';
148
149 # A Scheme built for this machine and a kernel built for another one; the
150 # usual fixups have an opinion about both, and neither wants it.
151 dontStrip = true;
152 dontPatchELF = true;
153 };
154
155 hostScheme = "${chezAndroid}/ta6le/bin/ta6le/scheme";
156 targetBoot = "${chezAndroid}/boot/tarm64le";
157 xpatch = "${chezAndroid}/xc-tarm64le/s/xpatch";
158
159 # --- the Jolt half --------------------------------------------------------
160 # frq's Scheme, cross-compiled to an arm64 boot image. android/build-jolt-boot.bb
161 # does this by hand-writing a deps.edn of :paths and then driving Chez; so
162 # does this, for the same reason — there is no dependency resolution inside a
163 # cross compile, so every source root deps.edn would have resolved is named.
164 #
165 # The jolt that runs it is the fork, not upstream and not the one the desktop
166 # package builds: upstream reads the socket address out of `struct addrinfo`
167 # at glibc's offset, which on Bionic is `ai_canonname`, and an APK built with
168 # it cannot open a TLS connection at all.
169 joltBoot = pkgs.stdenv.mkDerivation {
170 pname = "frq-jolt-boot";
171 inherit version;
172
173 dontUnpack = true;
174 strictDeps = true;
175 nativeBuildInputs = [ joltAndroid ];
176
177 buildPhase = ''
178 runHook preBuild
179
180 export HOME="$TMPDIR"
181 mkdir -p project cross
182
183 cat > project/deps.edn <<EOF
184 {:paths ["${self}/src" "${glimmer}/src" "${jolt-native}/jolt/glimmer-vidya/src"]}
185 EOF
186
187 # The flat build, which is the one shape make-boot-file can take.
188 ( cd project && JOLT_NO_FLAT_SPLIT=1 jolt build -m frq.app -o app )
189
190 cat > cross/compile.ss <<EOF
191 (import (chezscheme))
192 (load "${xpatch}")
193 (optimize-level 2)
194 (generate-inspector-information #f)
195 (compile-file "$PWD/project/app.build/flat.ss" "$PWD/cross/flat.so")
196 (make-boot-file "$PWD/jolt.boot" '()
197 "${targetBoot}/petite.boot"
198 "${targetBoot}/scheme.boot"
199 "$PWD/cross/flat.so")
200 EOF
201
202 SCHEMEHEAPDIRS="${chezAndroid}/ta6le/boot/ta6le" \
203 ${hostScheme} --script cross/compile.ss
204
205 runHook postBuild
206 '';
207
208 # scheme.h travels with the image because jolt_main.c includes it.
209 installPhase = ''
210 runHook preInstall
211 mkdir -p "$out"
212 cp jolt.boot "$out/jolt.boot"
213 cp ${targetBoot}/scheme.h "$out/scheme.h"
214 runHook postInstall
215 '';
216 };
217
218 # The image travels as a blob in an object file's data section. The
219 # _binary_jolt_boot_{start,end} symbols jolt_main.c reads are named after the
220 # input *path*, so this copies the file somewhere it is called exactly
221 # `jolt.boot` before converting it.
222 joltBootObj = pkgs.runCommand "jolt-boot-obj" { } ''
223 cp ${joltBoot}/jolt.boot jolt.boot
224 ${ndkBin}/llvm-objcopy \
225 --input-target=binary --output-target=elf64-littleaarch64 \
226 --binary-architecture=aarch64 jolt.boot "$out"
227 '';
228
229 # The glue: jolt-native's jolt_main.c over the boot image, linked against
230 # libvidya by name. --no-undefined is what makes a symbol the Scheme side
231 # registers but the ABI no longer exports a build failure here rather than a
232 # crash on the phone.
233 libjoltapp = pkgs.runCommand "libjoltapp.so" { } ''
234 mkdir -p lib
235 cp ${vidyaLib}/libvidya.so lib/libvidya.so
236
237 ${cc} -shared -fPIC -O2 -o "$out" \
238 ${glueSrc}/android/jolt_main.c \
239 ${joltBootObj} \
240 -I${joltBoot} \
241 -I${glueSrc}/include \
242 -Llib \
243 ${chezAndroid}/tarm64le/boot/tarm64le/libkernel.a \
244 ${chezAndroid}/lz4/lib/liblz4.a \
245 -lvidya -landroid -llog -lz -ldl -lm -Wl,--no-undefined
246 '';
247
248 # --- the Java half --------------------------------------------------------
249 # One class: the photo chooser's result has to land somewhere, and native
250 # code is not somewhere.
251 classesDex = pkgs.runCommand "classes.dex"
252 {
253 nativeBuildInputs = [ pkgs.jdk17 ];
254 } ''
255 mkdir -p classes out
256 # -encoding, because a build sandbox has no locale and javac then reads
257 # the source as US-ASCII on which the comments' em dashes are errors.
258 javac --release 17 -encoding UTF-8 --class-path ${androidJar} -d classes \
259 $(find ${self}/android/java -name '*.java')
260 ${buildTools}/d8 --min-api ${apiLevel} --output out $(find classes -name '*.class')
261 cp out/classes.dex "$out"
262 '';
263
264 # --- the package ----------------------------------------------------------
265 # OpenSSL travels with the app because the platform's own is not ours to
266 # load: an app's linker namespace refuses /system/lib64/libssl.so, and
267 # without one there is no TLS on the phone at all.
268 # Built by the NDK rather than by pkgsCross.aarch64-android: that cross
269 # stdenv cannot build its own compiler-rt on this nixpkgs — os_version_check.c
270 # includes <pthread.h> and the sysroot it is handed has no such header — and
271 # an APK has no use for a second toolchain anyway. OpenSSL's own android-arm64
272 # target wants the NDK's llvm on PATH and takes the API level from the flag.
273 # The version is the one ~/.cache/frq-openssl-android was built from.
274 opensslAndroid = pkgs.stdenv.mkDerivation {
275 pname = "openssl-android";
276 version = "3.5.4";
277
278 src = pkgs.fetchurl {
279 url = "https://github.com/openssl/openssl/releases/download/openssl-3.5.4/openssl-3.5.4.tar.gz";
280 sha256 = "16ay6ppxsky3qhg6573370iz93kihfwx9n5ipmlnjcam97w12wwn";
281 };
282
283 strictDeps = true;
284 nativeBuildInputs = with pkgs; [ perl ];
285
286 configurePhase = ''
287 runHook preConfigure
288 export ANDROID_NDK_ROOT="${ndkRoot}"
289 export PATH="${ndkBin}:$PATH"
290 # Through perl rather than as a program: its shebang is /usr/bin/env,
291 # which a build sandbox does not have.
292 perl ./Configure android-arm64 -D__ANDROID_API__=${apiLevel} \
293 shared no-tests no-docs \
294 --prefix="$out" --openssldir="$out/etc/ssl"
295 runHook postConfigure
296 '';
297
298 # install_sw, not install: the rest of an OpenSSL install is for a machine
299 # that runs it, and this one only ships two .so files into an APK.
300 installTargets = [ "install_sw" ];
301
302 dontStrip = true;
303 dontPatchELF = true;
304 };
305
306 # The libraries are stored rather than deflated: the loader maps them
307 # straight out of the APK. The dex is read rather than mapped, so it may as
308 # well compress.
309 apkUnsigned = pkgs.runCommand "frq-unsigned.apk"
310 {
311 nativeBuildInputs = [ pkgs.zip ];
312 } ''
313 mkdir -p stage/lib/${abi}
314 cp ${vidyaLib}/libvidya.so stage/lib/${abi}/libvidya.so
315 cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so
316 cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so
317 cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so
318 cp ${classesDex} stage/classes.dex
319 chmod -R u+w stage
320
321 ${buildTools}/aapt2 link -o "$out" -I ${androidJar} \
322 --manifest ${self}/android/AndroidManifest.xml \
323 --min-sdk-version ${apiLevel} --target-sdk-version ${targetSdk} \
324 --version-code 1 --version-name ${version}
325
326 ( cd stage && \
327 zip -q -0 "$out" lib/${abi}/libvidya.so lib/${abi}/libjoltapp.so \
328 lib/${abi}/libssl.so lib/${abi}/libcrypto.so && \
329 zip -q "$out" classes.dex )
330 '';
331
332 # Aligned and signed with a debug key. The key is generated here rather than
333 # read from ~/.android, which is the one place this build is deliberately
334 # not the buck one: a keystore outside the store would make the output
335 # depend on the machine, and a release key has no business in the store at
336 # all. So this output is installable and not reproducible — keytool stamps
337 # the certificate with the time — and anything meant for a store should be
338 # signed from .#apk-unsigned instead.
339 apk = pkgs.runCommand "frq-${version}.apk"
340 {
341 nativeBuildInputs = [ pkgs.jdk17 ];
342 meta = {
343 description = "frq for Android, debug-signed";
344 platforms = [ "x86_64-linux" ];
345 };
346 } ''
347 export HOME="$TMPDIR"
348 keytool -genkeypair -keystore debug.keystore \
349 -storepass android -keypass android -alias androiddebugkey \
350 -keyalg RSA -keysize 2048 -validity 10000 \
351 -dname 'CN=Android Debug,O=Android,C=US'
352
353 ${buildTools}/zipalign -f -p 4 ${apkUnsigned} aligned.apk
354 ${buildTools}/apksigner sign --ks debug.keystore \
355 --ks-key-alias androiddebugkey \
356 --ks-pass pass:android --key-pass pass:android \
357 --out "$out" aligned.apk
358 ${buildTools}/apksigner verify "$out"
359 '';
360in
361{
362 inherit chezAndroid joltBoot libjoltapp classesDex apk;
363 apk-unsigned = apkUnsigned;
364}