Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .pipelines/v2/templates/stages-build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# ====================================================================
Expand Down
214 changes: 214 additions & 0 deletions .pipelines/v2/templates/steps-build-android.yml
Original file line number Diff line number Diff line change
@@ -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'