From 61baa03fc6655d1cf52a7213d507af90ddb3fb3a Mon Sep 17 00:00:00 2001 From: sheetalarkadam <100380551+sheetalarkadam@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:10:30 -0700 Subject: [PATCH 1/5] ci(sdk_v2): build the C++ core for Android in the native pipeline Adds two cross-compiled Android stages (arm64-v8a, x86_64) to the sdk_v2 native build graph, running on the existing Linux pool via the NDK. Android resolves its native dependencies from different origins than the desktop platforms: ORT comes from the same Microsoft.ML.OnnxRuntime NuGet package, but GenAI ships as a standalone AAR on GitHub Releases. A version that exists on NuGet therefore does not necessarily exist for Android, and today nothing in CI notices - every desktop leg goes green while the Android build cannot configure at all. The stages publish `cpp-native-android-` artifacts but are deliberately not wired into either pack stage; neither the NuGet package nor the C++ SDK tgz has an Android consumer yet, so packaging is left as a separate question. Build-only for now. Emulator tests are plumbed behind a `runEmulatorTests` parameter on the x86_64 leg and can be enabled once the leg is proven stable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 24ef037b-246f-4fea-a1a3-4fae5223cb0c --- .../v2/templates/stages-build-native.yml | 69 +++++++ .../v2/templates/steps-build-android.yml | 170 ++++++++++++++++++ 2 files changed, 239 insertions(+) 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 2a7b465c0..2a47c549b 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -8,6 +8,14 @@ # pack_nuget – Microsoft.AI.Foundry.Local.Runtime # (win-x64, win-arm64, linux-x64, linux-arm64, osx-arm64; # Windows ships WinML; ARM64 Linux is CPU-only) +# +# Android (arm64-v8a, x86_64) is built and published as an artifact but is +# deliberately *not* fed into either pack stage: neither the NuGet package nor +# the C++ SDK tgz has an Android consumer today, and Android's dependency +# origins differ enough (GenAI ships as a GitHub Releases AAR rather than a +# NuGet package) that packaging is a separate design question. The stages exist +# so that a dependency bump which is valid on desktop but unavailable for +# Android fails here rather than silently. parameters: - name: buildConfig @@ -192,6 +200,67 @@ stages: genaiVersion: ${{ parameters.genaiVersion }} runTests: true + # ==================================================================== + # Android arm64-v8a — cross-compiled on a Linux host (build only) + # ==================================================================== + - stage: cpp_build_android_arm64 + 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..6164364e4 --- /dev/null +++ b/.pipelines/v2/templates/steps-build-android.yml @@ -0,0 +1,170 @@ +# Reusable Android cross-compile steps for the Foundry Local C++ SDK. +# +# Runs on a Linux host and cross-compiles with the Android NDK, so it uses the +# same pool as the linux-x64 stage. Supports arm64-v8a (devices) and x86_64 +# (emulator) via the `abi` parameter. +# +# Android resolves its native dependencies differently from the desktop +# platforms, which is the main reason this stage earns its keep: +# +# ORT – Microsoft.ML.OnnxRuntime NuGet, runtimes/android-{arm64,x64} +# (same package as desktop, so steps-prefetch-nuget.yml applies) +# GenAI – a standalone AAR on *GitHub Releases*, not NuGet +# (see cmake/FindOnnxRuntimeGenAI.cmake) +# +# Because GenAI comes from a different origin, a version that exists on NuGet +# does not necessarily exist as an Android AAR. Without this stage that skew is +# invisible: every desktop leg goes green while Android cannot configure at all. + +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 +# Pinned so a pool image refresh cannot silently change the toolchain. +- name: ndkVersion + type: string + default: '29.0.14206865' +# Emulator tests require the x86_64 ABI. Off by default: this stage exists to +# guard the build and the dependency wiring, and an emulator boot adds both +# runtime and flakiness. Enable per-stage once the leg is proven stable. +- 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' + +# Resolve the NDK from the pool image when possible, and only fall back to +# sdkmanager if the pinned version is genuinely absent. Both paths converge on +# the same pinned version so the toolchain is identical either way. +- bash: | + set -euo pipefail + pinned='${{ parameters.ndkVersion }}' + sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" + + if [ -z "$sdk" ]; then + echo "ERROR: neither ANDROID_SDK_ROOT nor ANDROID_HOME is set on this agent." >&2 + exit 1 + fi + + ndk="$sdk/ndk/$pinned" + if [ ! -d "$ndk" ]; then + echo "NDK $pinned not present; installing via sdkmanager." + sdkmanager="$sdk/cmdline-tools/latest/bin/sdkmanager" + if [ ! -x "$sdkmanager" ]; then + echo "ERROR: sdkmanager not found at $sdkmanager" >&2 + exit 1 + fi + yes | "$sdkmanager" --install "ndk;$pinned" > /dev/null + fi + + if [ ! -d "$ndk" ]; then + echo "ERROR: NDK $pinned still not found at $ndk" >&2 + exit 1 + fi + + echo "ANDROID_HOME = $sdk" + echo "ANDROID_NDK_HOME = $ndk" + echo "##vso[task.setvariable variable=androidSdkRoot]$sdk" + echo "##vso[task.setvariable variable=androidNdkHome]$ndk" + displayName: 'Resolve Android NDK (${{ parameters.ndkVersion }})' + +# Prefetches ORT (used by the Android build) and GenAI (unused here — Android +# takes the GitHub AAR instead — but harmless, and it keeps the pinned-version +# drift check against deps_versions.json running on this leg too). +- 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_HOME: $(androidSdkRoot) + ANDROID_NDK_HOME: $(androidNdkHome) + +- ${{ if eq(parameters.runEmulatorTests, true) }}: + - 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() + +# Unlike the desktop legs, Android stages all three .so files. Desktop +# consumers get ORT/GenAI out of band (pip on the Python side, NuGet on the C# +# side), but an Android consumer has no such channel — the libraries have to be +# packaged into the APK/AAR alongside libfoundry_local.so, so the build that +# produced them is the only place they can be captured consistently. +- 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 + displayName: 'Stage native artifacts' From 474b745d7a9543f70e677f9696c8e696828b0797 Mon Sep 17 00:00:00 2001 From: sheetalarkadam <100380551+sheetalarkadam@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:11:51 -0700 Subject: [PATCH 2/5] ci(sdk_v2): stage libmat.so for Android and verify DT_NEEDED closure The Android staging step copied three .so files and its comment claimed that was all of them. GenAI 0.15.0 split most of libonnxruntime-genai.so into libmat.so and records it as a DT_NEEDED, so the published artifact was missing a library its own contents require -- a consuming APK would build green and then fail to dlopen at runtime. Stage libmat.so, guarded on existence to mirror the copy in sdk_v2/cpp/CMakeLists.txt so 0.14.x keeps working unchanged. To stop this class of bug returning, assert DT_NEEDED closure over the staged set. The check keys off "the build produced this library in bin/", so system libraries are excluded without a whitelist and it cannot fail spuriously on an agent whose image differs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 24ef037b-246f-4fea-a1a3-4fae5223cb0c --- .../v2/templates/steps-build-android.yml | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/.pipelines/v2/templates/steps-build-android.yml b/.pipelines/v2/templates/steps-build-android.yml index 6164364e4..deec8910c 100644 --- a/.pipelines/v2/templates/steps-build-android.yml +++ b/.pipelines/v2/templates/steps-build-android.yml @@ -145,11 +145,11 @@ steps: displayName: 'Dump vcpkg error logs' condition: failed() -# Unlike the desktop legs, Android stages all three .so files. Desktop -# consumers get ORT/GenAI out of band (pip on the Python side, NuGet on the C# -# side), but an Android consumer has no such channel — the libraries have to be -# packaged into the APK/AAR alongside libfoundry_local.so, so the build that -# produced them is the only place they can be captured consistently. +# Unlike the desktop legs, Android stages the ORT/GenAI runtime alongside +# libfoundry_local.so. Desktop consumers get those out of band (pip on the +# Python side, NuGet on the C# side), but an Android consumer has no such +# channel — the libraries have to be packaged into the APK/AAR, so the build +# that produced them is the only place they can be captured consistently. - bash: | set -euo pipefail src='$(Build.SourcesDirectory)/sdk_v2/cpp/build/Android-${{ parameters.abi }}/${{ parameters.buildConfig }}/bin' @@ -167,4 +167,34 @@ steps: fi done [ "$missing" -eq 0 ] || exit 1 + + # GenAI 0.15.0 split its implementation into libmat.so, which + # libonnxruntime-genai.so lists as a DT_NEEDED — omitting it makes the + # consuming APK fail to dlopen at runtime. Guarded on existence to mirror + # the copy in sdk_v2/cpp/CMakeLists.txt, which keeps 0.14.x working. + 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: a future GenAI repackaging that adds another .so + # would otherwise give a green build and a runtime dlopen failure. Keying + # off "present in the build output" needs no whitelist of system libs — + # those never appear in bin/ — so it cannot fail spuriously on an agent. + 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' From b8529be9adfb0335140331f2e5b84d1392dec1f8 Mon Sep 17 00:00:00 2001 From: sheetalarkadam <100380551+sheetalarkadam@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:35:00 -0700 Subject: [PATCH 3/5] ci(sdk_v2): align the Android arm64 stage id with its artifact name Every other native producer stage uses the same arch token in its stage id and its artifact name (cpp_build_linux_arm64 -> cpp-native-linux-arm64). The Android arm64 stage shortened the ABI to "arm64" while publishing cpp-native-arm64-v8a, and did not even agree with its own x86_64 sibling, which already carries the full ABI string. Renaming is free right now because the stage has never run on main, so no branch policy can reference it as a required check yet. That stops being true once it merges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 24ef037b-246f-4fea-a1a3-4fae5223cb0c --- .pipelines/v2/templates/stages-build-native.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index 2a47c549b..d760db1ed 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -203,7 +203,7 @@ stages: # ==================================================================== # Android arm64-v8a — cross-compiled on a Linux host (build only) # ==================================================================== - - stage: cpp_build_android_arm64 + - stage: cpp_build_android_arm64_v8a displayName: 'C++ Native: Android arm64-v8a' dependsOn: - compute_version From cbefdf7845c19a5aed9fab228a3f041aca802f6b Mon Sep 17 00:00:00 2001 From: sheetalarkadam <100380551+sheetalarkadam@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:16:43 -0700 Subject: [PATCH 4/5] ci(sdk_v2): pin the Android NDK to a verified archive, drop sdkmanager The NDK version was pinned, but the way it was obtained was not. If the pool image lacked the pinned version the step shelled out to sdkmanager, which requires cmdline-tools on the image, prompts for licence acceptance, and performs no integrity check on what it downloads. Resolve it in two paths instead. Fast path: use the image's NDK, but only after confirming source.properties reports exactly the pinned revision -- the previous code inferred the version from the directory name alone. Otherwise download Google's published archive and verify its SHA-1 before use. The archive name and checksum become parameters alongside the version, since all three come from one entry in Google's package manifest and have to move together. A partially-updated triple cannot slip through: the extracted NDK's Pkg.Revision is asserted against ndkVersion, so bumping the version without the archive yields a checksum that still matches but a revision that does not. Extraction uses unzip rather than python -m zipfile, which discards the executable bit and would leave every toolchain binary unusable. Also stop requiring an Android SDK to build. A cross-compile needs only the NDK; the SDK is an emulator concern, so it is now resolved in a step guarded by runEmulatorTests. This removes a hard dependency on pool image contents that were never verified, and lets an image without an SDK still build. Verified against the real toolchain and the live archive: - pinned NDK on the image is detected and its revision asserted - a different NDK version is rejected rather than silently used - no NDK present falls through to the download path - the URL returns HTTP 200, Content-Length 783549481 matching the manifest, Content-Type application/zip, and PK\x03\x04 magic - the archive's first entry is android-ndk-r29/, confirming the single top-level directory the extraction assumes - sha1sum parsing yields a 40-char digest and detects a mismatch - all eight bash step bodies pass bash -n Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 24ef037b-246f-4fea-a1a3-4fae5223cb0c --- .../v2/templates/steps-build-android.yml | 107 ++++++++++++++---- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/.pipelines/v2/templates/steps-build-android.yml b/.pipelines/v2/templates/steps-build-android.yml index deec8910c..b81d10c83 100644 --- a/.pipelines/v2/templates/steps-build-android.yml +++ b/.pipelines/v2/templates/steps-build-android.yml @@ -30,10 +30,20 @@ parameters: - name: androidApi type: number default: 28 -# Pinned so a pool image refresh cannot silently change the toolchain. +# Pinned so a pool image refresh cannot silently change the toolchain. All +# three values come from the same entry in Google's package manifest: +# https://dl.google.com/android/repository/repository2-3.xml (ndk;) +# They must be bumped together. A mismatched triple is caught at runtime: the +# extracted NDK's Pkg.Revision is asserted against ndkVersion below. - name: ndkVersion type: string default: '29.0.14206865' +- name: ndkArchive + type: string + default: 'android-ndk-r29-linux.zip' +- name: ndkSha1 + type: string + default: '87e2bb7e9be5d6a1c6cdf5ec40dd4e0c6d07c30b' # Emulator tests require the x86_64 ABI. Off by default: this stage exists to # guard the build and the dependency wiring, and an emulator boot adds both # runtime and flakiness. Enable per-stage once the leg is proven stable. @@ -49,38 +59,78 @@ steps: "$(Build.BinariesDirectory)/vcpkg/bootstrap-vcpkg.sh" -disableMetrics displayName: 'Bootstrap vcpkg' -# Resolve the NDK from the pool image when possible, and only fall back to -# sdkmanager if the pinned version is genuinely absent. Both paths converge on -# the same pinned version so the toolchain is identical either way. +# Resolve the NDK. Fast path: the pool image already has the pinned version. +# Otherwise fetch Google's published archive and verify it, rather than going +# through sdkmanager -- that needs cmdline-tools on the image, prompts for +# licence acceptance, and gives no integrity check on what it downloaded. +# +# Note this deliberately does not require an Android SDK. A cross-compile only +# needs the NDK; the SDK is an emulator concern and is resolved separately by +# the test step below, so a pool image without one can still build. - bash: | set -euo pipefail pinned='${{ parameters.ndkVersion }}' - sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" + archive='${{ parameters.ndkArchive }}' + expected_sha1='${{ parameters.ndkSha1 }}' - if [ -z "$sdk" ]; then - echo "ERROR: neither ANDROID_SDK_ROOT nor ANDROID_HOME is set on this agent." >&2 - exit 1 - fi + # 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." - ndk="$sdk/ndk/$pinned" - if [ ! -d "$ndk" ]; then - echo "NDK $pinned not present; installing via sdkmanager." - sdkmanager="$sdk/cmdline-tools/latest/bin/sdkmanager" - if [ ! -x "$sdkmanager" ]; then - echo "ERROR: sdkmanager not found at $sdkmanager" >&2 + # 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 - yes | "$sdkmanager" --install "ndk;$pinned" > /dev/null + 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 - if [ ! -d "$ndk" ]; then - echo "ERROR: NDK $pinned still not found at $ndk" >&2 + # 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_HOME = $sdk" - echo "ANDROID_NDK_HOME = $ndk" - echo "##vso[task.setvariable variable=androidSdkRoot]$sdk" + echo "ANDROID_NDK_HOME = $ndk (revision $revision)" echo "##vso[task.setvariable variable=androidNdkHome]$ndk" displayName: 'Resolve Android NDK (${{ parameters.ndkVersion }})' @@ -118,10 +168,23 @@ steps: workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp env: VCPKG_ROOT: $(Build.BinariesDirectory)/vcpkg - ANDROID_HOME: $(androidSdkRoot) 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 \ From a669e916f21eaa2581685e370221a3675736a45f Mon Sep 17 00:00:00 2001 From: sheetalarkadam <100380551+sheetalarkadam@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:29:14 -0700 Subject: [PATCH 5/5] ci(sdk_v2): trim verbose comments in Android build templates --- .../v2/templates/stages-build-native.yml | 8 --- .../v2/templates/steps-build-android.yml | 51 +------------------ 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index d760db1ed..0d10fd8fa 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -8,14 +8,6 @@ # pack_nuget – Microsoft.AI.Foundry.Local.Runtime # (win-x64, win-arm64, linux-x64, linux-arm64, osx-arm64; # Windows ships WinML; ARM64 Linux is CPU-only) -# -# Android (arm64-v8a, x86_64) is built and published as an artifact but is -# deliberately *not* fed into either pack stage: neither the NuGet package nor -# the C++ SDK tgz has an Android consumer today, and Android's dependency -# origins differ enough (GenAI ships as a GitHub Releases AAR rather than a -# NuGet package) that packaging is a separate design question. The stages exist -# so that a dependency bump which is valid on desktop but unavailable for -# Android fails here rather than silently. parameters: - name: buildConfig diff --git a/.pipelines/v2/templates/steps-build-android.yml b/.pipelines/v2/templates/steps-build-android.yml index b81d10c83..90da4b97b 100644 --- a/.pipelines/v2/templates/steps-build-android.yml +++ b/.pipelines/v2/templates/steps-build-android.yml @@ -1,21 +1,3 @@ -# Reusable Android cross-compile steps for the Foundry Local C++ SDK. -# -# Runs on a Linux host and cross-compiles with the Android NDK, so it uses the -# same pool as the linux-x64 stage. Supports arm64-v8a (devices) and x86_64 -# (emulator) via the `abi` parameter. -# -# Android resolves its native dependencies differently from the desktop -# platforms, which is the main reason this stage earns its keep: -# -# ORT – Microsoft.ML.OnnxRuntime NuGet, runtimes/android-{arm64,x64} -# (same package as desktop, so steps-prefetch-nuget.yml applies) -# GenAI – a standalone AAR on *GitHub Releases*, not NuGet -# (see cmake/FindOnnxRuntimeGenAI.cmake) -# -# Because GenAI comes from a different origin, a version that exists on NuGet -# does not necessarily exist as an Android AAR. Without this stage that skew is -# invisible: every desktop leg goes green while Android cannot configure at all. - parameters: - name: abi type: string @@ -30,11 +12,6 @@ parameters: - name: androidApi type: number default: 28 -# Pinned so a pool image refresh cannot silently change the toolchain. All -# three values come from the same entry in Google's package manifest: -# https://dl.google.com/android/repository/repository2-3.xml (ndk;) -# They must be bumped together. A mismatched triple is caught at runtime: the -# extracted NDK's Pkg.Revision is asserted against ndkVersion below. - name: ndkVersion type: string default: '29.0.14206865' @@ -44,9 +21,6 @@ parameters: - name: ndkSha1 type: string default: '87e2bb7e9be5d6a1c6cdf5ec40dd4e0c6d07c30b' -# Emulator tests require the x86_64 ABI. Off by default: this stage exists to -# guard the build and the dependency wiring, and an emulator boot adds both -# runtime and flakiness. Enable per-stage once the leg is proven stable. - name: runEmulatorTests type: boolean default: false @@ -59,14 +33,6 @@ steps: "$(Build.BinariesDirectory)/vcpkg/bootstrap-vcpkg.sh" -disableMetrics displayName: 'Bootstrap vcpkg' -# Resolve the NDK. Fast path: the pool image already has the pinned version. -# Otherwise fetch Google's published archive and verify it, rather than going -# through sdkmanager -- that needs cmdline-tools on the image, prompts for -# licence acceptance, and gives no integrity check on what it downloaded. -# -# Note this deliberately does not require an Android SDK. A cross-compile only -# needs the NDK; the SDK is an emulator concern and is resolved separately by -# the test step below, so a pool image without one can still build. - bash: | set -euo pipefail pinned='${{ parameters.ndkVersion }}' @@ -134,9 +100,6 @@ steps: echo "##vso[task.setvariable variable=androidNdkHome]$ndk" displayName: 'Resolve Android NDK (${{ parameters.ndkVersion }})' -# Prefetches ORT (used by the Android build) and GenAI (unused here — Android -# takes the GitHub AAR instead — but harmless, and it keeps the pinned-version -# drift check against deps_versions.json running on this leg too). - template: steps-prefetch-nuget.yml parameters: ortVersion: ${{ parameters.ortVersion }} @@ -208,11 +171,6 @@ steps: displayName: 'Dump vcpkg error logs' condition: failed() -# Unlike the desktop legs, Android stages the ORT/GenAI runtime alongside -# libfoundry_local.so. Desktop consumers get those out of band (pip on the -# Python side, NuGet on the C# side), but an Android consumer has no such -# channel — the libraries have to be packaged into the APK/AAR, so the build -# that produced them is the only place they can be captured consistently. - bash: | set -euo pipefail src='$(Build.SourcesDirectory)/sdk_v2/cpp/build/Android-${{ parameters.abi }}/${{ parameters.buildConfig }}/bin' @@ -231,20 +189,13 @@ steps: done [ "$missing" -eq 0 ] || exit 1 - # GenAI 0.15.0 split its implementation into libmat.so, which - # libonnxruntime-genai.so lists as a DT_NEEDED — omitting it makes the - # consuming APK fail to dlopen at runtime. Guarded on existence to mirror - # the copy in sdk_v2/cpp/CMakeLists.txt, which keeps 0.14.x working. 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: a future GenAI repackaging that adds another .so - # would otherwise give a green build and a runtime dlopen failure. Keying - # off "present in the build output" needs no whitelist of system libs — - # those never appear in bin/ — so it cannot fail spuriously on an agent. + # but we did not stage. readelf=$(command -v readelf || true) if [ -n "$readelf" ]; then unmet=0