From ee57dd1320e7d6bb07125f6f553088a0a43a22fe Mon Sep 17 00:00:00 2001 From: Alex Moreton Date: Wed, 8 Jul 2026 23:55:37 +0100 Subject: [PATCH] Android config amends for eas build --- .github/workflows/native-artifacts.yml | 126 ++++++++++++++++++++++--- apps/mobile/eas.json | 1 + scripts/fetch-native-artifacts.sh | 74 ++++++++------- 3 files changed, 152 insertions(+), 49 deletions(-) diff --git a/.github/workflows/native-artifacts.yml b/.github/workflows/native-artifacts.yml index 83eb61b..dc30de9 100644 --- a/.github/workflows/native-artifacts.yml +++ b/.github/workflows/native-artifacts.yml @@ -1,21 +1,22 @@ -# Producer for gitignored native binaries (Rust → xcframework), consumed by -# scripts/fetch-native-artifacts.sh (EAS eas-build-pre-install hook + CI). +# Producer for gitignored native binaries (Rust → xcframework / jniLibs), +# consumed by scripts/fetch-native-artifacts.sh (EAS eas-build-pre-install hook +# + CI). # -# Model — the compiled xcframework is non-reproducible (embedded timestamps + +# Model — the compiled binaries are non-reproducible (embedded timestamps + # build paths, see .gitignore), so CI is the SOLE builder/publisher: it builds, # uploads the asset to R2 keyed by the Rust SOURCE fingerprint, and commits the -# refreshed checksum manifest + .artifact-key. That published asset therefore -# always matches the committed manifest the consumer verifies against. +# refreshed checksum manifest. That published asset therefore always matches the +# committed manifest the consumer verifies against. # # Idempotent: if the asset for the current source key already exists on R2, the -# expensive Rust/iOS build is skipped — reruns and non-Rust PR syncs are cheap. +# expensive Rust build is skipped — reruns and non-Rust PR syncs are cheap. # # ── Required config ─────────────────────────────────────────────────────────── # secrets.R2_ACCOUNT_ID Cloudflare account id (R2 S3 endpoint host) -# secrets.R2_ACCESS_KEY_ID R2 access key (Object Read & Write) +# secrets.R2_ACCESS_KEY_ID R2 access key (Object Read & Write, 32-char) # secrets.R2_SECRET_ACCESS_KEY R2 secret key # vars.R2_BUCKET bucket name, e.g. mortstack-native -# vars.R2_PUBLIC_BASE_URL public-read base, e.g. https://native./mortstack +# vars.R2_PUBLIC_BASE_URL public-read base, e.g. https://pub-xxxx.r2.dev name: Native artifacts on: @@ -55,7 +56,6 @@ jobs: - name: Checkout uses: actions/checkout@v5 with: - # Push commits back to the PR/main branch, not a detached HEAD. ref: ${{ github.head_ref || github.ref_name }} - name: Compute artifact key @@ -117,11 +117,8 @@ jobs: --only-show-errors echo "→ uploaded ${{ steps.key.outputs.asset }}" - # The build refreshes ios/*.sha256 + ios/Sources/ and we stamp the key. - # Committing them (only when changed) keeps the consumer's verify gate in - # lockstep with the asset just published. - name: Commit refreshed manifest + key - if: steps.exists.outputs.found != 'true' && github.event_name != 'pull_request' || (steps.exists.outputs.found != 'true' && github.event.pull_request.head.repo.full_name == github.repository) + if: steps.exists.outputs.found != 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) run: | echo "${{ steps.key.outputs.key }}" > "$PKG/ios/.artifact-key" git config user.name "github-actions[bot]" @@ -134,5 +131,108 @@ jobs: echo "nothing to commit" else git commit -m "chore(native): publish chat_mls_coreFFI ios (${{ steps.key.outputs.key }})" + git pull --rebase --autostash origin "${{ github.head_ref || github.ref_name }}" || true + git push + fi + + android: + name: chat_mls_core (Android jniLibs) + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + PKG: packages/chat-mls-core + R2_PUBLIC_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL }} + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.head_ref || github.ref_name }} + + - name: Compute artifact key + id: key + run: | + KEY="$(bash scripts/artifact-key.sh "$PKG" android)" + echo "key=$KEY" >> "$GITHUB_OUTPUT" + echo "asset=chat_mls_core-android-$KEY.tar.gz" >> "$GITHUB_OUTPUT" + + - name: Already published? + id: exists + run: | + URL="${R2_PUBLIC_BASE_URL%/}/${{ steps.key.outputs.asset }}" + if curl -fsI "$URL" >/dev/null 2>&1; then + echo "found=true" >> "$GITHUB_OUTPUT" + echo "✓ $URL already published — skipping build" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Setup Java + if: steps.exists.outputs.found != 'true' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Setup Rust + Android targets + if: steps.exists.outputs.found != 'true' + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android + + - name: Cache Rust build + if: steps.exists.outputs.found != 'true' + uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/chat-mls-core + + - name: Install cargo-ndk + if: steps.exists.outputs.found != 'true' + run: cargo install cargo-ndk --locked + + - name: Build Android jniLibs + if: steps.exists.outputs.found != 'true' + run: | + # ubuntu-latest ships an Android NDK; build-mls.sh needs ANDROID_NDK_ROOT. + export ANDROID_NDK_ROOT="${ANDROID_NDK_ROOT:-${ANDROID_NDK_LATEST_HOME:-$ANDROID_NDK_HOME}}" + echo "using NDK: $ANDROID_NDK_ROOT" + bash "$PKG/scripts/build-mls.sh" android + + - name: Package + if: steps.exists.outputs.found != 'true' + run: | + tar czf "${{ steps.key.outputs.asset }}" \ + -C "$PKG/android/src/main" jniLibs + + - name: Upload to R2 + if: steps.exists.outputs.found != 'true' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_BUCKET: ${{ vars.R2_BUCKET }} + run: | + aws s3 cp "${{ steps.key.outputs.asset }}" \ + "s3://${R2_BUCKET}/${{ steps.key.outputs.asset }}" \ + --endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" \ + --only-show-errors + echo "→ uploaded ${{ steps.key.outputs.asset }}" + + # Commits jniLibs.sha256 + the regenerated UniFFI Kotlin (java/uniffi) + + # its .sha256 + the key. Kept in lockstep with the published asset. + - name: Commit refreshed manifests + key + if: steps.exists.outputs.found != 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + run: | + echo "${{ steps.key.outputs.key }}" > "$PKG/android/src/main/.artifact-key" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$PKG/android/src/main/jniLibs.sha256" \ + "$PKG/android/src/main/java/uniffi" \ + "$PKG/android/src/main/.artifact-key" + if git diff --cached --quiet; then + echo "nothing to commit" + else + git commit -m "chore(native): publish chat_mls_core android (${{ steps.key.outputs.key }})" + git pull --rebase --autostash origin "${{ github.head_ref || github.ref_name }}" || true git push fi diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json index 4792160..65d906f 100644 --- a/apps/mobile/eas.json +++ b/apps/mobile/eas.json @@ -19,6 +19,7 @@ }, "preview": { "distribution": "internal", + "android": { "buildType": "apk" }, "env": { "R2_PUBLIC_BASE_URL": "https://pub-2ffb489eefe84de8b9b32e409fd30957.r2.dev" } diff --git a/scripts/fetch-native-artifacts.sh b/scripts/fetch-native-artifacts.sh index 6e48d09..079968e 100755 --- a/scripts/fetch-native-artifacts.sh +++ b/scripts/fetch-native-artifacts.sh @@ -1,27 +1,27 @@ #!/usr/bin/env bash -# Restore a package's gitignored native binary from R2 and verify it against the -# committed checksum manifest. Runs as the EAS `eas-build-pre-install` hook (on -# Expo's builder, before `pod install`) and anywhere else the xcframework binary -# is needed but not in git. +# Restore a package's gitignored native binaries from R2 and verify them against +# the committed checksum manifest. Runs as the EAS `eas-build-pre-install` hook +# (on Expo's builder, before pod install / gradle) and anywhere else the +# binaries are needed but not in git. # -# Contract (see docs/adr — non-reproducible binaries → CI is the sole builder): -# * The Mach-O binary inside chat_mls_coreFFI.xcframework is gitignored (too -# large + non-reproducible). Headers/plist/modulemap + the .sha256 manifest -# + .artifact-key ARE committed. -# * The producer workflow (native-artifacts.yml) builds the xcframework, -# uploads it to R2 keyed by the source fingerprint, and commits the -# refreshed manifest + .artifact-key. That published asset is what this -# script fetches, so asset ↔ manifest always match. +# Contract (non-reproducible binaries → CI is the sole builder): +# * The compiled binaries are gitignored (large + non-reproducible). Their +# containing dir's headers/metadata + the .sha256 manifest ARE committed. +# iOS → chat_mls_coreFFI.xcframework/**/chat_mls_coreFFI (the Mach-O) +# Android → android/src/main/jniLibs/*/libchat_mls_core.so +# * The producer workflow (native-artifacts.yml) builds them, uploads to R2 +# keyed by the source fingerprint, and commits the refreshed manifest. So +# the published asset always matches the committed manifest verified here. # # Usage: scripts/fetch-native-artifacts.sh # # Env: -# R2_PUBLIC_BASE_URL (required) public-read base, e.g. -# https://native.example.com/mortstack — asset is -# fetched from "$R2_PUBLIC_BASE_URL/". +# R2_PUBLIC_BASE_URL (required on cache-miss) public-read base, e.g. +# https://pub-xxxx.r2.dev — asset fetched from +# "$R2_PUBLIC_BASE_URL/". # ALLOW_BUILD=1 (optional) on cache-miss, compile from source instead -# of failing. For local dev only — the EAS builder has no -# Rust toolchain, so it must hit the fast fetch path. +# of failing. Local dev only — the EAS builder has no +# Rust/NDK toolchain, so it must hit the fetch path. set -euo pipefail TARGET="${1:?usage: fetch-native-artifacts.sh }" @@ -30,22 +30,24 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" # ── Artifact registry ───────────────────────────────────────────────────────── -# Extend with new rows as android AAR / chat-crypto SignalFfi come online. +# Each row: a directory of gitignored binaries ($ART_DIR) under $DEST_DIR, +# verified against a committed $MANIFEST sitting next to it, published to R2 as +# -.tar.gz (tar rooted at $DEST_DIR so it unpacks +# straight back into place). case "$TARGET" in ios) PKG="packages/chat-mls-core" DEST_DIR="$PKG/ios" - XCF="chat_mls_coreFFI.xcframework" - MANIFEST="$XCF.sha256" # relative to $DEST_DIR + ART_DIR="chat_mls_coreFFI.xcframework" + MANIFEST="chat_mls_coreFFI.xcframework.sha256" ASSET_PREFIX="chat_mls_coreFFI-ios" - BUILD_SCRIPT_TARGET="ios" ;; android) - # TODO: android jniLibs (*.so) are gitignored too — same trap, not yet - # published to R2. Soft no-op so android EAS builds aren't blocked by this - # hook until the AAR pipeline is wired. Wire it here when ready. - echo "native(android): AAR pipeline not yet published via R2 — skipping" - exit 0 + PKG="packages/chat-mls-core" + DEST_DIR="$PKG/android/src/main" + ART_DIR="jniLibs" + MANIFEST="jniLibs.sha256" + ASSET_PREFIX="chat_mls_core-android" ;; *) echo "fetch-native-artifacts: unsupported target '$TARGET'" >&2 @@ -53,15 +55,15 @@ case "$TARGET" in ;; esac -# Verify the extracted tree against the committed manifest. hash_dir wrote paths -# relative to the xcframework dir, so we cd into it and point at ../. +# hash_dir wrote manifest paths relative to $ART_DIR, so verify from inside it +# against ../. cd-fail (dir absent) → non-zero → treated as "absent". verify() { - ( cd "$DEST_DIR/$XCF" && shasum -a 256 -c "../$MANIFEST" ) >/dev/null 2>&1 + ( cd "$DEST_DIR/$ART_DIR" 2>/dev/null && shasum -a 256 -c "../$MANIFEST" ) >/dev/null 2>&1 } -# Already present and valid (warm builder / re-run)? Nothing to do. -if [ -f "$DEST_DIR/$XCF/Info.plist" ] && verify; then - echo "native($TARGET): $XCF present + verified — skip" +# Already present + valid (warm builder / re-run)? Nothing to do. +if verify; then + echo "native($TARGET): $ART_DIR present + verified — skip" exit 0 fi @@ -83,14 +85,14 @@ if fetch && verify; then fi # Extracted but checksum failed = corruption/tamper or stale asset ≠ manifest. -if [ -f "$DEST_DIR/$XCF/Info.plist" ] && ! verify; then +if [ -d "$DEST_DIR/$ART_DIR" ] && ! verify; then echo "native($TARGET): CHECKSUM MISMATCH — $ASSET does not match committed $MANIFEST" >&2 fi # ── Fallback ────────────────────────────────────────────────────────────────── if [ "${ALLOW_BUILD:-0}" = "1" ]; then echo "native($TARGET): asset missing/invalid — building from source (ALLOW_BUILD=1)" - pnpm --filter @repo/chat-mls-core "build:${BUILD_SCRIPT_TARGET}" + pnpm --filter @repo/chat-mls-core "build:${TARGET}" verify && { echo "native($TARGET): built + verified"; exit 0; } echo "native($TARGET): local build produced a tree that fails $MANIFEST" >&2 exit 1 @@ -102,8 +104,8 @@ native($TARGET): artifact not available. key : $KEY (source fingerprint of $PKG) base : ${R2_PUBLIC_BASE_URL:-} -The prebuilt xcframework for this source revision has not been published. +The prebuilt native artifact for this source revision has not been published. → Run the 'Native artifacts' workflow (Actions ▸ workflow_dispatch), or - → re-run locally with ALLOW_BUILD=1 to compile from source (needs Rust + Xcode). + → re-run locally with ALLOW_BUILD=1 to compile from source (needs Rust; Android also NDK). EOF exit 1