Build the APK from the flake, and from nothing
android/BUCK stays the build for a person at a terminal: it is incremental, and it lets a sibling jolt-native checkout win over the pinned release. What this adds is the other build — on a machine with no Android SDK, no NDK, no hand-built Chez cross target and no ~/.cache at all: nix build .#apk The five steps are the same five, in the same order; every path .buckconfig.local answers for is answered by the store instead. The SDK and NDK come from androidenv (r29, the version scripts/android-ndk.dotslash pins), libvidya and the glue by the digests in scripts/*.dotslash, and the boot image from the addrinfo fork rather than upstream — an APK built with upstream cannot open a TLS connection on Bionic at all. The one piece with no nixpkgs equivalent is Chez's tarm64le workarea, so it is built here. ~/.cache/vidya-chez-android was the reference for the flags, but only a partial one: its Mf-config records LIBS with -lrt and -lpthread, which Bionic has no separate libraries for, so that make died once libkernel.a existed and was left there. buck only ever reads the artifacts that got made; a build that has to finish needs --disable-auto-flags, and --disable-curses because Bionic has no curses.h. OpenSSL is built by the NDK rather than by pkgsCross.aarch64-android, whose compiler-rt does not compile on this nixpkgs. .#apk signs with a keystore generated in the derivation, so it is installable and not reproducible; .#apk-unsigned is the pure output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6aa2a6b parent: 9cc5d99 modified
flake.lock +40 -0 | @@ -1,5 +1,24 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "nodes": { |
| 3 | + "chez-src": { | |
| 4 | + "flake": false, | |
| 5 | + "locked": { | |
| 6 | + "lastModified": 1778520543, | |
| 7 | + "narHash": "sha256-/AQbCbjfIpZjgQJB2E1QXSOfcTGF24xfBDOyZDUWtO0=", | |
| 8 | + "ref": "refs/tags/v10.4.1", | |
| 9 | + "rev": "e95a7efbafa2cf3bd5343ea542e6bc909a7ab2c4", | |
| 10 | + "revCount": 2525, | |
| 11 | + "submodules": true, | |
| 12 | + "type": "git", | |
| 13 | + "url": "https://github.com/cisco/ChezScheme" | |
| 14 | + }, | |
| 15 | + "original": { | |
| 16 | + "ref": "refs/tags/v10.4.1", | |
| 17 | + "submodules": true, | |
| 18 | + "type": "git", | |
| 19 | + "url": "https://github.com/cisco/ChezScheme" | |
| 20 | + } | |
| 21 | + }, | |
| 3 | 22 | "flake-utils": { |
| 4 | 23 | "inputs": { |
| 5 | 24 | "systems": "systems" |
| @@ -35,6 +54,25 @@ | ||
| 35 | 54 | "url": "https://gitlab.com/nandithebull/glimmer" |
| 36 | 55 | } |
| 37 | 56 | }, |
| 57 | + "jolt-android-src": { | |
| 58 | + "flake": false, | |
| 59 | + "locked": { | |
| 60 | + "lastModified": 1788137223, | |
| 61 | + "narHash": "sha256-VeHi864PEtZoHRNA7ms9uaJb6+7nHvDyRXqLLPQ1tao=", | |
| 62 | + "ref": "refs/heads/main", | |
| 63 | + "rev": "2b80d68d1f7a31ba92b208b3957e5fb555617ada", | |
| 64 | + "revCount": 2742, | |
| 65 | + "submodules": true, | |
| 66 | + "type": "git", | |
| 67 | + "url": "https://gitlab.com/nandithebull/jolt" | |
| 68 | + }, | |
| 69 | + "original": { | |
| 70 | + "rev": "2b80d68d1f7a31ba92b208b3957e5fb555617ada", | |
| 71 | + "submodules": true, | |
| 72 | + "type": "git", | |
| 73 | + "url": "https://gitlab.com/nandithebull/jolt" | |
| 74 | + } | |
| 75 | + }, | |
| 38 | 76 | "jolt-native": { |
| 39 | 77 | "flake": false, |
| 40 | 78 | "locked": { |
| @@ -108,7 +146,9 @@ | ||
| 108 | 146 | }, |
| 109 | 147 | "root": { |
| 110 | 148 | "inputs": { |
| 149 | + "chez-src": "chez-src", | |
| 111 | 150 | "glimmer": "glimmer", |
| 151 | + "jolt-android-src": "jolt-android-src", | |
| 112 | 152 | "jolt-native": "jolt-native", |
| 113 | 153 | "jolt-src": "jolt-src", |
| 114 | 154 | "nixgl": "nixgl", |
| @@ -1,5 +1,24 @@ | |||
| 1 | { | 1 | { |
| 2 | "nodes": { | 2 | "nodes": { |
| 3 | + "chez-src": { | ||
| 4 | + "flake": false, | ||
| 5 | + "locked": { | ||
| 6 | + "lastModified": 1778520543, | ||
| 7 | + "narHash": "sha256-/AQbCbjfIpZjgQJB2E1QXSOfcTGF24xfBDOyZDUWtO0=", | ||
| 8 | + "ref": "refs/tags/v10.4.1", | ||
| 9 | + "rev": "e95a7efbafa2cf3bd5343ea542e6bc909a7ab2c4", | ||
| 10 | + "revCount": 2525, | ||
| 11 | + "submodules": true, | ||
| 12 | + "type": "git", | ||
| 13 | + "url": "https://github.com/cisco/ChezScheme" | ||
| 14 | + }, | ||
| 15 | + "original": { | ||
| 16 | + "ref": "refs/tags/v10.4.1", | ||
| 17 | + "submodules": true, | ||
| 18 | + "type": "git", | ||
| 19 | + "url": "https://github.com/cisco/ChezScheme" | ||
| 20 | + } | ||
| 21 | + }, | ||
| 3 | "flake-utils": { | 22 | "flake-utils": { |
| 4 | "inputs": { | 23 | "inputs": { |
| 5 | "systems": "systems" | 24 | "systems": "systems" |
| @@ -35,6 +54,25 @@ | |||
| 35 | "url": "https://gitlab.com/nandithebull/glimmer" | 54 | "url": "https://gitlab.com/nandithebull/glimmer" |
| 36 | } | 55 | } |
| 37 | }, | 56 | }, |
| 57 | + "jolt-android-src": { | ||
| 58 | + "flake": false, | ||
| 59 | + "locked": { | ||
| 60 | + "lastModified": 1788137223, | ||
| 61 | + "narHash": "sha256-VeHi864PEtZoHRNA7ms9uaJb6+7nHvDyRXqLLPQ1tao=", | ||
| 62 | + "ref": "refs/heads/main", | ||
| 63 | + "rev": "2b80d68d1f7a31ba92b208b3957e5fb555617ada", | ||
| 64 | + "revCount": 2742, | ||
| 65 | + "submodules": true, | ||
| 66 | + "type": "git", | ||
| 67 | + "url": "https://gitlab.com/nandithebull/jolt" | ||
| 68 | + }, | ||
| 69 | + "original": { | ||
| 70 | + "rev": "2b80d68d1f7a31ba92b208b3957e5fb555617ada", | ||
| 71 | + "submodules": true, | ||
| 72 | + "type": "git", | ||
| 73 | + "url": "https://gitlab.com/nandithebull/jolt" | ||
| 74 | + } | ||
| 75 | + }, | ||
| 38 | "jolt-native": { | 76 | "jolt-native": { |
| 39 | "flake": false, | 77 | "flake": false, |
| 40 | "locked": { | 78 | "locked": { |
| @@ -108,7 +146,9 @@ | |||
| 108 | }, | 146 | }, |
| 109 | "root": { | 147 | "root": { |
| 110 | "inputs": { | 148 | "inputs": { |
| 149 | + "chez-src": "chez-src", | ||
| 111 | "glimmer": "glimmer", | 150 | "glimmer": "glimmer", |
| 151 | + "jolt-android-src": "jolt-android-src", | ||
| 112 | "jolt-native": "jolt-native", | 152 | "jolt-native": "jolt-native", |
| 113 | "jolt-src": "jolt-src", | 153 | "jolt-src": "jolt-src", |
| 114 | "nixgl": "nixgl", | 154 | "nixgl": "nixgl", |
modified
flake.nix +64 -3 | @@ -34,6 +34,26 @@ | ||
| 34 | 34 | flake = false; |
| 35 | 35 | }; |
| 36 | 36 | |
| 37 | + # Chez itself, because the APK needs a cross target nixpkgs does not | |
| 38 | + # build: frq's Scheme is compiled to an arm64 boot image, and that wants | |
| 39 | + # Chez's own `tarm64le` workarea — boot files, xpatch and libkernel.a. | |
| 40 | + # The version is the one the hand-built tree under ~/.cache used, and the | |
| 41 | + # submodules are not optional (zuo builds it, lz4 and zlib link into it). | |
| 42 | + # The fork jolt's own Android pin names, built here rather than fetched as | |
| 43 | + # a release binary: upstream reads the socket address out of `struct | |
| 44 | + # addrinfo` at glibc's offset, which on Bionic is `ai_canonname`, so an APK | |
| 45 | + # built with upstream cannot open a TLS connection at all. Only the boot | |
| 46 | + # image uses it; the desktop package still builds jolt-src. | |
| 47 | + jolt-android-src = { | |
| 48 | + url = "git+https://gitlab.com/nandithebull/jolt?rev=2b80d68d1f7a31ba92b208b3957e5fb555617ada&submodules=1"; | |
| 49 | + flake = false; | |
| 50 | + }; | |
| 51 | + | |
| 52 | + chez-src = { | |
| 53 | + url = "git+https://github.com/cisco/ChezScheme?ref=refs/tags/v10.4.1&submodules=1"; | |
| 54 | + flake = false; | |
| 55 | + }; | |
| 56 | + | |
| 37 | 57 | # The sha deps.edn pins, on the fork with the reconciler fixes. |
| 38 | 58 | glimmer = { |
| 39 | 59 | url = "git+https://gitlab.com/nandithebull/glimmer?rev=399df371c790d690fb6e4560c3d4d7f838502857"; |
| @@ -47,7 +67,7 @@ | ||
| 47 | 67 | }; |
| 48 | 68 | }; |
| 49 | 69 | |
| 50 | - outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, nixgl }: | |
| 70 | + outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, chez-src, jolt-android-src, nixgl }: | |
| 51 | 71 | let |
| 52 | 72 | systems = [ "x86_64-linux" "aarch64-linux" ]; |
| 53 | 73 | forEachSystem = f: |
| @@ -115,10 +135,13 @@ | ||
| 115 | 135 | }; |
| 116 | 136 | |
| 117 | 137 | # Jolt itself: Clojure on Chez, built the way its own flake builds it. |
| 118 | - joltRuntime = pkgs.stdenv.mkDerivation { | |
| 138 | + # A function, because there are two of them — upstream for the | |
| 139 | + # desktop, and the Bionic-addrinfo fork for the boot image the APK | |
| 140 | + # carries. Nothing else about the build differs. | |
| 141 | + joltFrom = src: pkgs.stdenv.mkDerivation { | |
| 119 | 142 | pname = "jolt"; |
| 120 | 143 | version = "dev"; |
| 121 | - src = jolt-src; | |
| 144 | + inherit src; | |
| 122 | 145 | |
| 123 | 146 | strictDeps = true; |
| 124 | 147 | nativeBuildInputs = with pkgs; [ chez makeWrapper pkg-config xxd ]; |
| @@ -150,6 +173,9 @@ | ||
| 150 | 173 | ''; |
| 151 | 174 | }; |
| 152 | 175 | |
| 176 | + joltRuntime = joltFrom jolt-src; | |
| 177 | + joltAndroid = joltFrom jolt-android-src; | |
| 178 | + | |
| 153 | 179 | # glimmer-vidya lives inside the jolt-native checkout, and its own |
| 154 | 180 | # deps.edn asks for glimmer by git — the top-level override below |
| 155 | 181 | # answers for both. |
| @@ -208,11 +234,46 @@ | ||
| 208 | 234 | mkdir -p "$out/bin" |
| 209 | 235 | ln -s ${frqScript} "$out/bin/frq" |
| 210 | 236 | ''; |
| 237 | + # --- Android ------------------------------------------------------ | |
| 238 | + # The SDK and the NDK are Google's, which means unfree and a licence | |
| 239 | + # to accept — so this is its own import of nixpkgs rather than the | |
| 240 | + # `legacyPackages` everything above uses. Confined to the Android | |
| 241 | + # outputs: `nix build` of frq itself never evaluates it. | |
| 242 | + # | |
| 243 | + # The NDK here is r29, which is the version scripts/android-ndk.dotslash | |
| 244 | + # pins and the one the pinned libvidya was built with. | |
| 245 | + androidPkgs = import nixpkgs { | |
| 246 | + inherit (pkgs.stdenv.hostPlatform) system; | |
| 247 | + config = { | |
| 248 | + allowUnfree = true; | |
| 249 | + android_sdk.accept_license = true; | |
| 250 | + }; | |
| 251 | + }; | |
| 252 | + | |
| 253 | + androidComposition = androidPkgs.androidenv.composeAndroidPackages { | |
| 254 | + buildToolsVersions = [ "36.0.0" ]; | |
| 255 | + platformVersions = [ "36" ]; | |
| 256 | + includeNDK = true; | |
| 257 | + }; | |
| 258 | + | |
| 259 | + android = import ./nix/android.nix { | |
| 260 | + inherit pkgs self chez-src jolt-native glimmer joltAndroid; | |
| 261 | + inherit (pkgs) lib; | |
| 262 | + androidSdk = androidComposition.androidsdk; | |
| 263 | + ndk = androidComposition.ndk-bundle; | |
| 264 | + }; | |
| 211 | 265 | in |
| 212 | 266 | { |
| 213 | 267 | inherit native frq; |
| 214 | 268 | jolt = joltRuntime; |
| 215 | 269 | default = frq; |
| 270 | + } | |
| 271 | + # An APK is built by a linux-x86_64 NDK and a linux-x86_64 jolt, and | |
| 272 | + # Google ships no other; on aarch64 the Android outputs are simply | |
| 273 | + # absent rather than present and broken. | |
| 274 | + // lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") { | |
| 275 | + inherit (android) apk chezAndroid joltBoot libjoltapp; | |
| 276 | + apk-unsigned = android.apk-unsigned; | |
| 216 | 277 | }); |
| 217 | 278 | |
| 218 | 279 | apps = forEachSystem (pkgs: { |
| @@ -34,6 +34,26 @@ | |||
| 34 | flake = false; | 34 | flake = false; |
| 35 | }; | 35 | }; |
| 36 | 36 | ||
| 37 | + # Chez itself, because the APK needs a cross target nixpkgs does not | ||
| 38 | + # build: frq's Scheme is compiled to an arm64 boot image, and that wants | ||
| 39 | + # Chez's own `tarm64le` workarea — boot files, xpatch and libkernel.a. | ||
| 40 | + # The version is the one the hand-built tree under ~/.cache used, and the | ||
| 41 | + # submodules are not optional (zuo builds it, lz4 and zlib link into it). | ||
| 42 | + # The fork jolt's own Android pin names, built here rather than fetched as | ||
| 43 | + # a release binary: upstream reads the socket address out of `struct | ||
| 44 | + # addrinfo` at glibc's offset, which on Bionic is `ai_canonname`, so an APK | ||
| 45 | + # built with upstream cannot open a TLS connection at all. Only the boot | ||
| 46 | + # image uses it; the desktop package still builds jolt-src. | ||
| 47 | + jolt-android-src = { | ||
| 48 | + url = "git+https://gitlab.com/nandithebull/jolt?rev=2b80d68d1f7a31ba92b208b3957e5fb555617ada&submodules=1"; | ||
| 49 | + flake = false; | ||
| 50 | + }; | ||
| 51 | + | ||
| 52 | + chez-src = { | ||
| 53 | + url = "git+https://github.com/cisco/ChezScheme?ref=refs/tags/v10.4.1&submodules=1"; | ||
| 54 | + flake = false; | ||
| 55 | + }; | ||
| 56 | + | ||
| 37 | # The sha deps.edn pins, on the fork with the reconciler fixes. | 57 | # The sha deps.edn pins, on the fork with the reconciler fixes. |
| 38 | glimmer = { | 58 | glimmer = { |
| 39 | url = "git+https://gitlab.com/nandithebull/glimmer?rev=399df371c790d690fb6e4560c3d4d7f838502857"; | 59 | url = "git+https://gitlab.com/nandithebull/glimmer?rev=399df371c790d690fb6e4560c3d4d7f838502857"; |
| @@ -47,7 +67,7 @@ | |||
| 47 | }; | 67 | }; |
| 48 | }; | 68 | }; |
| 49 | 69 | ||
| 50 | - outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, nixgl }: | 70 | + outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, chez-src, jolt-android-src, nixgl }: |
| 51 | let | 71 | let |
| 52 | systems = [ "x86_64-linux" "aarch64-linux" ]; | 72 | systems = [ "x86_64-linux" "aarch64-linux" ]; |
| 53 | forEachSystem = f: | 73 | forEachSystem = f: |
| @@ -115,10 +135,13 @@ | |||
| 115 | }; | 135 | }; |
| 116 | 136 | ||
| 117 | # Jolt itself: Clojure on Chez, built the way its own flake builds it. | 137 | # Jolt itself: Clojure on Chez, built the way its own flake builds it. |
| 118 | - joltRuntime = pkgs.stdenv.mkDerivation { | 138 | + # A function, because there are two of them — upstream for the |
| 139 | + # desktop, and the Bionic-addrinfo fork for the boot image the APK | ||
| 140 | + # carries. Nothing else about the build differs. | ||
| 141 | + joltFrom = src: pkgs.stdenv.mkDerivation { | ||
| 119 | pname = "jolt"; | 142 | pname = "jolt"; |
| 120 | version = "dev"; | 143 | version = "dev"; |
| 121 | - src = jolt-src; | 144 | + inherit src; |
| 122 | 145 | ||
| 123 | strictDeps = true; | 146 | strictDeps = true; |
| 124 | nativeBuildInputs = with pkgs; [ chez makeWrapper pkg-config xxd ]; | 147 | nativeBuildInputs = with pkgs; [ chez makeWrapper pkg-config xxd ]; |
| @@ -150,6 +173,9 @@ | |||
| 150 | ''; | 173 | ''; |
| 151 | }; | 174 | }; |
| 152 | 175 | ||
| 176 | + joltRuntime = joltFrom jolt-src; | ||
| 177 | + joltAndroid = joltFrom jolt-android-src; | ||
| 178 | + | ||
| 153 | # glimmer-vidya lives inside the jolt-native checkout, and its own | 179 | # glimmer-vidya lives inside the jolt-native checkout, and its own |
| 154 | # deps.edn asks for glimmer by git — the top-level override below | 180 | # deps.edn asks for glimmer by git — the top-level override below |
| 155 | # answers for both. | 181 | # answers for both. |
| @@ -208,11 +234,46 @@ | |||
| 208 | mkdir -p "$out/bin" | 234 | mkdir -p "$out/bin" |
| 209 | ln -s ${frqScript} "$out/bin/frq" | 235 | ln -s ${frqScript} "$out/bin/frq" |
| 210 | ''; | 236 | ''; |
| 237 | + # --- Android ------------------------------------------------------ | ||
| 238 | + # The SDK and the NDK are Google's, which means unfree and a licence | ||
| 239 | + # to accept — so this is its own import of nixpkgs rather than the | ||
| 240 | + # `legacyPackages` everything above uses. Confined to the Android | ||
| 241 | + # outputs: `nix build` of frq itself never evaluates it. | ||
| 242 | + # | ||
| 243 | + # The NDK here is r29, which is the version scripts/android-ndk.dotslash | ||
| 244 | + # pins and the one the pinned libvidya was built with. | ||
| 245 | + androidPkgs = import nixpkgs { | ||
| 246 | + inherit (pkgs.stdenv.hostPlatform) system; | ||
| 247 | + config = { | ||
| 248 | + allowUnfree = true; | ||
| 249 | + android_sdk.accept_license = true; | ||
| 250 | + }; | ||
| 251 | + }; | ||
| 252 | + | ||
| 253 | + androidComposition = androidPkgs.androidenv.composeAndroidPackages { | ||
| 254 | + buildToolsVersions = [ "36.0.0" ]; | ||
| 255 | + platformVersions = [ "36" ]; | ||
| 256 | + includeNDK = true; | ||
| 257 | + }; | ||
| 258 | + | ||
| 259 | + android = import ./nix/android.nix { | ||
| 260 | + inherit pkgs self chez-src jolt-native glimmer joltAndroid; | ||
| 261 | + inherit (pkgs) lib; | ||
| 262 | + androidSdk = androidComposition.androidsdk; | ||
| 263 | + ndk = androidComposition.ndk-bundle; | ||
| 264 | + }; | ||
| 211 | in | 265 | in |
| 212 | { | 266 | { |
| 213 | inherit native frq; | 267 | inherit native frq; |
| 214 | jolt = joltRuntime; | 268 | jolt = joltRuntime; |
| 215 | default = frq; | 269 | default = frq; |
| 270 | + } | ||
| 271 | + # An APK is built by a linux-x86_64 NDK and a linux-x86_64 jolt, and | ||
| 272 | + # Google ships no other; on aarch64 the Android outputs are simply | ||
| 273 | + # absent rather than present and broken. | ||
| 274 | + // lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") { | ||
| 275 | + inherit (android) apk chezAndroid joltBoot libjoltapp; | ||
| 276 | + apk-unsigned = android.apk-unsigned; | ||
| 216 | }); | 277 | }); |
| 217 | 278 | ||
| 218 | apps = forEachSystem (pkgs: { | 279 | apps = forEachSystem (pkgs: { |
added
nix/android.nix +364 -0 | new file mode 100644 | ||
| @@ -0,0 +1,364 @@ | ||
| 1 | +# 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 | |
| 4 | +# 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 | + | |
| 28 | +let | |
| 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 | + ''; | |
| 360 | +in | |
| 361 | +{ | |
| 362 | + inherit chezAndroid joltBoot libjoltapp classesDex apk; | |
| 363 | + apk-unsigned = apkUnsigned; | |
| 364 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,364 @@ | |||
| 1 | +# 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 | ||
| 4 | +# 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 | + | ||
| 28 | +let | ||
| 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 | + ''; | ||
| 360 | +in | ||
| 361 | +{ | ||
| 362 | + inherit chezAndroid joltBoot libjoltapp classesDex apk; | ||
| 363 | + apk-unsigned = apkUnsigned; | ||
| 364 | +} | ||