#!/usr/bin/env bash # # Rebuild Arti native libraries from official source # # This script clones the official Arti repository, applies our custom JNI wrapper, # and builds the native libraries for Android. Use this to: # - Verify the pre-built .so files match the source # - Update to a new Arti version # - Debug or modify the wrapper code # # Requirements: # - Bash 4+ (macOS default bash is 3.2; install via Homebrew: brew install bash) # - Rust toolchain with Android targets: # rustup target add aarch64-linux-android x86_64-linux-android # - cargo-ndk: cargo install cargo-ndk # - Android NDK 25+ (for 16KB page size support) # # Usage: # ./build-arti.sh # Build both architectures (debug/emulator) # ./build-arti.sh --release # Build ARM64 only (production) # ./build-arti.sh --clean # Remove cloned Arti repo and rebuild set -euo pipefail # ============================================================================== # Configuration # ============================================================================== # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Script and project directories SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" ARTI_SOURCE_DIR="$SCRIPT_DIR/.arti-source" JNILIBS_DIR="$PROJECT_ROOT/app/src/main/jniLibs" detect_default_ndk_home() { local candidates=( "$HOME/Library/Android/sdk/ndk/27.0.12077973" "$HOME/Library/Android/sdk/ndk" "$HOME/Library/Android/sdk/ndk-bundle" "$HOME/Android/Sdk/ndk/27.0.12077973" "$HOME/Android/Sdk/ndk" "$HOME/Android/Sdk/ndk-bundle" ) for candidate in "${candidates[@]}"; do if [ -d "$candidate" ]; then echo "$candidate" return fi done local base for base in "$HOME/Library/Android/sdk/ndk" "$HOME/Android/Sdk/ndk"; do if [ -d "$base" ]; then local latest latest="$(find "$base" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort | tail -1)" if [ -n "$latest" ]; then echo "$latest" return fi fi done echo "" } # Read pinned version if [ ! -f "$SCRIPT_DIR/ARTI_VERSION" ]; then echo -e "${RED}Error: ARTI_VERSION file not found${NC}" exit 1 fi VERSION="$(tr -d '[:space:]' < "$SCRIPT_DIR/ARTI_VERSION")" # Android NDK path if [ -z "${ANDROID_NDK_HOME:-}" ]; then AUTO_NDK_HOME="$(detect_default_ndk_home)" if [ -n "$AUTO_NDK_HOME" ]; then ANDROID_NDK_HOME="$AUTO_NDK_HOME" fi fi if [ -z "${ANDROID_NDK_HOME:-}" ]; then echo -e "${RED}Error: ANDROID_NDK_HOME is not set and automatic detection failed.${NC}" echo "Set ANDROID_NDK_HOME to your NDK installation (e.g., ~/Android/Sdk/ndk/)." exit 1 fi export ANDROID_NDK_HOME # Min SDK version (must match bitchat-android minSdk) MIN_SDK_VERSION=26 # Parse arguments RELEASE_ONLY=false CLEAN_BUILD=false while [[ $# -gt 0 ]]; do case "$1" in --release) RELEASE_ONLY=true shift ;; --clean) CLEAN_BUILD=true shift ;; --help|-h) echo "Usage: $0 [--release] [--clean]" echo "" echo "Options:" echo " --release Build ARM64 only (smaller, for production)" echo " --clean Remove cached Arti source and rebuild from scratch" echo "" exit 0 ;; *) echo -e "${RED}Error: Unknown argument: $1${NC}" echo "Run: $0 --help" exit 1 ;; esac done # Architectures to build if [ "$RELEASE_ONLY" = true ]; then TARGETS=("aarch64-linux-android" "armv7-linux-androideabi") else TARGETS=("aarch64-linux-android" "x86_64-linux-android" "armv7-linux-androideabi" "i686-linux-android") fi # Map Rust targets to Android ABI names declare -A ABI_MAP=( ["aarch64-linux-android"]="arm64-v8a" ["x86_64-linux-android"]="x86_64" ["armv7-linux-androideabi"]="armeabi-v7a" ["i686-linux-android"]="x86" ) # Toolchain placeholders (set in detect_ndk_host) NDK_HOST="" NDK_LLVM_BIN="" LLVM_STRIP="" LLVM_NM="" LLVM_READELF="" # ============================================================================== # Functions # ============================================================================== print_header() { echo -e "${BLUE}=========================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}=========================================${NC}" } print_success() { echo -e "${GREEN}$1${NC}"; } print_error() { echo -e "${RED}$1${NC}"; } print_info() { echo -e "${YELLOW}$1${NC}"; } detect_ndk_host() { local uname_s uname_s="$(uname -s)" local PREBUILT_DIR="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt" local HOST_CANDIDATES=() case "$uname_s" in Darwin) HOST_CANDIDATES=("darwin-arm64" "darwin-x86_64") ;; Linux) HOST_CANDIDATES=("linux-x86_64" "linux-arm64" "linux-aarch64") ;; *) print_error "Unsupported host OS: $uname_s" exit 1 ;; esac for candidate in "${HOST_CANDIDATES[@]}"; do if [ -d "$PREBUILT_DIR/$candidate" ]; then NDK_HOST="$candidate" NDK_LLVM_BIN="$PREBUILT_DIR/$candidate/bin" LLVM_STRIP="$NDK_LLVM_BIN/llvm-strip" LLVM_NM="$NDK_LLVM_BIN/llvm-nm" LLVM_READELF="$NDK_LLVM_BIN/llvm-readelf" return 0 fi done print_error "No compatible NDK toolchain found under $PREBUILT_DIR" print_info "Searched for: ${HOST_CANDIDATES[*]}" exit 1 } check_prerequisites() { print_header "Checking Prerequisites" # Bash version if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then print_error "Bash 4+ is required. macOS ships bash 3.2 by default." print_info "macOS: brew install bash" print_info "Linux: use your distro package manager (e.g., sudo apt install bash)" print_info "Then run with the installed bash (e.g., /opt/homebrew/bin/bash ./build-arti.sh)" exit 1 fi print_success "Bash found: $BASH_VERSION" # Git if ! command -v git >/dev/null 2>&1; then print_error "git is not installed." exit 1 fi print_success "git found: $(git --version)" # Rust if ! command -v rustc >/dev/null 2>&1; then print_error "Rust is not installed. Install from https://rustup.rs/" exit 1 fi print_success "Rust found: $(rustc --version)" # rustup if ! command -v rustup >/dev/null 2>&1; then print_error "rustup is required (for managing targets). Install from https://rustup.rs/" exit 1 fi print_success "rustup found: $(rustup --version | head -1)" # cargo-ndk if ! command -v cargo-ndk >/dev/null 2>&1; then print_error "cargo-ndk is not installed. Run: cargo install cargo-ndk" exit 1 fi print_success "cargo-ndk found: $(cargo-ndk --version 2>/dev/null || echo 'installed')" # NDK if [ ! -d "$ANDROID_NDK_HOME" ]; then print_error "Android NDK not found at: $ANDROID_NDK_HOME" print_info "Set ANDROID_NDK_HOME environment variable to your NDK location" exit 1 fi print_success "Android NDK found: $ANDROID_NDK_HOME" # NDK version (should be 25+) NDK_VERSION="$(basename "$ANDROID_NDK_HOME" | cut -d'.' -f1)" if ! [[ "$NDK_VERSION" =~ ^[0-9]+$ ]]; then print_error "Could not parse NDK version from ANDROID_NDK_HOME: $ANDROID_NDK_HOME" exit 1 fi if [ "$NDK_VERSION" -lt 25 ]; then print_error "NDK version $NDK_VERSION is too old. NDK 25+ required for 16KB page size support" exit 1 fi print_success "NDK version: $NDK_VERSION (supports 16KB page size)" detect_ndk_host if [ ! -d "$NDK_LLVM_BIN" ]; then print_error "NDK LLVM toolchain bin directory not found: $NDK_LLVM_BIN" exit 1 fi print_success "NDK host tag: $NDK_HOST" if [ ! -x "$LLVM_STRIP" ]; then print_info "llvm-strip not found at: $LLVM_STRIP (stripping will be skipped)" else print_success "llvm-strip found" fi if [ ! -x "$LLVM_NM" ] && ! command -v nm >/dev/null 2>&1; then print_error "Neither llvm-nm nor nm found. Cannot verify JNI symbols." exit 1 fi if [ -x "$LLVM_NM" ]; then print_success "llvm-nm found" else print_info "llvm-nm not found, will fall back to system nm" fi if [ ! -x "$LLVM_READELF" ] && ! command -v readelf >/dev/null 2>&1; then print_info "Neither llvm-readelf nor readelf found. Alignment verification may be skipped." else print_success "readelf capability available" fi # Android targets for TARGET in "${TARGETS[@]}"; do if ! rustup target list --installed | grep -qx "$TARGET"; then print_error "Rust target $TARGET not installed" print_info "Run: rustup target add $TARGET" exit 1 fi done print_success "All Rust Android targets installed" echo "" } clone_or_update_arti() { print_header "Setting up Arti Source (version: $VERSION)" if [ "$CLEAN_BUILD" = true ] && [ -d "$ARTI_SOURCE_DIR" ]; then print_info "Cleaning existing Arti source..." rm -rf "$ARTI_SOURCE_DIR" fi if [ ! -d "$ARTI_SOURCE_DIR" ]; then print_info "Cloning official Arti repository..." git clone https://gitlab.torproject.org/tpo/core/arti.git "$ARTI_SOURCE_DIR" else print_info "Using cached Arti source at $ARTI_SOURCE_DIR" fi cd "$ARTI_SOURCE_DIR" print_info "Fetching tags..." git fetch --tags --quiet print_info "Checking out version: $VERSION" git checkout "$VERSION" --quiet 2>/dev/null || { print_error "Version $VERSION not found. Available versions:" git tag | grep "^arti-v" | tail -10 exit 1 } # Ensure clean working tree to avoid cached modifications influencing builds print_info "Resetting repository state (hard) and cleaning untracked files..." git reset --hard --quiet git clean -ffdqx --quiet print_success "Arti source ready at version $VERSION" # Apply patches if [ -d "$SCRIPT_DIR/patches" ]; then print_info "Applying patches..." for patch in "$SCRIPT_DIR/patches"/*.patch; do if [ -f "$patch" ]; then print_info "Applying $(basename "$patch")" git apply "$patch" || { print_error "Failed to apply $patch"; exit 1; } fi done fi echo "" } setup_wrapper() { print_header "Setting up JNI Wrapper" WRAPPER_DIR="$ARTI_SOURCE_DIR/arti-android-wrapper" # Recreate wrapper directory to avoid stale files rm -rf "$WRAPPER_DIR" mkdir -p "$WRAPPER_DIR/src" cp "$SCRIPT_DIR/src/lib.rs" "$WRAPPER_DIR/src/" cp "$SCRIPT_DIR/Cargo.toml" "$WRAPPER_DIR/" print_success "Wrapper files copied to $WRAPPER_DIR" echo "" } build_for_target() { local TARGET="$1" local ABI="${ABI_MAP[$TARGET]}" local OUTPUT_PATH="$JNILIBS_DIR/$ABI" print_header "Building for $ABI ($TARGET)" mkdir -p "$OUTPUT_PATH" print_info "Building Arti Android wrapper..." cargo ndk \ -t "$TARGET" \ --platform "$MIN_SDK_VERSION" \ -o "$OUTPUT_PATH" \ build --release \ --locked \ --manifest-path "$ARTI_SOURCE_DIR/arti-android-wrapper/Cargo.toml" local LIB_NAME="libarti_android.so" local NESTED_PATH="$OUTPUT_PATH/$ABI/$LIB_NAME" if [ -f "$NESTED_PATH" ]; then mv "$NESTED_PATH" "$OUTPUT_PATH/$LIB_NAME" rmdir "$OUTPUT_PATH/$ABI" 2>/dev/null || true fi if [ -f "$OUTPUT_PATH/$LIB_NAME" ]; then print_success "Built: $OUTPUT_PATH/$LIB_NAME" # Strip debug symbols safely print_info "Stripping debug symbols..." if [ -x "$LLVM_STRIP" ]; then "$LLVM_STRIP" --strip-debug "$OUTPUT_PATH/$LIB_NAME" 2>/dev/null || true print_success "Stripped debug symbols" else print_info "Skipping strip (llvm-strip not available)" fi local SIZE SIZE="$(du -h "$OUTPUT_PATH/$LIB_NAME" | cut -f1)" print_success "Final size: $SIZE" # Verify 16KB page size alignment (best-effort) print_info "Verifying 16KB page alignment..." local READELF_TOOL="" if [ -x "$LLVM_READELF" ]; then READELF_TOOL="$LLVM_READELF" elif command -v readelf >/dev/null 2>&1; then READELF_TOOL="$(command -v readelf)" fi if [ -n "$READELF_TOOL" ]; then local ALIGNMENT ALIGNMENT="$("$READELF_TOOL" -l "$OUTPUT_PATH/$LIB_NAME" 2>/dev/null | grep "LOAD" | head -1 | awk '{print $NF}' || echo "unknown")" if [ "$ALIGNMENT" = "0x4000" ] || [ "$ALIGNMENT" = "16384" ]; then print_success "16KB page alignment verified: $ALIGNMENT" else print_info "Page alignment: $ALIGNMENT (NDK handles 16KB at link time)" fi else print_info "Skipping alignment check (readelf not available)" fi else print_error "Build failed: $LIB_NAME not found" return 1 fi echo "" } verify_jni_symbols_for_lib() { local LIB_PATH="$1" if [ ! -f "$LIB_PATH" ]; then print_error "Library not found: $LIB_PATH" return 1 fi local EXPECTED_SYMBOLS=( "Java_org_torproject_arti_ArtiNative_getVersion" "Java_org_torproject_arti_ArtiNative_setLogCallback" "Java_org_torproject_arti_ArtiNative_initialize" "Java_org_torproject_arti_ArtiNative_startSocksProxy" "Java_org_torproject_arti_ArtiNative_stop" ) local ALL_FOUND=true for SYMBOL in "${EXPECTED_SYMBOLS[@]}"; do local FOUND=false if [ -x "$LLVM_NM" ]; then if "$LLVM_NM" -D --defined-only "$LIB_PATH" 2>/dev/null | grep -q "$SYMBOL"; then FOUND=true fi elif command -v nm >/dev/null 2>&1; then # Fallback (may be unreliable for ELF on macOS) if nm -g "$LIB_PATH" 2>/dev/null | grep -q "$SYMBOL"; then FOUND=true fi fi if [ "$FOUND" = true ]; then print_success " Found: $SYMBOL" else print_error " Missing: $SYMBOL" ALL_FOUND=false fi done if [ "$ALL_FOUND" = true ]; then print_success "All JNI symbols verified for: $LIB_PATH" else print_error "Some JNI symbols are missing for: $LIB_PATH" return 1 fi return 0 } verify_jni_symbols() { print_header "Verifying JNI Symbols" print_info "Checking exported JNI symbols..." local FAILED=false for TARGET in "${TARGETS[@]}"; do local ABI="${ABI_MAP[$TARGET]}" local LIB_PATH="$JNILIBS_DIR/$ABI/libarti_android.so" print_info "Verifying $ABI: $LIB_PATH" if ! verify_jni_symbols_for_lib "$LIB_PATH"; then FAILED=true fi done if [ "$FAILED" = true ]; then return 1 fi echo "" } show_summary() { print_header "Build Complete!" echo -e "${GREEN}Built libraries:${NC}" for TARGET in "${TARGETS[@]}"; do local ABI="${ABI_MAP[$TARGET]}" local LIB_PATH="$JNILIBS_DIR/$ABI/libarti_android.so" if [ -f "$LIB_PATH" ]; then local SIZE SIZE="$(du -h "$LIB_PATH" | cut -f1)" echo -e " ${GREEN}$ABI:${NC} $SIZE" fi done echo "" echo -e "${GREEN}Arti version:${NC} $VERSION" echo -e "${GREEN}Source:${NC} https://gitlab.torproject.org/tpo/core/arti" echo "" echo -e "${GREEN}Next steps:${NC}" echo " 1. Test the build: ./gradlew assembleDebug" echo " 2. Commit the .so files: git add app/src/main/jniLibs/" echo "" echo -e "${GREEN}To update Arti version:${NC}" echo " 1. Edit ARTI_VERSION with new version tag (e.g., arti-v1.8.0)" echo " 2. Run: ./build-arti.sh --clean" echo "" } # ============================================================================== # Main # ============================================================================== main() { print_header "Arti Android Build Script" echo -e "${BLUE}Building Arti for Android with 16KB page size support${NC}" echo -e "${BLUE}Version: $VERSION${NC}" echo -e "${BLUE}Architectures: ${TARGETS[*]}${NC}" echo "" check_prerequisites clone_or_update_arti setup_wrapper ensure_wrapper_lockfile for TARGET in "${TARGETS[@]}"; do build_for_target "$TARGET" done verify_jni_symbols show_summary } ensure_wrapper_lockfile() { print_header "Ensuring wrapper Cargo.lock exists" local WRAPPER_DIR="$ARTI_SOURCE_DIR/arti-android-wrapper" local LOCKFILE="$WRAPPER_DIR/Cargo.lock" if [ -f "$LOCKFILE" ]; then print_success "Cargo.lock already exists" echo "" return 0 fi print_info "Cargo.lock missing; generating it once (network access may be required)..." (cd "$WRAPPER_DIR" && cargo generate-lockfile) if [ ! -f "$LOCKFILE" ]; then print_error "Failed to generate Cargo.lock at $LOCKFILE" return 1 fi print_success "Generated Cargo.lock" echo "" } main