Skip to content

docs(i18n): propagate 2026-04-17 session updates to 21 translated REA… #95

docs(i18n): propagate 2026-04-17 session updates to 21 translated REA…

docs(i18n): propagate 2026-04-17 session updates to 21 translated REA… #95

Workflow file for this run

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"
# musl dynamic linker
echo "Downloading musl libc..."
curl -fsSL https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/musl-1.2.5-r9.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..."
curl -fsSL https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/libstdc++-14.2.0-r4.apk -o /tmp/libstdcpp.apk
curl -fsSL https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/libgcc-14.2.0-r4.apk -o /tmp/libgcc.apk
cd /tmp && tar -xzf libstdcpp.apk usr/lib/libstdc++.so.6.0.33
cd /tmp && tar -xzf libgcc.apk usr/lib/libgcc_s.so.1
cp /tmp/usr/lib/libstdc++.so.6.0.33 "$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