kotlin(d2+d3): move android natives to AAR module + wire android-smoke #34
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
| # Release pipeline. Fires on `v*` tag push, after the release PR has merged. | ||
|
Check failure on line 1 in .github/workflows/release.yml
|
||
| # | ||
| # The Swift xcframework zip is built upstream by release_swift.yml on the | ||
| # `release/v*` PR branch and uploaded to a draft GitHub release. This | ||
| # workflow's create-release job promotes that draft to published; the other | ||
| # jobs then `gh release upload` the rest of the assets to the same release. | ||
| # Single Swift build (in PR CI), no cross-host determinism requirement. | ||
| name: release | ||
| on: | ||
| push: | ||
| tags: | ||
| - "v*" | ||
| env: | ||
| LIB_NAME: libiroh_ffi | ||
| PACKAGE_NAME: libiroh | ||
| IROH_FORCE_STAGING_RELAYS: "1" | ||
| # `gh release upload` + `gh release edit --draft=false` both need write | ||
| # access to the release. | ||
| permissions: | ||
| contents: write | ||
| # Tag-push publish: PR-CI (release_swift.yml) created a draft release with | ||
| # the Swift xcframework zip already attached and the SHA already baked into | ||
| # Package.swift on main. Tag time, this job just promotes the draft to | ||
| # published (or creates fresh if no draft, e.g. a manual tag without a PR). | ||
| # Other jobs then `gh release upload` the rest of the assets. | ||
| create-release: | ||
| name: create-release | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| outputs: | ||
| release_version: ${{ env.RELEASE_VERSION }} | ||
| steps: | ||
| - name: Get the release version from the tag | ||
| shell: bash | ||
| if: env.RELEASE_VERSION == '' | ||
| run: | | ||
| echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV | ||
| echo "version is: ${GITHUB_REF#refs/tags/}" | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: Promote draft release (or create fresh) | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| TAG: ${{ env.RELEASE_VERSION }} | ||
| shell: bash | ||
| run: | | ||
| set -eu | ||
| if gh release view "$TAG" --json isDraft -q .isDraft 2>/dev/null | grep -q true; then | ||
| echo "Promoting existing draft $TAG to published" | ||
| gh release edit "$TAG" --draft=false | ||
| elif ! gh release view "$TAG" >/dev/null 2>&1; then | ||
| echo "Creating fresh published release $TAG (no draft from release_swift.yml)" | ||
| gh release create "$TAG" --title "$TAG" --notes "" | ||
| else | ||
| echo "Release $TAG already published" | ||
| fi | ||
| build-and-publish-libs: | ||
| name: Build & publish release libraries | ||
| needs: create-release | ||
| runs-on: ${{ matrix.runner }} | ||
| strategy: | ||
| matrix: | ||
| name: [ubuntu-latest, ubuntu-arm-latest, macOS-arm-latest, windows-latest] | ||
| rust: [stable] | ||
| include: | ||
| - name: ubuntu-arm-latest | ||
| os: ubuntu-latest | ||
| cargo_target: "aarch64-unknown-linux-musl" | ||
| target: linux-aarch64 | ||
| runner: [self-hosted, linux, ARM64] | ||
| - name: ubuntu-latest | ||
| os: ubuntu-latest | ||
| cargo_target: "x86_64-unknown-linux-musl" | ||
| target: linux-x86_64 | ||
| runner: [self-hosted, linux, X64] | ||
| - name: macOS-arm-latest | ||
| os: macOS-latest | ||
| cargo_target: "aarch64-apple-darwin" | ||
| target: darwin-aarch64 | ||
| runner: [self-hosted, macOS, ARM64] | ||
| # TODO: windows runner is not available on the org level | ||
| - name: windows-latest | ||
| os: windows-latest | ||
| cargo_target: "x86_64-pc-windows-msvc" | ||
| target: windows-x86_64 | ||
| runner: [windows-latest] | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| toolchain: ${{ matrix.rust }} | ||
| targets: ${{matrix.cargo_target}} | ||
| - name: Ensure musl support | ||
| if: ${{ contains(matrix.cargo_target, '-musl') }} | ||
| run: sudo apt-get install musl-tools -y | ||
| - name: Build release binary | ||
| shell: bash | ||
| run: | | ||
| if [ "${{ matrix.name }}" = "ubuntu-arm-latest" ]; then | ||
| export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc | ||
| export CC=aarch64-linux-gnu-gcc | ||
| fi | ||
| cargo build --verbose --release --target ${{matrix.cargo_target}} | ||
| - name: Build archive | ||
| shell: bash | ||
| run: | | ||
| outdir="./target/${{matrix.cargo_target}}/release" | ||
| staging="${{ env.PACKAGE_NAME }}-${{ matrix.target }}" | ||
| mkdir -p "$staging" | ||
| if [ "${{ matrix.os }}" = "windows-latest" ]; then | ||
| cp "target/${{matrix.cargo_target}}/release/iroh_ffi.lib" "$staging/iroh.lib" | ||
| cd "$staging" | ||
| 7z a "../$staging.zip" . | ||
| echo "ASSET=$staging.zip" >> $GITHUB_ENV | ||
| else | ||
| cp "target/${{matrix.cargo_target}}/release/${{ env.LIB_NAME }}.a" "$staging/${{ env.PACKAGE_NAME }}.a" | ||
| tar czf "$staging.tar.gz" -C "$staging" . | ||
| echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV | ||
| fi | ||
| - name: Upload release archive | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| TAG: ${{ needs.create-release.outputs.release_version }} | ||
| shell: bash | ||
| run: gh release upload "$TAG" "$ASSET" --clobber | ||
| # build-and-publish-swift was here. The Swift xcframework now ships from | ||
| # release_swift.yml on the release/v* PR branch (it's already attached to | ||
| # the draft release that create-release promotes), so there's nothing to do | ||
| # here on tag. | ||
| # Cross-build the desktop cdylibs for each JVM host the Maven artifact | ||
| # should support. JNI libs are loaded by JNA from classpath at | ||
| # `<jna-platform>/libname.{so,dylib,dll}`; see the staging step below. | ||
| # Android ABIs are built separately by build-kotlin-android and shipped | ||
| # in the same jar under `lib/<abi>/`, where AGP merges them into APKs. | ||
| build-kotlin-cdylib: | ||
| name: Build Kotlin cdylib (${{ matrix.name }}) | ||
| needs: create-release | ||
| timeout-minutes: 60 | ||
| runs-on: ${{ matrix.runner }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - name: linux-x86_64 | ||
| runner: [self-hosted, linux, X64] | ||
| target: x86_64-unknown-linux-gnu | ||
| lib_file: libiroh_ffi.so | ||
| - name: linux-aarch64 | ||
| runner: [self-hosted, linux, ARM64] | ||
| target: aarch64-unknown-linux-gnu | ||
| lib_file: libiroh_ffi.so | ||
| - name: macos-aarch64 | ||
| runner: [self-hosted, macOS, ARM64] | ||
| target: aarch64-apple-darwin | ||
| lib_file: libiroh_ffi.dylib | ||
| - name: windows-x86_64 | ||
| runner: [windows-latest] | ||
| target: x86_64-pc-windows-msvc | ||
| lib_file: iroh_ffi.dll | ||
| steps: | ||
| - uses: actions/checkout@master | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: ${{ matrix.target }} | ||
| - uses: Swatinem/rust-cache@v2 | ||
| - run: cargo build --release --lib -p iroh-ffi --target ${{ matrix.target }} | ||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: kotlin-cdylib-${{ matrix.name }} | ||
| path: target/${{ matrix.target }}/release/${{ matrix.lib_file }} | ||
| if-no-files-found: error | ||
| # Cross-build the native lib for every Android ABI with the NDK. These | ||
| # ship in the same computer.iroh:iroh jar under `lib/<abi>/`; AGP merges | ||
| # `lib/<abi>/*.so` from JAR dependencies into the consumer APK, where | ||
| # IrohAndroid loads it via System.loadLibrary("iroh_ffi"). cargo-ndk | ||
| # writes one `<abi>/libiroh_ffi.so` per target under the -o directory. | ||
| build-kotlin-android: | ||
| name: Build Kotlin Android cdylibs | ||
| needs: create-release | ||
| timeout-minutes: 60 | ||
| runs-on: [self-hosted, linux, X64] | ||
| steps: | ||
| - uses: actions/checkout@master | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android | ||
| - uses: Swatinem/rust-cache@v2 | ||
| - uses: android-actions/setup-android@v3 | ||
| - name: Setup Android NDK | ||
| uses: arqu/setup-ndk@main | ||
| id: setup-ndk | ||
| with: | ||
| ndk-version: r23 | ||
| add-to-path: true | ||
| - name: Cross-build Android cdylibs | ||
| env: | ||
| ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} | ||
| run: | | ||
| command -v cargo-ndk >/dev/null 2>&1 || cargo install --version 3.5.4 cargo-ndk --locked | ||
| cargo ndk -o ./android-jniLibs \ | ||
| -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 \ | ||
| build --release --lib -p iroh-ffi | ||
| find ./android-jniLibs -type f | ||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: kotlin-android-jniLibs | ||
| path: android-jniLibs/ | ||
| if-no-files-found: error | ||
| # Pre-publish Android consumer smoke. Pulls the just-built android natives | ||
| # from build-kotlin-android, stages them under lib/<abi>/, assembles a tiny | ||
| # consumer app that depends on computer.iroh:iroh, and runs an instrumented | ||
| # JUnit test inside an Android emulator. Catches the runtime class of bugs | ||
| # that layout checks can't: native lib loads but JNI symbol missing, AGP | ||
| # fails to merge from JAR, NDK API level too high for emulator, etc. | ||
| verify-kotlin-android-consumer: | ||
| name: Smoke Kotlin Android consumer (emulator) | ||
| needs: [create-release, build-kotlin-android] | ||
| timeout-minutes: 45 | ||
| # ubuntu-latest has /dev/kvm via nested-virt — required by the emulator | ||
| # action. The self-hosted linux runner used by build-kotlin-android above | ||
| # doesn't have KVM enabled. | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@master | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| - uses: Swatinem/rust-cache@v2 | ||
| - uses: davidB/rust-cargo-make@v1 | ||
| - uses: actions/setup-java@v4 | ||
| with: | ||
| distribution: temurin | ||
| java-version: "17" | ||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| name: kotlin-android-jniLibs | ||
| path: android-jniLibs/ | ||
| - name: Stage Android natives + generate Kotlin bindings | ||
| run: | | ||
| set -eu | ||
| # AAR source-set path — AGP packages files here into the AAR's | ||
| # jni/<abi>/, which consumers' APKs then merge. | ||
| JNI=kotlin/android/src/main/jniLibs | ||
| for abi in armeabi-v7a arm64-v8a x86 x86_64; do | ||
| install -D -m644 "android-jniLibs/$abi/libiroh_ffi.so" "$JNI/$abi/libiroh_ffi.so" | ||
| done | ||
| # Composite-build :android:assemble needs iroh_ffi.kt for :lib. | ||
| cargo make bindgen-kotlin | ||
| - name: Enable KVM | ||
| # reactivecircus/android-emulator-runner needs HW accel on | ||
| # ubuntu-latest's nested-virt KVM. | ||
| run: | | ||
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | ||
| sudo udevadm control --reload-rules | ||
| sudo udevadm trigger --name-match=kvm | ||
| - name: Run Android consumer smoke on emulator | ||
| # API 35 / x86_64 / google_apis mirrors n0-computer/iroh's android job. | ||
| # `script:` runs each line as a separate `sh -c`, so state across | ||
| # lines is lost. Delegate to scripts/run_android_smoke.sh so cd + | ||
| # variable assignment + gradlew live in one shell. | ||
| uses: reactivecircus/android-emulator-runner@v2 | ||
| with: | ||
| api-level: 35 | ||
| arch: x86_64 | ||
| target: google_apis | ||
| force-avd-creation: false | ||
| emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -no-metrics | ||
| disable-animations: true | ||
| script: bash scripts/run_android_smoke.sh | ||
| build-and-publish-kotlin: | ||
| name: Publish Kotlin to Maven Central | ||
| timeout-minutes: 30 | ||
| needs: [create-release, build-kotlin-cdylib, build-kotlin-android, verify-kotlin-android-consumer] | ||
| runs-on: [self-hosted, linux, X64] | ||
| steps: | ||
| - uses: actions/checkout@master | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| - uses: Swatinem/rust-cache@v2 | ||
| - uses: davidB/rust-cargo-make@v1 | ||
| - uses: actions/setup-java@v4 | ||
| with: | ||
| distribution: temurin | ||
| java-version: "17" | ||
| # AGP requires the Android SDK at gradle-configure time; the :android | ||
| # subproject in kotlin/ root configures even when only :lib is being | ||
| # built. Same reason setup-android shows up in ci_kotlin.yml jobs. | ||
| - uses: android-actions/setup-android@v3 | ||
| # Generate `iroh_ffi.kt` from the local debug cdylib. We discard the | ||
| # flat resources/libiroh_ffi.so it also copies, because we're shipping | ||
| # the per-platform release builds under jna-platform/ dirs instead. | ||
| - name: Generate Kotlin bindings | ||
| run: cargo make bindgen-kotlin | ||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| pattern: kotlin-cdylib-* | ||
| path: cdylibs/ | ||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| name: kotlin-android-jniLibs | ||
| path: android-jniLibs/ | ||
| - name: Stage natives into both module trees | ||
| shell: bash | ||
| run: | | ||
| set -eu | ||
| # :lib (JVM JAR) — desktop cdylibs at JNA-platform paths. | ||
| RES=kotlin/lib/src/main/resources | ||
| rm -f "$RES"/libiroh_ffi.so "$RES"/libiroh_ffi.dylib "$RES"/iroh_ffi.dll | ||
| install -D -m644 cdylibs/kotlin-cdylib-linux-x86_64/libiroh_ffi.so "$RES/linux-x86-64/libiroh_ffi.so" | ||
| install -D -m644 cdylibs/kotlin-cdylib-linux-aarch64/libiroh_ffi.so "$RES/linux-aarch64/libiroh_ffi.so" | ||
| install -D -m644 cdylibs/kotlin-cdylib-macos-aarch64/libiroh_ffi.dylib "$RES/darwin-aarch64/libiroh_ffi.dylib" | ||
| install -D -m644 cdylibs/kotlin-cdylib-windows-x86_64/iroh_ffi.dll "$RES/win32-x86-64/iroh_ffi.dll" | ||
| # :android (AAR) — per-ABI .so under jniLibs/, AGP packages these | ||
| # into the AAR's jni/<abi>/ which consumer APKs then merge. | ||
| JNI=kotlin/android/src/main/jniLibs | ||
| for abi in armeabi-v7a arm64-v8a x86 x86_64; do | ||
| install -D -m644 "android-jniLibs/$abi/libiroh_ffi.so" "$JNI/$abi/libiroh_ffi.so" | ||
| done | ||
| find "$RES" "$JNI" -type f | ||
| - name: Verify Kotlin artifact contents | ||
| # Gate publish on every required native lib being present at the | ||
| # right path with the right architecture (catches iroh-ffi#246-class | ||
| # bugs — artifact uploaded missing a platform slice, app crashes at | ||
| # runtime with UnsatisfiedLinkError). | ||
| run: cargo make verify-kotlin-artifact | ||
| - name: Publish to Maven Central | ||
| working-directory: kotlin | ||
| env: | ||
| ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} | ||
| ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} | ||
| ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} | ||
| # Both modules in one Vanniktech invocation — :lib publishes | ||
| # computer.iroh:iroh (JAR), :android publishes computer.iroh:iroh- | ||
| # android (AAR). Each subproject's mavenPublishing block configures | ||
| # its own coordinates + publication shape. | ||
| run: ./gradlew publishAndReleaseToMavenCentral --no-daemon --console=plain | ||