forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
286 lines (238 loc) · 12.6 KB
/
android.yml
File metadata and controls
286 lines (238 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
name: Build Android APK
on:
push:
branches: [dev]
paths:
- 'packages/mobile/**'
- 'packages/opencode/**'
- 'packages/app/**'
- '.github/workflows/android.yml'
workflow_dispatch:
jobs:
build-android:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Install Android NDK + CMake
run: |
# Remove ALL pre-installed NDKs to avoid version conflicts
rm -rf $ANDROID_HOME/ndk/* 2>/dev/null || true
sdkmanager "ndk;27.0.12077973" "cmake;3.22.1"
echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/mobile/src-tauri
cache-targets: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
# ─── Prepare runtime binaries ──────────────────────────────────
- name: Download runtime binaries
run: |
JNIDIR="packages/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a"
ASSETS="packages/mobile/src-tauri/gen/android/app/src/main/assets/runtime"
RUNTIME="packages/mobile/src-tauri/assets/runtime"
mkdir -p "$JNIDIR" "$ASSETS" "$RUNTIME/node_modules/@parcel/watcher"
# Bun (aarch64-linux-musl)
echo "Downloading Bun..."
curl -fsSL https://github.com/oven-sh/bun/releases/latest/download/bun-linux-aarch64-musl.zip -o /tmp/bun.zip
unzip -o -q /tmp/bun.zip -d /tmp/bun-extract
cp /tmp/bun-extract/bun-linux-aarch64-musl/bun "$JNIDIR/libbun_exec.so"
# Resolve the latest apk revision from the Alpine index. Alpine bumps
# `-r*` revisions without warning, so hardcoding breaks the build with 404.
ALPINE_INDEX="https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64"
resolve_apk() {
local pkg="$1"
curl -fsSL "$ALPINE_INDEX/" \
| grep -oE "${pkg}-[0-9][0-9.]*-r[0-9]+\.apk" \
| sort -uV | tail -1
}
# musl dynamic linker
echo "Downloading musl libc..."
MUSL_APK=$(resolve_apk "musl")
echo "Resolved: $MUSL_APK"
curl -fsSL "$ALPINE_INDEX/$MUSL_APK" -o /tmp/musl.apk
cd /tmp && tar -xzf musl.apk lib/ld-musl-aarch64.so.1
cp /tmp/lib/ld-musl-aarch64.so.1 "$GITHUB_WORKSPACE/$JNIDIR/libmusl_linker.so"
cd "$GITHUB_WORKSPACE"
# libstdc++ and libgcc_s
echo "Downloading C++ runtime libs..."
LIBSTDCPP_APK=$(resolve_apk "libstdc\+\+")
LIBGCC_APK=$(resolve_apk "libgcc")
echo "Resolved: $LIBSTDCPP_APK / $LIBGCC_APK"
curl -fsSL "$ALPINE_INDEX/$LIBSTDCPP_APK" -o /tmp/libstdcpp.apk
curl -fsSL "$ALPINE_INDEX/$LIBGCC_APK" -o /tmp/libgcc.apk
cd /tmp && tar -xzf libstdcpp.apk --wildcards 'usr/lib/libstdc++.so.6.*'
cd /tmp && tar -xzf libgcc.apk usr/lib/libgcc_s.so.1
cp /tmp/usr/lib/libstdc++.so.6.* "$GITHUB_WORKSPACE/$JNIDIR/libstdcpp_compat.so"
cp /tmp/usr/lib/libgcc_s.so.1 "$GITHUB_WORKSPACE/$JNIDIR/libgcc_compat.so"
cd "$GITHUB_WORKSPACE"
# Bash (static)
echo "Downloading Bash..."
curl -fsSL https://github.com/robxu9/bash-static/releases/latest/download/bash-linux-aarch64 -o "$JNIDIR/libbash_exec.so"
# Ripgrep
echo "Downloading Ripgrep..."
RG_VERSION=$(curl -fsSL "https://api.github.com/repos/BurntSushi/ripgrep/releases/latest" | jq -r '.tag_name')
curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/ripgrep-${RG_VERSION}-aarch64-unknown-linux-gnu.tar.gz" -o /tmp/rg.tar.gz
tar -xzf /tmp/rg.tar.gz -C /tmp
find /tmp -name "rg" -type f | head -1 | xargs -I{} cp {} "$JNIDIR/librg_exec.so"
# Toybox — provides standard Unix commands (ls, cat, grep, etc.)
# Android SELinux blocks /system/bin exec from app sandbox, so we bundle
# our own multi-call binary. Symlinks are created at runtime by runtime.rs.
echo "Downloading Toybox..."
curl -fsSL "https://landley.net/toybox/bin/toybox-aarch64" -o "$JNIDIR/libtoybox_exec.so"
# PTY Server — TCP relay for PTY sessions, compiled with NDK (bionic).
# Spawned from Java Foreground Service (Seccomp: 0) so bash children
# can fork+exec external commands. Bun connects via TCP instead of FFI.
echo "Compiling PTY server..."
NDK=$ANDROID_HOME/ndk/27.0.12077973
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang \
-o "$JNIDIR/libpty_server.so" \
packages/mobile/src-tauri/gen/android/pty_server.c \
-O2 -Wall -Wextra -Wno-unused-parameter
echo "PTY server compiled: $(ls -lh "$JNIDIR/libpty_server.so")"
chmod 755 "$JNIDIR"/*
echo "JNI libs:"
ls -lh "$JNIDIR/"
- name: Build llama.cpp for Android
run: |
JNIDIR="packages/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a"
HEADERS="packages/mobile/src-tauri/gen/android/llama-headers"
NDK=$ANDROID_HOME/ndk/27.0.12077973
# Clone llama.cpp
git clone --depth 1 https://github.com/ggml-org/llama.cpp.git /tmp/llama.cpp
# Build shared libraries with NDK
cmake -B /tmp/llama.cpp/build-android \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-28 \
-DCMAKE_C_FLAGS="-march=armv8.2-a+dotprod+fp16" \
-DCMAKE_CXX_FLAGS="-march=armv8.2-a+dotprod+fp16" \
-DGGML_OPENMP=OFF \
-DGGML_LLAMAFILE=OFF \
-DLLAMA_CURL=OFF \
-DBUILD_SHARED_LIBS=ON \
-DCMAKE_BUILD_TYPE=Release \
-S /tmp/llama.cpp
cmake --build /tmp/llama.cpp/build-android --config Release -j$(nproc)
# Copy shared libs to jniLibs
cp /tmp/llama.cpp/build-android/bin/libllama.so "$JNIDIR/"
cp /tmp/llama.cpp/build-android/bin/libggml.so "$JNIDIR/"
cp /tmp/llama.cpp/build-android/bin/libggml-base.so "$JNIDIR/"
cp /tmp/llama.cpp/build-android/bin/libggml-cpu.so "$JNIDIR/"
# Strip
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip "$JNIDIR"/libllama.so "$JNIDIR"/libggml*.so
# Also build static llama-server for HTTP endpoint
cmake -B /tmp/llama.cpp/build-static \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DCMAKE_C_COMPILER=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang \
-DCMAKE_CXX_COMPILER=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang++ \
-DCMAKE_C_FLAGS="-march=armv8.2-a" \
-DCMAKE_CXX_FLAGS="-march=armv8.2-a" \
-DBUILD_SHARED_LIBS=OFF \
-DGGML_OPENMP=OFF \
-DGGML_LLAMAFILE=OFF \
-DLLAMA_CURL=OFF \
-DCMAKE_BUILD_TYPE=Release \
-S /tmp/llama.cpp
cmake --build /tmp/llama.cpp/build-static --config Release --target llama-server -j$(nproc)
cp /tmp/llama.cpp/build-static/bin/llama-server "$JNIDIR/libllama_server.so"
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip "$JNIDIR/libllama_server.so"
# Copy headers for JNI compilation
mkdir -p "$HEADERS"
cp /tmp/llama.cpp/include/*.h "$HEADERS/"
cp /tmp/llama.cpp/ggml/include/*.h "$HEADERS/"
echo "llama.cpp built:"
ls -lh "$JNIDIR"/libllama*.so "$JNIDIR"/libggml*.so
- name: Bundle OpenCode CLI
run: |
RUNTIME="packages/mobile/src-tauri/assets/runtime"
ASSETS="packages/mobile/src-tauri/gen/android/app/src/main/assets/runtime"
mkdir -p "$RUNTIME/node_modules/@parcel/watcher" "$ASSETS/node_modules/@parcel/watcher"
# Bundle CLI JS with inlined SQL migrations.
# Uses scripts/bundle-mobile.mjs to avoid shell escaping issues
# (SQL contains backticks, tabs, newlines that corrupt --define).
node scripts/bundle-mobile.mjs --outdir "$RUNTIME"
# Parcel watcher shim
echo 'export function createWrapper() { return undefined }' > "$RUNTIME/node_modules/@parcel/watcher/wrapper.js"
echo '{"name":"@parcel/watcher","version":"0.0.0","main":"wrapper.js"}' > "$RUNTIME/node_modules/@parcel/watcher/package.json"
# Copy to gen/android assets
cp "$RUNTIME/opencode-cli.js" "$ASSETS/opencode-cli.js"
cp -r "$RUNTIME/node_modules" "$ASSETS/"
# Copy wasm files if present
cp "$RUNTIME"/*.wasm "$ASSETS/" 2>/dev/null || true
echo "CLI bundle: $(du -sh "$RUNTIME/opencode-cli.js" | cut -f1)"
# ─── Build APK ────────────────────────────────────────────────
- name: Install Tauri CLI
run: |
bun add -g @tauri-apps/cli
cargo install tauri-cli --version "^2" --locked || true
- name: Build frontend
working-directory: packages/mobile
run: bun run build
- name: Setup Android NDK env
run: |
# Remove any cached .cargo/config.toml with Windows paths
rm -f packages/mobile/src-tauri/.cargo/config.toml
- name: Download ONNX Runtime for Android
run: |
echo "Downloading ONNX Runtime for Android (aarch64)..."
ORT_VERSION="1.22.0"
# Download Microsoft's official ONNX Runtime Android AAR
curl -fsSL "https://repo1.maven.org/maven2/com/microsoft/onnxruntime/onnxruntime-android/${ORT_VERSION}/onnxruntime-android-${ORT_VERSION}.aar" \
-o /tmp/ort.aar
# Extract the native .so for arm64
cd /tmp && unzip -o ort.aar "jni/arm64-v8a/*"
mkdir -p /tmp/ort-android/lib
cp /tmp/jni/arm64-v8a/libonnxruntime.so /tmp/ort-android/lib/
ls -la /tmp/ort-android/lib/
# Also need the headers — download the full release for headers
curl -fsSL "https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-android-${ORT_VERSION}.aar" \
-o /tmp/ort-headers.aar 2>/dev/null || true
# Set ORT_LIB_LOCATION for ort-sys build
echo "ORT_LIB_LOCATION=/tmp/ort-android" >> $GITHUB_ENV
echo "ORT_PREFER_DYNAMIC_LINK=1" >> $GITHUB_ENV
echo "ONNX Runtime Android ready at /tmp/ort-android"
- name: Build Android APK
working-directory: packages/mobile
run: bunx tauri android build --target aarch64
# ─── Sign APK ─────────────────────────────────────────────────
- name: Sign APK
run: |
APK="packages/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk"
SIGNED="packages/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/opencode-mobile.apk"
# Create a debug keystore for CI
keytool -genkey -v \
-keystore /tmp/ci.keystore \
-alias opencode -keyalg RSA -keysize 2048 -validity 10000 \
-storepass android -keypass android \
-dname "CN=OpenCode CI, OU=Dev, O=OpenCode, L=Paris, ST=IDF, C=FR"
cp "$APK" "$SIGNED"
java -jar "$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools | tail -1)/lib/apksigner.jar" sign \
--ks /tmp/ci.keystore \
--ks-pass pass:android \
--key-pass pass:android \
"$SIGNED"
echo "Signed APK: $(du -sh "$SIGNED" | cut -f1)"
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: opencode-mobile-apk
path: packages/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/opencode-mobile.apk
retention-days: 30