feat(Android): implement the native VPN tunnel #14
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: CI | |
| # Validation for every push to main and every pull request. Build/sign/publish live in | |
| # release.yml (tag-triggered) - this workflow never releases anything, it only catches | |
| # breakage before it lands: type errors, lint, Rust formatting, and clippy. | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| # Supersede in-flight runs for the same ref (PR pushes), but let main runs finish. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| jobs: | |
| frontend: | |
| name: Frontend (check, build) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - run: npm ci | |
| # `azeroth check` is the combined gate: typecheck + lint in one pass, which is | |
| # why there is no separate lint step. | |
| - name: Check (typecheck + lint) | |
| run: npm run check | |
| - name: Build | |
| run: npm run build | |
| rust: | |
| name: Rust (rustfmt, clippy) | |
| # The backend is platform-dispatched (src-tauri/src/platform): the Windows code is | |
| # behind cfg(windows) and other targets compile the `unsupported` fallback (Linux | |
| # gets its real backend in a later phase). So clippy runs on every desktop OS - a | |
| # cross-platform break fails here the moment it lands, not only when someone builds | |
| # on that OS. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ ubuntu-latest, macos-latest, windows-latest ] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: src-tauri | |
| # Tauri's Linux crates (webkit2gtk/gtk/soup/javascriptcore -sys) need their dev | |
| # packages present for clippy to even compile the dependency tree. | |
| - name: Install Linux Tauri dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev | |
| - name: Provide a placeholder frontend dist | |
| shell: bash | |
| run: | | |
| # tauri's generate_context! reads frontendDist (../dist) at compile | |
| # time, so the directory must exist for clippy/check to compile. Its | |
| # contents don't affect Rust lints, so a stub is enough - this job | |
| # deliberately does not build the real frontend. | |
| mkdir -p dist | |
| printf '<!doctype html><title>ci</title>' > dist/index.html | |
| - name: rustfmt | |
| working-directory: src-tauri | |
| run: cargo fmt --all --check | |
| - name: clippy | |
| working-directory: src-tauri | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| linux-build: | |
| name: Linux build (deb + AppImage) | |
| # A full tauri build is heavy, so it runs on pushes to main only - the rust matrix | |
| # above already proves cross-platform compilation cheaply on every PR. This proves | |
| # the Linux app actually BUNDLES, in exactly the shape release.yml ships: the .deb | |
| # plus the AppImage, which is the Linux updater target. | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: src-tauri | |
| - name: Install Linux Tauri dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev | |
| - run: npm ci | |
| - name: Fetch the Linux Xray core | |
| run: node scripts/fetch-core.mjs --target linux-64 | |
| # createUpdaterArtifacts signs the build, so the updater key is needed even for | |
| # a throwaway CI bundle. Secrets are only present on same-repo pushes, which is | |
| # exactly where this job runs. | |
| - name: Build the .deb + AppImage | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: npm run tauri build -- --bundles deb,appimage | |
| macos-build: | |
| name: macOS build (universal app + dmg) | |
| # Heavy; pushes to main only. Proves the macOS app bundles in exactly the shape | |
| # release.yml ships - a UNIVERSAL build whose Xray core is two arch binaries fused | |
| # with lipo - so that path is exercised on a push instead of first at tag time. | |
| # UNSIGNED: code-signing + notarization need a paid Apple Developer account. | |
| if: github.event_name == 'push' | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin,x86_64-apple-darwin | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: src-tauri | |
| - run: npm ci | |
| - name: Fetch and fuse the macOS Xray cores | |
| run: | | |
| node scripts/fetch-core.mjs --target macos-arm64-v8a | |
| mv src-tauri/assets/app-xray "$RUNNER_TEMP/app-xray-arm64" | |
| node scripts/fetch-core.mjs --target macos-64 | |
| mv src-tauri/assets/app-xray "$RUNNER_TEMP/app-xray-x86_64" | |
| lipo -create "$RUNNER_TEMP/app-xray-arm64" "$RUNNER_TEMP/app-xray-x86_64" \ | |
| -output src-tauri/assets/app-xray | |
| chmod +x src-tauri/assets/app-xray | |
| lipo -info src-tauri/assets/app-xray | |
| - name: Build the universal .app + .dmg (unsigned) | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: npm run tauri build -- --target universal-apple-darwin --bundles app,dmg | |
| windows-build: | |
| name: Windows build (nsis) | |
| # Symmetry with the Linux/macOS build jobs: prove the Windows installer bundles on | |
| # every push to main (the rust matrix only compiles). The core (app-xray.exe + | |
| # wintun.dll) is committed, so no fetch. The signed public release is still cut by | |
| # release.yml on a tag. | |
| if: github.event_name == 'push' | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: src-tauri | |
| - run: npm ci | |
| - name: Build the installer (nsis) | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: npm run tauri build -- --bundles nsis | |
| android-build: | |
| name: Android build (apk) | |
| # Best-effort (continue-on-error): assembles the APK to catch build breaks. The | |
| # native VPN plugin (src-tauri/mobile) is not wired yet, so this stays non-blocking | |
| # until `tauri android init` + the plugin land. Pushes to main only. | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: 17 | |
| - uses: android-actions/setup-android@v3 | |
| - name: Install the Android NDK | |
| run: | | |
| sdkmanager "ndk;27.0.12077973" | |
| echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> "$GITHUB_ENV" | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-linux-android,x86_64-linux-android | |
| - name: Install Linux Tauri dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev | |
| - run: npm ci | |
| - name: Fetch the Android Xray core | |
| run: npm run fetch-core:android | |
| # No prebuilt Android tun2socks exists, so it is built from source here. It is | |
| # what bridges the VpnService tun fd into the core's SOCKS inbound. | |
| - name: Build tun2socks | |
| run: npm run build-tun2socks | |
| - name: Assemble the APK | |
| run: npm run tauri android build -- --apk --target aarch64 | |
| # iOS CI is intentionally omitted for now: it needs `tauri ios init` (a Mac), a Packet | |
| # Tunnel extension target, an xcframework core, and a paid Apple Developer account to | |
| # sign. Build it locally with `npm run ios-build` once set up (see src-tauri/mobile). |