docs(i18n): propagate 2026-04-17 session updates to 21 translated REA… #95
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |