chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
name: Android Cross Build & Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
api:
|
||||
description: 'Android API level'
|
||||
required: false
|
||||
default: '34'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NDK_VERSION: '26.1.10909125'
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: [x86_64]
|
||||
api: ${{ github.event.inputs.api && fromJSON(format('["{0}"]', github.event.inputs.api)) || fromJSON('["34"]') }}
|
||||
steps:
|
||||
# ── Environment setup ──────────────────────────────────────────────
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
cmake ninja-build git ca-certificates python3 \
|
||||
build-essential make unzip curl
|
||||
|
||||
- name: Setup ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: android-${{ matrix.abi }}
|
||||
max-size: 300M
|
||||
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: Enable KVM
|
||||
run: sudo chmod 666 /dev/kvm || true
|
||||
|
||||
- name: Install NDK, emulator and system image
|
||||
shell: bash
|
||||
run: |
|
||||
sdkmanager --install \
|
||||
"ndk;$NDK_VERSION" \
|
||||
"platform-tools" \
|
||||
"platforms;android-${{ matrix.api }}" \
|
||||
"emulator"
|
||||
|
||||
# Install x86_64 system image (try variants in order of availability)
|
||||
sdkmanager --install "system-images;android-${{ matrix.api }};google_apis;x86_64" 2>/dev/null || \
|
||||
sdkmanager --install "system-images;android-${{ matrix.api }};google_apis_playstore;x86_64" 2>/dev/null || \
|
||||
sdkmanager --install "system-images;android-${{ matrix.api }};default;x86_64"
|
||||
|
||||
# ── Step 1: build host protoc (using HOST compiler, NOT NDK) ───────
|
||||
- name: Cache host protoc
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: build_host
|
||||
key: ${{ runner.os }}-host-protoc-${{ hashFiles('thirdparty/protobuf/**', 'CMakeLists.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-host-protoc-
|
||||
|
||||
- name: 'Step 1: Build host protoc'
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -f "build_host/bin/protoc" ]; then
|
||||
git submodule foreach --recursive 'git stash --include-untracked' 2>/dev/null || true
|
||||
cmake -S . -B build_host \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_TOOLCHAIN_FILE="" \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-G Ninja
|
||||
cmake --build build_host --target protoc --parallel
|
||||
else
|
||||
echo "Using cached host protoc"
|
||||
fi
|
||||
|
||||
# ── Step 2: cross-compile zvec + tests for Android ─────────────────
|
||||
- name: 'Step 2: Cross-compile zvec and tests'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_DIR: build_android_${{ matrix.abi }}
|
||||
run: |
|
||||
ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
|
||||
# Reset thirdparty so the cross toolchain can patch cleanly
|
||||
git submodule foreach --recursive 'git stash --include-untracked' 2>/dev/null || true
|
||||
|
||||
# Force reconfigure to pick up any cmake changes
|
||||
rm -f "$BUILD_DIR/CMakeCache.txt"
|
||||
|
||||
cmake -S . -B "$BUILD_DIR" -G Ninja \
|
||||
-DANDROID_NDK="$ANDROID_NDK_HOME" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
|
||||
-DANDROID_ABI=${{ matrix.abi }} \
|
||||
-DANDROID_NATIVE_API_LEVEL=${{ matrix.api }} \
|
||||
-DANDROID_STL=c++_static \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_PYTHON_BINDINGS=OFF \
|
||||
-DBUILD_TOOLS=OFF \
|
||||
-DENABLE_NATIVE=OFF \
|
||||
-DAUTO_DETECT_ARCH=OFF \
|
||||
-DENABLE_WERROR=ON \
|
||||
-DCMAKE_INSTALL_PREFIX="$BUILD_DIR/install" \
|
||||
-DGLOBAL_CC_PROTOBUF_PROTOC="$GITHUB_WORKSPACE/build_host/bin/protoc" \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
|
||||
echo "Building all targets..."
|
||||
cmake --build "$BUILD_DIR" --parallel
|
||||
|
||||
# Discover test targets from ctest metadata
|
||||
echo "Discovering test targets..."
|
||||
TEST_NAMES=()
|
||||
while IFS= read -r line; do
|
||||
name=$(echo "$line" | sed -n 's/.*Test[[:space:]]*#[0-9]*:[[:space:]]*//p')
|
||||
[ -n "$name" ] && TEST_NAMES+=("$name")
|
||||
done < <(cd "$BUILD_DIR" && ctest --show-only 2>/dev/null || true)
|
||||
|
||||
# Fallback: scan ninja targets for *_test
|
||||
if [ ${#TEST_NAMES[@]} -eq 0 ]; then
|
||||
echo "ctest unavailable, scanning ninja targets..."
|
||||
while IFS= read -r line; do
|
||||
name=$(echo "$line" | sed -n 's/^\([a-zA-Z0-9_]*_test\): .*/\1/p')
|
||||
[ -n "$name" ] && TEST_NAMES+=("$name")
|
||||
done < <(ninja -C "$BUILD_DIR" -t targets all 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
echo "Building ${#TEST_NAMES[@]} test executables..."
|
||||
ninja -C "$BUILD_DIR" -j$(nproc) "${TEST_NAMES[@]}"
|
||||
|
||||
# ── Step 3: start emulator ─────────────────────────────────────────
|
||||
- name: 'Step 3: Start Android emulator'
|
||||
shell: bash
|
||||
run: |
|
||||
AVD_NAME="zvec_test_avd"
|
||||
AVD_DIR="$HOME/.android/avd/${AVD_NAME}.avd"
|
||||
AVD_INI="$HOME/.android/avd/${AVD_NAME}.ini"
|
||||
|
||||
# Find installed system image (same priority as build_android.sh)
|
||||
SYS_IMG=""
|
||||
for variant in google_apis google_apis_playstore default; do
|
||||
candidate="$ANDROID_HOME/system-images/android-${{ matrix.api }}/$variant/x86_64"
|
||||
if [ -d "$candidate" ]; then
|
||||
SYS_IMG="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$SYS_IMG" ]; then
|
||||
SYS_IMG=$(find "$ANDROID_HOME/system-images" -type d -name "x86_64" 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$SYS_IMG" ]; then
|
||||
echo "ERROR: No x86_64 system image found"
|
||||
exit 1
|
||||
fi
|
||||
echo "System image: $SYS_IMG"
|
||||
|
||||
# Extract tag from path (e.g. .../google_apis/x86_64 -> google_apis)
|
||||
SYS_TAG=$(basename "$(dirname "$SYS_IMG")")
|
||||
|
||||
# Create AVD via INI files (faster and more reliable than avdmanager)
|
||||
mkdir -p "$AVD_DIR"
|
||||
|
||||
cat > "$AVD_INI" << EOAVD
|
||||
avd.ini.encoding=UTF-8
|
||||
path=${AVD_DIR}
|
||||
path.rel=avd/${AVD_NAME}.avd
|
||||
target=android-${{ matrix.api }}
|
||||
EOAVD
|
||||
|
||||
cat > "$AVD_DIR/config.ini" << EOCFG
|
||||
AvdId=${AVD_NAME}
|
||||
PlayStore.enabled=false
|
||||
abi.type=x86_64
|
||||
avd.ini.displayname=${AVD_NAME}
|
||||
avd.ini.encoding=UTF-8
|
||||
disk.dataPartition.size=8G
|
||||
hw.accelerator.isConfigured=true
|
||||
hw.cpu.arch=x86_64
|
||||
hw.cpu.ncore=4
|
||||
hw.lcd.density=420
|
||||
hw.lcd.height=1920
|
||||
hw.lcd.width=1080
|
||||
hw.ramSize=4096
|
||||
image.sysdir.1=${SYS_IMG}/
|
||||
tag.display=${SYS_TAG}
|
||||
tag.id=${SYS_TAG}
|
||||
EOCFG
|
||||
|
||||
echo "Created AVD: $AVD_NAME"
|
||||
|
||||
# Launch emulator in background
|
||||
$ANDROID_HOME/emulator/emulator -avd "$AVD_NAME" \
|
||||
-no-window -no-audio -no-boot-anim \
|
||||
-gpu swiftshader_indirect \
|
||||
-netdelay none -netspeed full \
|
||||
-memory 4096 \
|
||||
-no-snapshot \
|
||||
-wipe-data &
|
||||
echo "EMULATOR_PID=$!" >> "$GITHUB_ENV"
|
||||
|
||||
# Wait for device to be reachable
|
||||
adb wait-for-device
|
||||
|
||||
# Poll for boot completion with timeout
|
||||
echo "Waiting for boot to complete..."
|
||||
TIMEOUT=300
|
||||
ELAPSED=0
|
||||
while true; do
|
||||
BOOTED=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n ' || true)
|
||||
if [ "$BOOTED" = "1" ]; then
|
||||
echo "Emulator ready (took ${ELAPSED}s)"
|
||||
break
|
||||
fi
|
||||
if [ $ELAPSED -ge $TIMEOUT ]; then
|
||||
echo "ERROR: Emulator failed to boot within ${TIMEOUT}s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
ELAPSED=$((ELAPSED + 3))
|
||||
done
|
||||
|
||||
echo "Device ABI: $(adb shell getprop ro.product.cpu.abi | tr -d '\r')"
|
||||
echo "ABI list : $(adb shell getprop ro.product.cpu.abilist | tr -d '\r')"
|
||||
|
||||
# ── Step 4: run unit tests on emulator ─────────────────────────────
|
||||
- name: 'Step 4: Run unit tests on emulator'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_DIR: build_android_${{ matrix.abi }}
|
||||
run: |
|
||||
DEVICE_TEST_DIR="/data/local/tmp/zvec_tests"
|
||||
DEVICE_LIB_DIR="$DEVICE_TEST_DIR/lib"
|
||||
adb shell "mkdir -p $DEVICE_TEST_DIR $DEVICE_LIB_DIR"
|
||||
|
||||
# Push shared libraries
|
||||
echo "Pushing shared libraries..."
|
||||
SO_COUNT=0
|
||||
while IFS= read -r so_file; do
|
||||
adb push "$so_file" "$DEVICE_LIB_DIR/$(basename "$so_file")" > /dev/null 2>&1
|
||||
SO_COUNT=$((SO_COUNT + 1))
|
||||
done < <(find "$BUILD_DIR/lib" -name "*.so" -type f 2>/dev/null)
|
||||
echo "Pushed $SO_COUNT shared libraries"
|
||||
|
||||
# Push helper binaries (needed by crash_recovery tests which fork+exec them)
|
||||
echo "Pushing helper binaries..."
|
||||
for helper_name in data_generator collection_optimizer; do
|
||||
helper_path=$(find "$BUILD_DIR" -name "$helper_name" -type f -executable ! -name "*_test" 2>/dev/null | head -1)
|
||||
if [ -n "$helper_path" ]; then
|
||||
adb push "$helper_path" "$DEVICE_TEST_DIR/$helper_name" > /dev/null 2>&1
|
||||
adb shell "chmod 755 $DEVICE_TEST_DIR/$helper_name"
|
||||
echo " Pushed $helper_name"
|
||||
fi
|
||||
done
|
||||
|
||||
# Discover test targets from ctest metadata
|
||||
TEST_NAMES=()
|
||||
while IFS= read -r line; do
|
||||
name=$(echo "$line" | sed -n 's/.*Test[[:space:]]*#[0-9]*:[[:space:]]*//p')
|
||||
[ -n "$name" ] && TEST_NAMES+=("$name")
|
||||
done < <(cd "$BUILD_DIR" && ctest --show-only 2>/dev/null || true)
|
||||
|
||||
if [ ${#TEST_NAMES[@]} -eq 0 ]; then
|
||||
while IFS= read -r line; do
|
||||
name=$(echo "$line" | sed -n 's/^\([a-zA-Z0-9_]*_test\): .*/\1/p')
|
||||
[ -n "$name" ] && TEST_NAMES+=("$name")
|
||||
done < <(ninja -C "$BUILD_DIR" -t targets all 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Collect test binaries
|
||||
TEST_BINS=()
|
||||
for name in "${TEST_NAMES[@]}"; do
|
||||
bin_path=$(find "$BUILD_DIR" -name "$name" -type f -executable 2>/dev/null | head -1)
|
||||
if [ -n "$bin_path" ]; then
|
||||
TEST_BINS+=("$bin_path")
|
||||
else
|
||||
echo "WARNING: binary not found for '$name'"
|
||||
fi
|
||||
done
|
||||
|
||||
TOTAL=${#TEST_BINS[@]}
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
FAILED_NAMES=()
|
||||
IDX=0
|
||||
|
||||
echo "Running $TOTAL unit tests on emulator..."
|
||||
|
||||
for test_bin in "${TEST_BINS[@]}"; do
|
||||
IDX=$((IDX + 1))
|
||||
test_name=$(basename "$test_bin")
|
||||
device_path="$DEVICE_TEST_DIR/$test_name"
|
||||
# Give each test its own working directory to avoid name collisions
|
||||
WORK_DIR="$DEVICE_TEST_DIR/workdir_${test_name}"
|
||||
|
||||
echo ""
|
||||
echo "────────────────────────────────────────"
|
||||
echo " [$IDX/$TOTAL] $test_name"
|
||||
echo "────────────────────────────────────────"
|
||||
|
||||
set +e
|
||||
# Create isolated working directory
|
||||
adb shell "mkdir -p $WORK_DIR" 2>/dev/null
|
||||
|
||||
# Copy helper binaries into working directory so crash_recovery tests
|
||||
# (which fork+exec data_generator / collection_optimizer) can find them
|
||||
adb shell "for h in $DEVICE_TEST_DIR/data_generator $DEVICE_TEST_DIR/collection_optimizer; do [ -f \$h ] && cp \$h $WORK_DIR/; done" 2>/dev/null
|
||||
|
||||
# Push test binary
|
||||
adb push "$test_bin" "$device_path" > /dev/null 2>&1
|
||||
adb shell "chmod 755 $device_path" 2>/dev/null
|
||||
|
||||
# Run test from its own working directory with LD_LIBRARY_PATH
|
||||
OUTPUT=$(adb shell "cd $WORK_DIR && LD_LIBRARY_PATH=$DEVICE_LIB_DIR $device_path 2>&1; echo EXIT_CODE=\$?" 2>&1)
|
||||
|
||||
# Extract exit code from the output
|
||||
EXIT_CODE=$(echo "$OUTPUT" | grep -o 'EXIT_CODE=[0-9]*' | tail -1 | cut -d= -f2)
|
||||
set -e
|
||||
|
||||
# Print test output (without the EXIT_CODE marker)
|
||||
echo "$OUTPUT" | grep -v 'EXIT_CODE=' | sed 's/^/ /' || true
|
||||
|
||||
if [ "$EXIT_CODE" = "0" ]; then
|
||||
echo " >>> PASSED"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
# Detect "crash-on-exit" pattern: all gtest assertions passed but
|
||||
# process crashed during static destructor teardown (common with c++_static STL)
|
||||
GTEST_PASSED_LINE=$(echo "$OUTPUT" | grep '\[ PASSED \]' | tail -1 || true)
|
||||
GTEST_FAILED_LINE=$(echo "$OUTPUT" | grep '\[ FAILED \]' | head -1 || true)
|
||||
if [ -n "$GTEST_PASSED_LINE" ] && [ -z "$GTEST_FAILED_LINE" ] && \
|
||||
{ [ "$EXIT_CODE" = "139" ] || [ "$EXIT_CODE" = "134" ] || [ "$EXIT_CODE" = "135" ]; }; then
|
||||
echo " >>> PASSED (crash-on-exit ignored, exit=$EXIT_CODE)"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo " >>> FAILED (exit=$EXIT_CODE)"
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_NAMES+=("$test_name")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up binary and working directory to reclaim disk space
|
||||
adb shell "rm -rf $device_path $WORK_DIR" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " Test Summary"
|
||||
echo "============================================================"
|
||||
echo " Total : $TOTAL"
|
||||
echo " Passed : $PASSED"
|
||||
echo " Failed : $FAILED"
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo ""
|
||||
echo " Failed tests:"
|
||||
for name in "${FAILED_NAMES[@]}"; do
|
||||
echo " - $name"
|
||||
done
|
||||
fi
|
||||
echo "============================================================"
|
||||
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "All tests passed!"
|
||||
|
||||
# ── Step 5: build and run examples ─────────────────────────────────
|
||||
- name: 'Step 5: Build and run examples'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_DIR: build_android_${{ matrix.abi }}
|
||||
run: |
|
||||
ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
EXAMPLES_BUILD="examples/c++/build-android-examples-${{ matrix.abi }}"
|
||||
|
||||
cmake -S examples/c++ -B "$EXAMPLES_BUILD" -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
|
||||
-DANDROID_ABI=${{ matrix.abi }} \
|
||||
-DANDROID_PLATFORM=android-${{ matrix.api }} \
|
||||
-DANDROID_STL=c++_static \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DHOST_BUILD_DIR="$BUILD_DIR" \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
cmake --build "$EXAMPLES_BUILD" --parallel
|
||||
|
||||
# Reuse the shared-library directory from Step 4; push again in
|
||||
# case Step 4 was skipped or the directory was cleaned.
|
||||
DEVICE_LIB_DIR="/data/local/tmp/zvec_tests/lib"
|
||||
adb shell "mkdir -p $DEVICE_LIB_DIR" 2>/dev/null || true
|
||||
SO_COUNT=0
|
||||
while IFS= read -r so_file; do
|
||||
adb push "$so_file" "$DEVICE_LIB_DIR/$(basename "$so_file")" > /dev/null 2>&1
|
||||
SO_COUNT=$((SO_COUNT + 1))
|
||||
done < <(find "$BUILD_DIR/lib" -name "*.so" -type f 2>/dev/null)
|
||||
echo "Pushed $SO_COUNT shared libraries to $DEVICE_LIB_DIR"
|
||||
|
||||
for example in ailego-example core-example db-example; do
|
||||
if [ -f "$EXAMPLES_BUILD/$example" ]; then
|
||||
echo "=== Running $example ==="
|
||||
adb push "$EXAMPLES_BUILD/$example" "/data/local/tmp/$example" > /dev/null 2>&1
|
||||
adb shell "chmod 755 /data/local/tmp/$example && cd /data/local/tmp && LD_LIBRARY_PATH=$DEVICE_LIB_DIR ./$example"
|
||||
adb shell "rm -f /data/local/tmp/$example"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Cleanup ────────────────────────────────────────────────────────
|
||||
- name: Stop emulator
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
adb emu kill 2>/dev/null || true
|
||||
sleep 2
|
||||
if [ -n "$EMULATOR_PID" ]; then
|
||||
kill "$EMULATOR_PID" 2>/dev/null || true
|
||||
fi
|
||||
Reference in New Issue
Block a user