From 621c8806377e39902930dc3c7294f5bd5978b749 Mon Sep 17 00:00:00 2001 From: sheetalarkadam <100380551+sheetalarkadam@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:56:39 -0700 Subject: [PATCH] ci(sdk_v2): add Android C++ core build pipeline Combines the Android native CI pipeline (#954) with the versioned GenAI Android AAR cache fix (#960) it depended on, into a single change for review. Pipeline (.pipelines/v2/templates): - Add Android build stages to the native pipeline, publishing cpp-native-android- artifacts (arm64-v8a, x86_64). - Build-only; emulator tests are opt-in via runEmulatorTests on the x86_64 leg. - Stage all four .so files (libfoundry_local, libonnxruntime, libonnxruntime-genai, libmat); a readelf DT_NEEDED-closure check fails the stage if a staged library needs a build-produced .so that wasn't staged. GenAI AAR cache (sdk_v2/cpp): - Scope both the archive and the extracted tree by version so a version bump fetches its own copy and cannot collide with a stale cache. - Newer GenAI versions target libmat.so. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a5c2fc55-3b22-46d7-a22c-63beaa5720ae --- .../v2/templates/stages-build-native.yml | 61 +++++ .../v2/templates/steps-build-android.yml | 214 ++++++++++++++++++ sdk_v2/cpp/CMakeLists.txt | 10 + sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake | 19 +- 4 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 .pipelines/v2/templates/steps-build-android.yml diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index 5fb3a2bc3..584391702 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -193,6 +193,67 @@ stages: genaiVersion: ${{ parameters.genaiVersion }} runTests: true + # ==================================================================== + # Android arm64-v8a — cross-compiled on a Linux host (build only) + # ==================================================================== + - stage: cpp_build_android_arm64_v8a + displayName: 'C++ Native: Android arm64-v8a' + dependsOn: + - compute_version + jobs: + - job: build + pool: + name: onnxruntime-Ubuntu2404-AMD-CPU + os: linux + templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' + outputs: + - output: pipelineArtifact + artifactName: 'cpp-native-android-arm64-v8a' + targetPath: '$(Build.ArtifactStagingDirectory)/native' + steps: + - template: steps-build-android.yml + parameters: + abi: arm64-v8a + buildConfig: ${{ parameters.buildConfig }} + ortVersion: ${{ parameters.ortVersion }} + genaiVersion: ${{ parameters.genaiVersion }} + + # ==================================================================== + # Android x86_64 — cross-compiled on a Linux host (build only) + # + # This is the emulator ABI, so it is the leg that can eventually run tests + # (see steps-build-android.yml's runEmulatorTests). Kept build-only for now. + # ==================================================================== + - stage: cpp_build_android_x86_64 + displayName: 'C++ Native: Android x86_64' + dependsOn: + - compute_version + jobs: + - job: build + pool: + name: onnxruntime-Ubuntu2404-AMD-CPU + os: linux + templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' + outputs: + - output: pipelineArtifact + artifactName: 'cpp-native-android-x86_64' + targetPath: '$(Build.ArtifactStagingDirectory)/native' + steps: + - template: steps-build-android.yml + parameters: + abi: x86_64 + buildConfig: ${{ parameters.buildConfig }} + ortVersion: ${{ parameters.ortVersion }} + genaiVersion: ${{ parameters.genaiVersion }} + # ==================================================================== # Pack — C++ SDK tgz bundles (base platforms) # ==================================================================== diff --git a/.pipelines/v2/templates/steps-build-android.yml b/.pipelines/v2/templates/steps-build-android.yml new file mode 100644 index 000000000..90da4b97b --- /dev/null +++ b/.pipelines/v2/templates/steps-build-android.yml @@ -0,0 +1,214 @@ +parameters: +- name: abi + type: string + values: ['arm64-v8a', 'x86_64'] +- name: buildConfig + type: string +- name: ortVersion + type: string +- name: genaiVersion + type: string +# Minimum supported API level. Keep in sync with build.py's --android_api default. +- name: androidApi + type: number + default: 28 +- name: ndkVersion + type: string + default: '29.0.14206865' +- name: ndkArchive + type: string + default: 'android-ndk-r29-linux.zip' +- name: ndkSha1 + type: string + default: '87e2bb7e9be5d6a1c6cdf5ec40dd4e0c6d07c30b' +- name: runEmulatorTests + type: boolean + default: false + +steps: + +- bash: | + set -euo pipefail + git clone https://github.com/microsoft/vcpkg.git "$(Build.BinariesDirectory)/vcpkg" + "$(Build.BinariesDirectory)/vcpkg/bootstrap-vcpkg.sh" -disableMetrics + displayName: 'Bootstrap vcpkg' + +- bash: | + set -euo pipefail + pinned='${{ parameters.ndkVersion }}' + archive='${{ parameters.ndkArchive }}' + expected_sha1='${{ parameters.ndkSha1 }}' + + # Look for the pinned NDK where agent images conventionally place it. + ndk='' + for candidate in \ + "${ANDROID_NDK_HOME:-}" \ + "${ANDROID_NDK_ROOT:-}" \ + "${ANDROID_SDK_ROOT:-}/ndk/$pinned" \ + "${ANDROID_HOME:-}/ndk/$pinned"; do + [ -n "$candidate" ] || continue + if [ -r "$candidate/source.properties" ] && + grep -qx "Pkg.Revision = $pinned" "$candidate/source.properties"; then + ndk="$candidate" + echo "Found pinned NDK on the image: $ndk" + break + fi + done + + if [ -z "$ndk" ]; then + echo "NDK $pinned not on this image; downloading the pinned archive." + + # unzip, not python -m zipfile: zipfile drops the executable bit, which + # would leave every toolchain binary in the NDK unusable. + command -v unzip >/dev/null || { echo "ERROR: unzip is required but not installed." >&2; exit 1; } + + workdir="$(Agent.TempDirectory)/ndk-download" + mkdir -p "$workdir" + curl -fsSL --retry 3 --retry-delay 5 \ + -o "$workdir/$archive" \ + "https://dl.google.com/android/repository/$archive" + + actual_sha1=$(sha1sum "$workdir/$archive" | cut -d' ' -f1) + if [ "$actual_sha1" != "$expected_sha1" ]; then + echo "ERROR: checksum mismatch for $archive" >&2 + echo " expected $expected_sha1" >&2 + echo " actual $actual_sha1" >&2 + exit 1 + fi + echo "Checksum verified: $actual_sha1" + + unzip -q "$workdir/$archive" -d "$workdir/extracted" + # The archive expands to a single release-named directory (android-ndk-r29). + extracted=$(find "$workdir/extracted" -maxdepth 1 -mindepth 1 -type d) + [ "$(echo "$extracted" | wc -l)" -eq 1 ] || { + echo "ERROR: expected exactly one top-level directory in $archive" >&2; exit 1; } + ndk="$extracted" + rm -f "$workdir/$archive" + fi + + # Assert we got what we pinned. This is what catches a partially-updated + # parameter triple: bump ndkVersion without ndkArchive/ndkSha1 and the + # checksum still matches, but the revision here will not. + revision=$(sed -n 's/^Pkg.Revision = //p' "$ndk/source.properties") + if [ "$revision" != "$pinned" ]; then + echo "ERROR: NDK at $ndk reports revision '$revision', expected '$pinned'." >&2 + echo " ndkVersion, ndkArchive and ndkSha1 must be bumped together." >&2 + exit 1 + fi + + echo "ANDROID_NDK_HOME = $ndk (revision $revision)" + echo "##vso[task.setvariable variable=androidNdkHome]$ndk" + displayName: 'Resolve Android NDK (${{ parameters.ndkVersion }})' + +- template: steps-prefetch-nuget.yml + parameters: + ortVersion: ${{ parameters.ortVersion }} + genaiVersion: ${{ parameters.genaiVersion }} + winmlVersion: '' + includeWinml: false + shell: bash + +# Bake the pipeline-computed version into the binary so +# FoundryLocalGetVersionString() matches the package version rather than the +# cmake default. Mirrors the desktop legs. +- bash: | + set -euo pipefail + version=$(cat "$(Pipeline.Workspace)/version-info/sdkVersion.txt" | tr -d '[:space:]') + defines="$(cmakeFetchDefines) \"FOUNDRY_LOCAL_VERSION_STRING=$version\"" + echo "##vso[task.setvariable variable=cmakeFetchDefines]$defines" + echo "cmakeFetchDefines = $defines" + displayName: 'Append version define' + +- bash: | + set -euo pipefail + python3 build.py --configure --build \ + --android \ + --android_abi ${{ parameters.abi }} \ + --android_api ${{ parameters.androidApi }} \ + --config ${{ parameters.buildConfig }} \ + --cmake_extra_defines $(cmakeFetchDefines) + displayName: 'Configure and build (${{ parameters.abi }})' + workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp + env: + VCPKG_ROOT: $(Build.BinariesDirectory)/vcpkg + ANDROID_NDK_HOME: $(androidNdkHome) + +- ${{ if eq(parameters.runEmulatorTests, true) }}: + # Only the emulator needs a full SDK (avdmanager/emulator/adb), so this is + # resolved here rather than as a precondition of the whole stage. + - bash: | + set -euo pipefail + sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" + if [ -z "$sdk" ] || [ ! -d "$sdk" ]; then + echo "ERROR: emulator tests need an Android SDK, but neither ANDROID_SDK_ROOT" >&2 + echo " nor ANDROID_HOME points at one on this agent." >&2 + exit 1 + fi + echo "ANDROID_HOME = $sdk" + echo "##vso[task.setvariable variable=androidSdkRoot]$sdk" + displayName: 'Resolve Android SDK (emulator)' + + - bash: | + set -euo pipefail + python3 build.py --test \ + --android \ + --android_abi ${{ parameters.abi }} \ + --android_api ${{ parameters.androidApi }} \ + --android_run_emulator \ + --config ${{ parameters.buildConfig }} + displayName: 'Run tests on emulator (${{ parameters.abi }})' + workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp + env: + VCPKG_ROOT: $(Build.BinariesDirectory)/vcpkg + ANDROID_HOME: $(androidSdkRoot) + ANDROID_NDK_HOME: $(androidNdkHome) + +- bash: | + echo "=== vcpkg buildtrees error logs ===" + find "$(Build.BinariesDirectory)/vcpkg/buildtrees" -name "*err.log" -exec echo "--- {} ---" \; -exec cat {} \; || true + echo "=== vcpkg buildtrees config output logs ===" + find "$(Build.BinariesDirectory)/vcpkg/buildtrees" -name "config-*-out.log" -exec echo "--- {} ---" \; -exec tail -100 {} \; || true + displayName: 'Dump vcpkg error logs' + condition: failed() + +- bash: | + set -euo pipefail + src='$(Build.SourcesDirectory)/sdk_v2/cpp/build/Android-${{ parameters.abi }}/${{ parameters.buildConfig }}/bin' + dst='$(Build.ArtifactStagingDirectory)/native' + mkdir -p "$dst" + + missing=0 + for lib in libfoundry_local.so libonnxruntime.so libonnxruntime-genai.so; do + if [ -f "$src/$lib" ]; then + cp -P "$src/$lib" "$dst/" + echo " staged $lib" + else + echo "ERROR: $lib not found at $src/$lib" >&2 + missing=1 + fi + done + [ "$missing" -eq 0 ] || exit 1 + + if [ -f "$src/libmat.so" ]; then + cp -P "$src/libmat.so" "$dst/" + echo " staged libmat.so" + fi + + # Fail loudly if a staged library has a DT_NEEDED that the build produced + # but we did not stage. + readelf=$(command -v readelf || true) + if [ -n "$readelf" ]; then + unmet=0 + for so in "$dst"/*.so; do + for need in $("$readelf" -d "$so" | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p'); do + if [ -f "$src/$need" ] && [ ! -f "$dst/$need" ]; then + echo "ERROR: $(basename "$so") needs $need, which the build produced but this step did not stage" >&2 + unmet=1 + fi + done + done + [ "$unmet" -eq 0 ] || exit 1 + else + echo "NOTE: readelf unavailable; skipped DT_NEEDED closure check." + fi + displayName: 'Stage native artifacts' diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index 1a070e397..bbe52ed27 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -540,6 +540,16 @@ if(TARGET OnnxRuntime::OnnxRuntime) $ ) + # GenAI 0.15.0 split most of its implementation out of + # libonnxruntime-genai.so into libmat.so + if(EXISTS "${ORT_GENAI_LIB_DIR}/libmat.so") + add_custom_command(TARGET foundry_local POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${ORT_GENAI_LIB_DIR}/libmat.so" + $ + ) + endif() + if(EXISTS "${ORT_LIB_DIR}/libonnxruntime_providers_shared.so") add_custom_command(TARGET foundry_local POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake b/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake index 5e46ac817..fe6806099 100644 --- a/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake +++ b/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake @@ -123,19 +123,28 @@ endif() if(ANDROID) # GenAI publishes a standalone AAR on GitHub Releases that contains # both C/C++ headers and native .so files — no NuGet package needed. + # + # Scope the archive and extracted tree by version. Without a version in the path, a warm build + # directory would keep reusing the previously downloaded AAR after a version bump, so pin the + # cache path to ${ORT_GENAI_VERSION}. set(_GENAI_AAR_URL "https://github.com/microsoft/onnxruntime-genai/releases/download/v${ORT_GENAI_VERSION}/onnxruntime-genai-android-${ORT_GENAI_VERSION}.aar") - set(_GENAI_AAR_DIR "${CMAKE_BINARY_DIR}/_deps/genai-android-aar") - set(_GENAI_AAR_FILE "${_GENAI_AAR_DIR}/onnxruntime-genai-android.aar") + set(_GENAI_AAR_DIR "${CMAKE_BINARY_DIR}/_deps/genai-android-aar/${ORT_GENAI_VERSION}") + set(_GENAI_AAR_FILE "${_GENAI_AAR_DIR}/onnxruntime-genai-android-${ORT_GENAI_VERSION}.aar") if(NOT EXISTS "${_GENAI_AAR_FILE}") message(STATUS "Downloading ORT GenAI Android AAR v${ORT_GENAI_VERSION} from GitHub Releases") - file(DOWNLOAD "${_GENAI_AAR_URL}" "${_GENAI_AAR_FILE}" + set(_GENAI_AAR_TMP "${_GENAI_AAR_FILE}.tmp") + file(DOWNLOAD "${_GENAI_AAR_URL}" "${_GENAI_AAR_TMP}" STATUS _GENAI_DL_STATUS) list(GET _GENAI_DL_STATUS 0 _GENAI_DL_CODE) if(NOT _GENAI_DL_CODE EQUAL 0) list(GET _GENAI_DL_STATUS 1 _GENAI_DL_MSG) - message(FATAL_ERROR "Failed to download GenAI AAR: ${_GENAI_DL_MSG}") + file(REMOVE "${_GENAI_AAR_TMP}") + message(FATAL_ERROR "Failed to download GenAI Android AAR v${ORT_GENAI_VERSION} from " + "${_GENAI_AAR_URL}: ${_GENAI_DL_MSG}. Verify that this version has " + "an Android AAR attached to its GitHub release.") endif() + file(RENAME "${_GENAI_AAR_TMP}" "${_GENAI_AAR_FILE}") endif() if(NOT EXISTS "${_GENAI_AAR_DIR}/jni") @@ -146,7 +155,7 @@ if(ANDROID) set(_GENAI_HEADER_DIR "${_GENAI_AAR_DIR}/headers") set(_GENAI_LIB_DIR "${_GENAI_AAR_DIR}/jni/${ANDROID_ABI}") - message(STATUS "OnnxRuntimeGenAI via GitHub AAR: ${_GENAI_AAR_DIR}") + message(STATUS "OnnxRuntimeGenAI via GitHub AAR v${ORT_GENAI_VERSION}: ${_GENAI_AAR_DIR}") else() # Allow the pipeline or caller to override the download URL (e.g., to use a local # file:// path when direct nuget.org access is blocked in CI).