Skip to content

build: disambiguate android savedmodel deps #96

build: disambiguate android savedmodel deps

build: disambiguate android savedmodel deps #96

name: Build TensorFlow Android ARM64 JNI
on:
workflow_dispatch:
push:
branches:
- main
- codex/fix-android-arm64-build
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 360
env:
TFJAVA_COMMIT: c065b70c05acefe703fee21a156561e76e54c66d
ANDROID_PLATFORM: android-arm64
ANDROID_API_LEVEL: "29"
ANDROID_BUILD_TOOLS_VERSION: "29.0.3"
ANDROID_NDK_API_LEVEL: "21"
ANDROID_NDK_VERSION: 18.1.5063045
USE_BAZEL_VERSION: 2.0.0
steps:
- uses: actions/checkout@v4
- name: Reclaim Runner Disk Space
shell: bash
run: |
set -euxo pipefail
echo "Disk usage before cleanup:"
df -h
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL || true
echo "Disk usage after cleanup:"
df -h
- name: Set Up Bazelisk
uses: bazelbuild/setup-bazelisk@v3
- name: Set Up Android SDK
uses: android-actions/setup-android@v3
- name: Install Android Components
shell: bash
run: |
sdkmanager \
"platform-tools" \
"platforms;android-${ANDROID_API_LEVEL}" \
"build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
"ndk;${ANDROID_NDK_VERSION}"
- name: Restore Bazel Repository Cache
id: restore_bazel_repository_cache
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/.bazel-cache/repository
key: ${{ runner.os }}-bazel-${{ env.ANDROID_PLATFORM }}-${{ env.TFJAVA_COMMIT }}-${{ hashFiles('scripts/patch_tfjava.py') }}
restore-keys: |
${{ runner.os }}-bazel-${{ env.ANDROID_PLATFORM }}-${{ env.TFJAVA_COMMIT }}-
${{ runner.os }}-bazel-${{ env.ANDROID_PLATFORM }}-
- name: Clone TensorFlow Java
shell: bash
run: |
git clone --filter=blob:none --no-checkout https://github.com/tensorflow/java.git tfjava
cd tfjava
git checkout "${TFJAVA_COMMIT}"
- name: Set Up Java 11
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "11"
cache: maven
cache-dependency-path: |
tfjava/pom.xml
tfjava/**/pom.xml
- name: Patch TensorFlow Java For Android ARM64
shell: bash
run: |
python3 scripts/patch_tfjava.py tfjava
- name: Vendor Android GNU STL Headers
shell: bash
run: |
set -e
STL_ROOT="tfjava/tensorflow-core/tensorflow-core-api/android-gnu-stl"
NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}"
rm -rf "${STL_ROOT}"
mkdir -p "${STL_ROOT}/include" "${STL_ROOT}/arm64-v8a/include"
if [ -d "${NDK_ROOT}/sources/cxx-stl/gnu-libstdc++/4.9/include" ]; then
GNUSTL_LIB_ROOT="${NDK_ROOT}/sources/cxx-stl/gnu-libstdc++/4.9/libs/arm64-v8a"
SUPCXX_PATH="$(find "${NDK_ROOT}" -path '*aarch64*' -name 'libsupc++.a' -print -quit 2>/dev/null || true)"
if [ -z "${SUPCXX_PATH}" ]; then
SUPCXX_PATH="$(find "${NDK_ROOT}" -name 'libsupc++.a' -print -quit 2>/dev/null || true)"
fi
cp -R "${NDK_ROOT}/sources/cxx-stl/gnu-libstdc++/4.9/include/." "${STL_ROOT}/include/"
if [ -d "${GNUSTL_LIB_ROOT}/include" ]; then
cp -R "${GNUSTL_LIB_ROOT}/include/." "${STL_ROOT}/arm64-v8a/include/"
fi
cp "${GNUSTL_LIB_ROOT}/libgnustl_static.a" "${STL_ROOT}/arm64-v8a/libgnustl_real.a"
if [ -n "${SUPCXX_PATH}" ]; then
cp "${SUPCXX_PATH}" "${STL_ROOT}/arm64-v8a/libsupc++.a"
printf 'GROUP ( libgnustl_real.a libsupc++.a )\n' > "${STL_ROOT}/arm64-v8a/libgnustl_static.a"
else
cp "${GNUSTL_LIB_ROOT}/libgnustl_static.a" "${STL_ROOT}/arm64-v8a/libgnustl_static.a"
fi
else
LIBCXX_LIB_ROOT="${NDK_ROOT}/sources/cxx-stl/llvm-libc++/libs/arm64-v8a"
LIBCXXABI_PATH="$(find "${NDK_ROOT}" -path '*arm64-v8a*' -name 'libc++abi.a' -print -quit 2>/dev/null || true)"
ANDROID_SUPPORT_PATH="$(find "${NDK_ROOT}" -path '*arm64-v8a*' -name 'libandroid_support.a' -print -quit 2>/dev/null || true)"
cp -R "${NDK_ROOT}/sources/cxx-stl/llvm-libc++/include/." "${STL_ROOT}/include/"
if [ -d "${NDK_ROOT}/sources/cxx-stl/llvm-libc++abi/include" ]; then
cp -R "${NDK_ROOT}/sources/cxx-stl/llvm-libc++abi/include/." "${STL_ROOT}/include/"
fi
if [ -d "${NDK_ROOT}/sources/android/support/include" ]; then
cp -R "${NDK_ROOT}/sources/android/support/include/." "${STL_ROOT}/include/"
fi
cp "${LIBCXX_LIB_ROOT}/libc++_static.a" "${STL_ROOT}/arm64-v8a/libc++_static.a"
if [ -f "${LIBCXXABI_PATH}" ]; then
cp "${LIBCXXABI_PATH}" "${STL_ROOT}/arm64-v8a/libc++abi.a"
fi
if [ -f "${ANDROID_SUPPORT_PATH}" ]; then
cp "${ANDROID_SUPPORT_PATH}" "${STL_ROOT}/arm64-v8a/libandroid_support.a"
printf 'GROUP ( libc++_static.a libc++abi.a libandroid_support.a )\n' > "${STL_ROOT}/arm64-v8a/libgnustl_static.a"
elif [ -f "${LIBCXXABI_PATH}" ]; then
printf 'GROUP ( libc++_static.a libc++abi.a )\n' > "${STL_ROOT}/arm64-v8a/libgnustl_static.a"
else
cp "${LIBCXX_LIB_ROOT}/libc++_static.a" "${STL_ROOT}/arm64-v8a/libgnustl_static.a"
fi
fi
find "${STL_ROOT}" -maxdepth 3 -type f | sort | sed -n '1,20p'
- name: Write TensorFlow Configure Overrides
shell: bash
run: |
cat > tfjava/tensorflow-core/tensorflow-core-api/.tf_configure.bazelrc <<EOF
build --action_env=ANDROID_HOME=${ANDROID_SDK_ROOT}
build --action_env=ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}
build --action_env=ANDROID_NDK_API_LEVEL=${ANDROID_NDK_API_LEVEL}
build --action_env=ANDROID_SDK_HOME=${ANDROID_SDK_ROOT}
build --action_env=ANDROID_SDK_API_LEVEL=${ANDROID_API_LEVEL}
build --action_env=ANDROID_BUILD_TOOLS_VERSION=${ANDROID_BUILD_TOOLS_VERSION}
build --repo_env=ANDROID_HOME=${ANDROID_SDK_ROOT}
build --repo_env=ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}
build --repo_env=ANDROID_NDK_API_LEVEL=${ANDROID_NDK_API_LEVEL}
build --repo_env=ANDROID_SDK_HOME=${ANDROID_SDK_ROOT}
build --repo_env=ANDROID_SDK_API_LEVEL=${ANDROID_API_LEVEL}
build --repo_env=ANDROID_BUILD_TOOLS_VERSION=${ANDROID_BUILD_TOOLS_VERSION}
EOF
- name: Show Patched Snippets
shell: bash
run: |
cd tfjava
sed -n '1,120p' tensorflow-core/tensorflow-core-api/build.sh
echo '---'
grep -n 'PLATFORM>${javacpp.platform}' tensorflow-core/tensorflow-core-api/pom.xml || true
echo '---'
grep -n 'android-gnu-stl' tensorflow-core/tensorflow-core-api/pom.xml || true
echo '---'
grep -n 'androidndk\\|androidsdk' tensorflow-core/tensorflow-core-api/WORKSPACE || true
echo '---'
grep -n 'android-arm64' tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/internal/c_api/presets/tensorflow.java || true
echo '---'
cat tensorflow-core/tensorflow-core-api/.tf_configure.bazelrc
- name: Build Android ARM64 Native Libraries
id: build_android
shell: bash
run: |
set -o pipefail
export ANDROID_HOME="${ANDROID_SDK_ROOT}"
export ANDROID_SDK_HOME="${ANDROID_SDK_ROOT}"
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_NDK_HOME}"
export ANDROID_SDK_API_LEVEL="${ANDROID_API_LEVEL}"
export ANDROID_BUILD_TOOLS_VERSION="${ANDROID_BUILD_TOOLS_VERSION}"
export ANDROID_NDK_API_LEVEL="${ANDROID_NDK_API_LEVEL}"
export ANDROID_STL_INCLUDE="${ANDROID_NDK_HOME}/sources/cxx-stl/gnu-libstdc++/4.9/include"
export ANDROID_STL_INCLUDE_ABI="${ANDROID_NDK_HOME}/sources/cxx-stl/gnu-libstdc++/4.9/libs/arm64-v8a/include"
export ANDROID_STL_LIB="${ANDROID_NDK_HOME}/sources/cxx-stl/gnu-libstdc++/4.9/libs/arm64-v8a"
export ANDROID_LLVM_BIN="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin"
export ANDROID_GCC_TOOLCHAIN="${ANDROID_NDK_HOME}/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64"
export ANDROID_CLANG_TARGET="aarch64-linux-android${ANDROID_NDK_API_LEVEL}"
export ANDROID_LLVM_CXX_WRAPPER="${GITHUB_WORKSPACE}/android-clang++"
if [ -x "${ANDROID_LLVM_BIN}/${ANDROID_CLANG_TARGET}-clang++" ]; then
printf '%s\n' '#!/usr/bin/env bash' \
"exec \"${ANDROID_LLVM_BIN}/${ANDROID_CLANG_TARGET}-clang++\" -fuse-ld=lld -B\"${ANDROID_LLVM_BIN}\" --gcc-toolchain=\"${ANDROID_GCC_TOOLCHAIN}\" \"\$@\"" \
> "${ANDROID_LLVM_CXX_WRAPPER}"
elif [ -x "${ANDROID_LLVM_BIN}/clang++" ]; then
printf '%s\n' '#!/usr/bin/env bash' \
"exec \"${ANDROID_LLVM_BIN}/clang++\" --target=${ANDROID_CLANG_TARGET} -fuse-ld=lld -B\"${ANDROID_LLVM_BIN}\" --gcc-toolchain=\"${ANDROID_GCC_TOOLCHAIN}\" \"\$@\"" \
> "${ANDROID_LLVM_CXX_WRAPPER}"
else
echo "No usable LLVM clang++ found under ${ANDROID_LLVM_BIN}" >&2
exit 1
fi
chmod +x "${ANDROID_LLVM_CXX_WRAPPER}"
"${ANDROID_LLVM_CXX_WRAPPER}" --version
export JAVA_HOME="${JAVA_HOME_11_X64}"
export LANG="C.UTF-8"
export LC_ALL="C.UTF-8"
export PATH="${JAVA_HOME}/bin:${PATH}"
export CPLUS_INCLUDE_PATH="${ANDROID_STL_INCLUDE}:${ANDROID_STL_INCLUDE_ABI}${CPLUS_INCLUDE_PATH:+:${CPLUS_INCLUDE_PATH}}"
export C_INCLUDE_PATH="${ANDROID_STL_INCLUDE_ABI}${C_INCLUDE_PATH:+:${C_INCLUDE_PATH}}"
export LIBRARY_PATH="${ANDROID_STL_LIB}${LIBRARY_PATH:+:${LIBRARY_PATH}}"
export BAZEL_REPOSITORY_CACHE="${GITHUB_WORKSPACE}/.bazel-cache/repository"
mkdir -p "${BAZEL_REPOSITORY_CACHE}"
mkdir -p "${GITHUB_WORKSPACE}/artifacts"
cd tfjava
mvn -B -e \
-Dproject.build.sourceEncoding=UTF-8 \
-Dproject.reporting.outputEncoding=UTF-8 \
-DskipTests \
-Djavacpp.platform="${ANDROID_PLATFORM}" \
-Djavacpp.platform.properties="${ANDROID_PLATFORM}" \
-Djavacpp.platform.compiler="${ANDROID_LLVM_CXX_WRAPPER}" \
-Djavacpp.platform.root="${ANDROID_NDK_HOME}" \
-pl tensorflow-core/tensorflow-core-api \
-am \
package 2>&1 | tee "${GITHUB_WORKSPACE}/artifacts/build.log"
- name: Summarize First Build Failure
if: failure() && steps.build_android.outcome == 'failure'
shell: bash
run: |
python3 - <<'PY'
import re
from pathlib import Path
log_path = Path("artifacts/build.log")
if not log_path.exists():
print("::error::build.log was not produced")
raise SystemExit(0)
lines = log_path.read_text(encoding="utf-8", errors="ignore").splitlines()
primary_patterns = [
re.compile(r"^\s*.+?:\d+(?::\d+)?: (?:fatal )?error: .+$"),
re.compile(r"^\s*.+?: error: .+$"),
re.compile(r"^\s*(?:ld(?:\.lld)?|clang(?:\+\+)?): error: .+$"),
re.compile(r"^\s*undefined reference to .+$"),
re.compile(r"^\s*cannot find -l.+$"),
re.compile(r"^\s*collect2: error: .+$"),
re.compile(r"^\s*.+?UnsatisfiedLinkError.+$"),
re.compile(r"^\s*.+?NoSuch.+$"),
]
skip = (
"Failed to execute goal org.bytedeco:javacpp",
"Execution javacpp-build of goal",
"Process exited with an error: 1",
"BUILD FAILURE",
"compilation of rule",
"Linking of rule",
"Failed to restore: Cache service responded with 400",
)
def is_skippable(line: str) -> bool:
return any(token in line for token in skip)
hit = None
for i, line in enumerate(lines):
if is_skippable(line):
continue
if any(p.search(line) for p in primary_patterns):
hit = i
break
if hit is None:
for i in range(len(lines) - 1, -1, -1):
line = lines[i]
normalized = line.strip()
if not normalized or is_skippable(line):
continue
if "error:" in normalized or "undefined reference" in normalized:
hit = i
break
if hit is None:
for i, line in enumerate(lines):
if is_skippable(line):
continue
if line.startswith("ERROR: ") or line.startswith("FAILED: "):
hit = i
break
if hit is None:
hit = max(0, len(lines) - 80)
start = max(0, hit - 8)
end = min(len(lines), hit + 20)
snippet = "\n".join(lines[start:end]).strip()
if not snippet:
snippet = "\n".join(lines[-40:]).strip()
summary_path = Path("artifacts/first_failure_summary.txt")
summary_path.write_text(snippet + "\n", encoding="utf-8")
headline = lines[hit].strip() if lines else "Build failed without captured output"
headline = headline.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
print(f"::error::{headline[:400]}")
print("First failure summary:")
print(snippet)
PY
- name: Collect Artifacts
if: always()
shell: bash
run: |
set -e
mkdir -p "${GITHUB_WORKSPACE}/artifacts/runtime" "${GITHUB_WORKSPACE}/artifacts/logs"
NATIVE_ROOT="tfjava/tensorflow-core/tensorflow-core-api/target/native"
STRIP_BIN="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip"
for name in \
libjnitensorflow.so \
libjnijavacpp.so \
libtensorflow.so.2 \
libtensorflow_framework.so.2
do
src="$(find "${NATIVE_ROOT}" -type f -name "${name}" -print -quit 2>/dev/null || true)"
if [ -n "${src}" ]; then
cp "${src}" "${GITHUB_WORKSPACE}/artifacts/runtime/${name}"
fi
done
if [ -x "${STRIP_BIN}" ]; then
echo "Runtime library sizes before strip:"
find "${GITHUB_WORKSPACE}/artifacts/runtime" -maxdepth 1 -type f -name '*.so*' -exec du -h {} \; | sort
find "${GITHUB_WORKSPACE}/artifacts/runtime" -maxdepth 1 -type f -name '*.so*' -print0 | while IFS= read -r -d '' lib; do
"${STRIP_BIN}" --strip-unneeded "${lib}" || "${STRIP_BIN}" --strip-debug "${lib}" || true
done
echo "Runtime library sizes after strip:"
find "${GITHUB_WORKSPACE}/artifacts/runtime" -maxdepth 1 -type f -name '*.so*' -exec du -h {} \; | sort
else
echo "llvm-strip not found at ${STRIP_BIN}; uploading unstripped runtime libraries" >&2
fi
if [ -f "${GITHUB_WORKSPACE}/artifacts/build.log" ]; then
mv "${GITHUB_WORKSPACE}/artifacts/build.log" "${GITHUB_WORKSPACE}/artifacts/logs/build.log"
fi
if [ -f "${GITHUB_WORKSPACE}/artifacts/first_failure_summary.txt" ]; then
mv "${GITHUB_WORKSPACE}/artifacts/first_failure_summary.txt" "${GITHUB_WORKSPACE}/artifacts/logs/first_failure_summary.txt"
fi
- name: Save Bazel Repository Cache
if: always() && steps.restore_bazel_repository_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/.bazel-cache/repository
key: ${{ runner.os }}-bazel-${{ env.ANDROID_PLATFORM }}-${{ env.TFJAVA_COMMIT }}-${{ hashFiles('scripts/patch_tfjava.py') }}
- name: Upload Runtime Artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: tensorflow-android-arm64-build
path: artifacts/runtime
if-no-files-found: warn
- name: Upload Failure Logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: tensorflow-android-arm64-debug
path: artifacts/logs
if-no-files-found: warn