diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index 2a7b465c0..0d10fd8fa 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -192,6 +192,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'