396 lines
14 KiB
Bash
Executable File
396 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
set -e
|
|
|
|
#######################################################################
|
|
# build_android.sh — cross-compile zvec for Android arm64-v8a,
|
|
# then run all C/C++ unit tests inside an emulator.
|
|
#
|
|
# Usage:
|
|
# ./scripts/build_android.sh [api_level] [build_type]
|
|
#
|
|
# Examples:
|
|
# ./scripts/build_android.sh # defaults: API 36, Release
|
|
# ./scripts/build_android.sh 36 Debug
|
|
#######################################################################
|
|
|
|
CURRENT_DIR=$(pwd)
|
|
ABI="arm64-v8a"
|
|
API_LEVEL=${1:-35}
|
|
BUILD_TYPE=${2:-"Release"}
|
|
CORE_COUNT=$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
|
|
|
|
# ── Android SDK paths (set later, after host protoc build) ───────────
|
|
ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}
|
|
ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$(ls -d "$ANDROID_SDK_ROOT/ndk/"* 2>/dev/null | sort -V | tail -1)}
|
|
|
|
echo "============================================================"
|
|
echo " Android Cross Build & Test"
|
|
echo "============================================================"
|
|
echo " ABI : $ABI"
|
|
echo " API Level : $API_LEVEL"
|
|
echo " Build Type : $BUILD_TYPE"
|
|
echo " NDK : $ANDROID_NDK_HOME"
|
|
echo " CPU cores : $CORE_COUNT"
|
|
echo "============================================================"
|
|
|
|
if [ ! -d "$ANDROID_NDK_HOME" ]; then
|
|
echo "ERROR: ANDROID_NDK_HOME not found at $ANDROID_NDK_HOME"
|
|
echo "Please install the NDK via Android Studio or sdkmanager."
|
|
exit 1
|
|
fi
|
|
|
|
# ── Step 1: build host protoc (using HOST compiler, NOT NDK) ─────────
|
|
echo ""
|
|
echo ">>> Step 1: Building protoc for host..."
|
|
HOST_BUILD_DIR="build_host"
|
|
|
|
git submodule foreach --recursive 'git stash --include-untracked' 2>/dev/null || true
|
|
|
|
if [ ! -f "$CURRENT_DIR/$HOST_BUILD_DIR/bin/protoc" ]; then
|
|
# Explicitly avoid NDK toolchain for host build
|
|
cmake -S . -B "$HOST_BUILD_DIR" \
|
|
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
|
-DCMAKE_TOOLCHAIN_FILE="" \
|
|
-G Ninja
|
|
cmake --build "$HOST_BUILD_DIR" --target protoc -j"$CORE_COUNT"
|
|
else
|
|
echo " (cached — skipping)"
|
|
fi
|
|
PROTOC_EXECUTABLE=$CURRENT_DIR/$HOST_BUILD_DIR/bin/protoc
|
|
echo ">>> Step 1: Done (protoc=$PROTOC_EXECUTABLE)"
|
|
|
|
# ── Now export Android env vars for cross-compilation ────────────────
|
|
export ANDROID_SDK_ROOT
|
|
export ANDROID_HOME=$ANDROID_SDK_ROOT
|
|
export ANDROID_NDK_HOME
|
|
export CMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake
|
|
|
|
export PATH=$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin
|
|
export PATH=$PATH:$ANDROID_SDK_ROOT/platform-tools
|
|
export PATH=$PATH:$ANDROID_SDK_ROOT/emulator
|
|
export PATH=$PATH:$ANDROID_NDK_HOME
|
|
|
|
# ── Step 2: cross-compile zvec + tests for Android ───────────────────
|
|
echo ""
|
|
echo ">>> Step 2: Cross-compiling zvec for Android ($ABI, API $API_LEVEL)..."
|
|
|
|
# reset thirdparty so the cross toolchain can patch cleanly
|
|
git submodule foreach --recursive 'git stash --include-untracked' 2>/dev/null || true
|
|
|
|
BUILD_DIR="build_android_${ABI}"
|
|
|
|
# Force CMake reconfigure to pick up any bazel.cmake changes
|
|
rm -f "$BUILD_DIR/CMakeCache.txt"
|
|
|
|
cmake -S . -B "$BUILD_DIR" -G Ninja \
|
|
-DANDROID_NDK="$ANDROID_NDK_HOME" \
|
|
-DCMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" \
|
|
-DANDROID_ABI="$ABI" \
|
|
-DANDROID_NATIVE_API_LEVEL="$API_LEVEL" \
|
|
-DANDROID_STL="c++_static" \
|
|
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
|
-DBUILD_PYTHON_BINDINGS=OFF \
|
|
-DBUILD_TOOLS=OFF \
|
|
-DENABLE_NATIVE=OFF \
|
|
-DAUTO_DETECT_ARCH=OFF \
|
|
-DCMAKE_INSTALL_PREFIX="$BUILD_DIR/install" \
|
|
-DGLOBAL_CC_PROTOBUF_PROTOC="$PROTOC_EXECUTABLE"
|
|
|
|
echo " Building library..."
|
|
cmake --build "$BUILD_DIR" -j"$CORE_COUNT"
|
|
|
|
# Discover test target names from ctest metadata (before building)
|
|
echo " Discovering test targets from ctest..."
|
|
TEST_NAMES=()
|
|
while IFS= read -r line; do
|
|
name=$(echo "$line" | sed -n 's/.*Test[[:space:]]*#[0-9]*:[[:space:]]*//p')
|
|
if [ -n "$name" ]; then
|
|
TEST_NAMES+=("$name")
|
|
fi
|
|
done < <(cd "$BUILD_DIR" && ctest --show-only 2>/dev/null || true)
|
|
|
|
# Fallback: find *_test targets from CMake cache
|
|
if [ ${#TEST_NAMES[@]} -eq 0 ]; then
|
|
echo " ctest listing unavailable, scanning ninja targets..."
|
|
while IFS= read -r line; do
|
|
name=$(echo "$line" | sed -n 's/^\([a-zA-Z0-9_]*_test\): .*/\1/p')
|
|
if [ -n "$name" ]; then
|
|
TEST_NAMES+=("$name")
|
|
fi
|
|
done < <(ninja -C "$BUILD_DIR" -t targets all 2>/dev/null || true)
|
|
fi
|
|
|
|
echo " Found ${#TEST_NAMES[@]} test targets."
|
|
if [ ${#TEST_NAMES[@]} -eq 0 ]; then
|
|
echo "WARNING: No test targets found. Skipping emulator step."
|
|
exit 0
|
|
fi
|
|
|
|
# Build all test executables at once (without running ctest)
|
|
echo " Building ${#TEST_NAMES[@]} test executables..."
|
|
ninja -C "$BUILD_DIR" -j"$CORE_COUNT" "${TEST_NAMES[@]}"
|
|
|
|
echo ">>> Step 2: Done"
|
|
|
|
# ── Step 3: collect test binaries ────────────────────────────────────
|
|
echo ""
|
|
echo ">>> Step 3: Collecting test binaries..."
|
|
|
|
TEST_BINS=()
|
|
for name in "${TEST_NAMES[@]}"; do
|
|
bin_path=$(find "$BUILD_DIR" -name "$name" -type f -perm +111 2>/dev/null | head -1)
|
|
if [ -n "$bin_path" ]; then
|
|
TEST_BINS+=("$bin_path")
|
|
else
|
|
echo " WARNING: binary not found for target '$name'"
|
|
fi
|
|
done
|
|
|
|
echo " Found ${#TEST_BINS[@]} test binaries."
|
|
if [ ${#TEST_BINS[@]} -eq 0 ]; then
|
|
echo "WARNING: No test binaries found. Skipping emulator step."
|
|
exit 0
|
|
fi
|
|
|
|
# ── Step 4: start emulator ───────────────────────────────────────────
|
|
echo ""
|
|
echo ">>> Step 4: Starting Android emulator..."
|
|
|
|
EMULATOR_BIN="$ANDROID_SDK_ROOT/emulator/emulator"
|
|
ADB_BIN="$ANDROID_SDK_ROOT/platform-tools/adb"
|
|
AVD_NAME="zvec_test_avd"
|
|
|
|
# Find a usable system image
|
|
SYS_IMG=""
|
|
for candidate in \
|
|
"$ANDROID_SDK_ROOT/system-images/android-${API_LEVEL}/default/arm64-v8a" \
|
|
"$ANDROID_SDK_ROOT/system-images/android-${API_LEVEL}/google_apis/arm64-v8a" \
|
|
"$ANDROID_SDK_ROOT/system-images/android-${API_LEVEL}/google_apis_playstore/arm64-v8a"; do
|
|
if [ -d "$candidate" ]; then
|
|
SYS_IMG="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
|
|
# If the exact API level isn't found, try any available arm64-v8a image
|
|
if [ -z "$SYS_IMG" ]; then
|
|
echo " No system image for API $API_LEVEL, searching for any arm64-v8a image..."
|
|
SYS_IMG=$(find "$ANDROID_SDK_ROOT/system-images" -type d -name "arm64-v8a" 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [ -z "$SYS_IMG" ]; then
|
|
echo "ERROR: No arm64-v8a system image found."
|
|
echo "Install one via: sdkmanager 'system-images;android-${API_LEVEL};default;arm64-v8a'"
|
|
exit 1
|
|
fi
|
|
echo " Using system image: $SYS_IMG"
|
|
|
|
# Create AVD without avdmanager (write INI files directly)
|
|
AVD_DIR="$HOME/.android/avd/${AVD_NAME}.avd"
|
|
AVD_INI="$HOME/.android/avd/${AVD_NAME}.ini"
|
|
|
|
cleanup_emulator() {
|
|
echo ""
|
|
echo ">>> Cleaning up emulator..."
|
|
$ADB_BIN emu kill 2>/dev/null || true
|
|
sleep 2
|
|
# Kill any lingering emulator processes for our AVD
|
|
pkill -f "emulator.*${AVD_NAME}" 2>/dev/null || true
|
|
}
|
|
trap cleanup_emulator EXIT
|
|
|
|
mkdir -p "$AVD_DIR"
|
|
|
|
# Write the top-level .ini
|
|
cat > "$AVD_INI" << EOF
|
|
avd.ini.encoding=UTF-8
|
|
path=${AVD_DIR}
|
|
path.rel=avd/${AVD_NAME}.avd
|
|
target=android-${API_LEVEL}
|
|
EOF
|
|
|
|
# Write the AVD config
|
|
cat > "$AVD_DIR/config.ini" << EOF
|
|
AvdId=${AVD_NAME}
|
|
PlayStore.enabled=false
|
|
abi.type=arm64-v8a
|
|
avd.ini.displayname=${AVD_NAME}
|
|
avd.ini.encoding=UTF-8
|
|
disk.dataPartition.size=8G
|
|
hw.accelerator.isConfigured=true
|
|
hw.cpu.arch=arm64
|
|
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=Default
|
|
tag.id=default
|
|
EOF
|
|
|
|
echo " Created AVD: $AVD_NAME"
|
|
|
|
# Kill any existing emulator
|
|
$ADB_BIN emu kill 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# Start emulator
|
|
echo " Launching emulator..."
|
|
$EMULATOR_BIN -avd "$AVD_NAME" \
|
|
-no-window -no-audio -no-boot-anim \
|
|
-gpu swiftshader_indirect \
|
|
-netdelay none -netspeed full \
|
|
-memory 4096 \
|
|
-no-snapshot \
|
|
-wipe-data \
|
|
2>&1 | sed 's/^/ [emulator] /' &
|
|
EMULATOR_PID=$!
|
|
|
|
# Wait for the device to become reachable
|
|
echo " Waiting for device..."
|
|
$ADB_BIN wait-for-device
|
|
|
|
# Wait for boot_completed
|
|
echo " Waiting for boot to complete..."
|
|
BOOT_TIMEOUT=300
|
|
BOOT_ELAPSED=0
|
|
while true; do
|
|
BOOTED=$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n ' || true)
|
|
if [ "$BOOTED" = "1" ]; then
|
|
echo " Emulator is ready! (took ${BOOT_ELAPSED}s)"
|
|
break
|
|
fi
|
|
if [ $BOOT_ELAPSED -ge $BOOT_TIMEOUT ]; then
|
|
echo "ERROR: Emulator failed to boot within ${BOOT_TIMEOUT}s"
|
|
exit 1
|
|
fi
|
|
sleep 3
|
|
BOOT_ELAPSED=$((BOOT_ELAPSED + 3))
|
|
done
|
|
|
|
# Print device info
|
|
echo ""
|
|
echo " Device ABI : $($ADB_BIN shell getprop ro.product.cpu.abi | tr -d '\r')"
|
|
echo " ABI list : $($ADB_BIN shell getprop ro.product.cpu.abilist | tr -d '\r')"
|
|
echo " CPU info :"
|
|
$ADB_BIN shell 'cat /proc/cpuinfo | grep -E "^(Features|flags|processor)"' 2>/dev/null | head -4 | sed 's/^/ /'
|
|
|
|
# ── Step 5: run tests on emulator ────────────────────────────────────
|
|
echo ""
|
|
echo ">>> Step 5: Running ${#TEST_BINS[@]} unit tests on emulator..."
|
|
|
|
DEVICE_TEST_DIR="/data/local/tmp/zvec_tests"
|
|
DEVICE_LIB_DIR="$DEVICE_TEST_DIR/lib"
|
|
$ADB_BIN shell "mkdir -p $DEVICE_TEST_DIR $DEVICE_LIB_DIR" 2>/dev/null
|
|
|
|
# Push all shared libraries that tests may depend on
|
|
echo " Pushing shared libraries..."
|
|
SO_COUNT=0
|
|
while IFS= read -r so_file; do
|
|
$ADB_BIN 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 non-test helper binaries (e.g. data_generator, collection_optimizer)
|
|
# These are needed by crash_recovery tests which fork+exec them at runtime.
|
|
echo " Pushing helper binaries..."
|
|
HELPER_COUNT=0
|
|
for helper_name in data_generator collection_optimizer; do
|
|
helper_path=$(find "$BUILD_DIR" -name "$helper_name" -type f -perm +111 ! -name "*_test" 2>/dev/null | head -1)
|
|
if [ -n "$helper_path" ]; then
|
|
$ADB_BIN push "$helper_path" "$DEVICE_TEST_DIR/$helper_name" > /dev/null 2>&1
|
|
$ADB_BIN shell "chmod 755 $DEVICE_TEST_DIR/$helper_name" 2>/dev/null
|
|
HELPER_COUNT=$((HELPER_COUNT + 1))
|
|
fi
|
|
done
|
|
echo " Pushed $HELPER_COUNT helper binaries."
|
|
|
|
TOTAL=${#TEST_BINS[@]}
|
|
PASSED=0
|
|
FAILED=0
|
|
FAILED_NAMES=()
|
|
IDX=0
|
|
|
|
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
|
|
# (e.g. flat_builder_test binary vs flat_builder_test/ directory)
|
|
WORK_DIR="$DEVICE_TEST_DIR/workdir_${test_name}"
|
|
|
|
echo ""
|
|
echo "────────────────────────────────────────"
|
|
echo " [$IDX/$TOTAL] $test_name"
|
|
echo "────────────────────────────────────────"
|
|
|
|
set +e
|
|
# Create isolated working directory
|
|
$ADB_BIN 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_BIN 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 binary
|
|
$ADB_BIN push "$test_bin" "$device_path" > /dev/null 2>&1
|
|
$ADB_BIN shell "chmod 755 $device_path" 2>/dev/null
|
|
|
|
# Run test from its own working directory with LD_LIBRARY_PATH
|
|
OUTPUT=$($ADB_BIN 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 line)
|
|
echo "$OUTPUT" | grep -v 'EXIT_CODE=' | sed 's/^/ /'
|
|
|
|
if [ "$EXIT_CODE" = "0" ]; then
|
|
echo " >>> PASSED"
|
|
PASSED=$((PASSED + 1))
|
|
else
|
|
# Detect "segfault-on-exit" pattern: all gtest assertions passed but
|
|
# process crashed during teardown (common with cross-compiled gmock tests).
|
|
GTEST_PASSED_LINE=$(echo "$OUTPUT" | grep '\[ PASSED \]' | tail -1)
|
|
GTEST_FAILED_LINE=$(echo "$OUTPUT" | grep '\[ FAILED \]' | head -1)
|
|
if [ -n "$GTEST_PASSED_LINE" ] && [ -z "$GTEST_FAILED_LINE" ] && \
|
|
{ [ "$EXIT_CODE" = "139" ] || [ "$EXIT_CODE" = "134" ] || [ "$EXIT_CODE" = "135" ]; }; then
|
|
echo " >>> PASSED (with crash-on-exit, exit code: $EXIT_CODE — ignored)"
|
|
PASSED=$((PASSED + 1))
|
|
else
|
|
echo " >>> FAILED (exit code: $EXIT_CODE)"
|
|
FAILED=$((FAILED + 1))
|
|
FAILED_NAMES+=("$test_name")
|
|
fi
|
|
fi
|
|
|
|
# Clean up binary and working directory to reclaim disk space
|
|
$ADB_BIN shell "rm -rf $device_path $WORK_DIR" 2>/dev/null || true
|
|
done
|
|
|
|
# ── Summary ──────────────────────────────────────────────────────────
|
|
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!"
|