feat: add product flavors and TorProvider abstraction (#508)

* feat: add product flavors and TorProvider abstraction

Introduces build flavors to separate Tor functionality from the standard build, reducing APK size for users who don't need Tor.

- Creates `standard` and `tor` product flavors.
- The `standard` flavor is the default, lightweight build.
- The `tor` flavor includes the Arti (Tor) dependency and is identified by the `.tor` application ID suffix.
- Adds a `TorProvider` interface and a `TorProviderFactory` to abstract Tor implementation details between flavors.

Prepares architecture for optional Tor support to reduce APK size
from 142MB to ~4-5MB for standard builds

Related to #454

* Refactor: implement StandardTorProvider and RealTorProvider

This commit refactors the Tor integration by introducing a `TorProvider` interface and creating separate implementations for 'tor' and 'standard' product flavors.

- The original `TorManager` singleton has been moved into `RealTorProvider` for the 'tor' flavor.
- A no-op `StandardTorProvider` is introduced for the 'standard' flavor, which reports Tor as unavailable.
- A `TorProviderFactory` is used to create the appropriate provider at runtime based on the build variant.

* refactor: migrate to TorProvider abstraction

Replaced direct calls to the static `TorManager` with an instance obtained from `TorProviderFactory`.

This change allows for different Tor implementations based on build flavors, improving modularity and abstracting the Tor provider logic.

Updated `BitchatApplication`, `ChatHeader`, `OkHttpProvider`, and `AboutSheet` to use the new factory pattern for accessing Tor functionalities.

* build: add flavor-specific ProGuard rules and CI/CD

 - Split ProGuard rules: base, standard-specific, tor-specific
  - Move Arti/Guardian Project rules to proguard-tor.pro
  - Update CI/CD workflow to build both flavors
  - Add separate artifact uploads for each flavor
  - Add descriptive release notes template

  CI now builds both standard (~4-5MB) and tor (~140MB) APKs."

  resolves #454

* refactor: centralize network reset logic

Extracts the repeated network connection reset logic into a new private function `resetNetworkConnections()`.

This change also replaces direct `_statusFlow.value = ...` assignments with the safer `_statusFlow.update { ... }` function to prevent race conditions.

* ci: run separate build steps for flavors

* feat: disable Tor toggle if not available in build

* ci: parallelize builds and add conditional tor lint

Optimizes CI workflow:
- Parallel matrix builds (4 runners instead of sequential)
- Conditional tor lint only when app/src/tor/ changes in PRs
- Merged test+lint jobs to reduce setup overhead

Reduces CI time by ~50% (8-11 min vs 18-26 min)

* refactor: Unify Tor implementation and remove build flavors

This commit refactors the Tor integration by removing the `standard` and `tor` product flavors in favor of a single, unified build that always includes a custom-built Arti (Tor) library.

Key changes include:
*   **Removed Build Flavors:** Deleted the `standard` and `tor` product flavors from `build.gradle.kts`, simplifying the build process and CI configuration.
*   **Unified Tor Manager:** Replaced the `TorProvider` interface and flavor-specific implementations (`StandardTorProvider`, `RealTorProvider`) with a new singleton, `ArtiTorManager`. This class now manages the Arti lifecycle for all builds.
*   **Custom Arti Wrapper:** Introduced a new `ArtiProxy` class to provide a compatible API wrapper around the custom-built native Arti library (`libarti_android.so`). This replaces the dependency on the external `arti-mobile-ex` library.
*   **Updated Proguard:** Consolidated and updated Proguard rules into the main `proguard-rules.pro` file to keep the necessary `ArtiTorManager` and native library classes.
*   **CI/CD Simplification:** Updated GitHub Actions workflows (`release.yml`, `android-build.yml`) to build and release a single APK instead of separate ones for each flavor.

* build: Configure ABI filters for debug and release builds

For debug builds, include `x86_64` to support emulators.

For release builds, only include `arm64-v8a` to minimize the final APK size.

* feat: Ignore jniLibs directory

This change adds the `app/src/main/jniLibs/` directory to the `.gitignore` file to prevent native libraries from being committed to the repository.

* feat: Refine .gitignore for Arti build artifacts

Improves the `.gitignore` file by:
- Ignoring all `build/` directories except for `tools/arti-build/`.
- Adding specific ignores for Arti build artifacts, including the cloned source repository and the Rust build cache directory.

* feat: Update arti android native library

* feat: add build script and JNI wrapper for Arti

Adds a comprehensive build system for creating custom Arti (Tor in Rust) shared libraries for Android. This replaces the dependency on external, outdated AARs with a fully transparent and reproducible build process.

Key changes:
- Introduces `build-arti.sh`, a script to clone the official Arti repository, apply a JNI wrapper, and build `.so` files for Android.
- Adds `ARTI_VERSION` to pin the build to a specific Arti release (v1.7.0).
- Implements a new Rust JNI wrapper (`src/lib.rs`) that exposes core functions like `initialize`, `startSocksProxy`, and `stop` to the Android app.
- Includes a `Cargo.toml` with release profile optimizations for size (`lto`, `strip`, `opt-level = "z"`).
- Provides detailed documentation in `README.md` explaining the build process, prerequisites, and architecture.

* build script for mac

* consolidate both scripts

* improve script

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
Moe Hamade
2025-12-12 23:27:52 +07:00
committed by GitHub
co-authored by callebtc
parent a174ac5185
commit b2febcee88
22 changed files with 1981 additions and 226 deletions
+1
View File
@@ -0,0 +1 @@
arti-v1.7.0
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "arti-android-wrapper"
version = "1.7.0"
edition = "2021"
[workspace]
# Empty workspace table to exclude from parent workspace
[lib]
crate-type = ["cdylib"]
name = "arti_android"
[dependencies]
arti-client = { path = "../crates/arti-client", default-features = false, features = ["tokio", "rustls", "compression", "bridge-client", "onion-service-client", "static-sqlite"] }
tor-rtcompat = { path = "../crates/tor-rtcompat", features = ["tokio", "rustls"] }
jni = "0.21"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
panic = "abort" # Smaller panic handler
+223
View File
@@ -0,0 +1,223 @@
# Arti Android Build Tools
This directory contains the build scripts and source files for compiling the custom Arti (Tor in Rust) library for Android.
## Overview
bitchat-android uses a custom-built Arti library instead of Guardian Project's outdated `arti-mobile-ex` AAR. This provides:
- **Smaller APK size**: ~11MB total vs ~140MB with Guardian Project AAR (28x reduction)
- **Latest Arti version**: Currently v1.7.0 with pure Rust TLS (rustls)
- **16KB page size support**: Required for Google Play (Nov 2025)
- **Full transparency**: Build from official Arti source + our JNI wrapper
## Quick Start
The pre-built `.so` files are committed to the repo, so you don't need to build unless you want to:
1. **Verify the binaries** match the source
2. **Update to a new Arti version**
3. **Modify the JNI wrapper**
## Directory Structure
```
tools/arti-build/
├── README.md # This file
├── build-arti.sh # Main build script (clones Arti, builds .so files)
├── ARTI_VERSION # Pinned Arti version tag (e.g., arti-v1.7.0)
├── Cargo.toml # Rust package configuration
├── src/
│ └── lib.rs # JNI wrapper (Rust -> Kotlin/Java bridge)
└── .arti-source/ # [GITIGNORED] Cloned official Arti repo
app/src/main/jniLibs/ # [COMMITTED] Pre-built native libraries
├── arm64-v8a/
│ └── libarti_android.so (~5.3MB)
└── x86_64/
└── libarti_android.so (~6.2MB)
```
## Rebuilding from Source
### Prerequisites
1. **Rust toolchain** with Android targets:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add aarch64-linux-android x86_64-linux-android
```
2. **cargo-ndk** for Android cross-compilation:
```bash
cargo install cargo-ndk
```
3. **Android NDK 25+** (for 16KB page size support):
```bash
# Via Android Studio SDK Manager, or:
$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager "ndk;27.0.12077973"
# Set environment variable
export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/27.0.12077973"
```
### Build Commands
```bash
cd tools/arti-build
# Build both architectures (arm64 + x86_64 for emulator)
./build-arti.sh
# Build ARM64 only (smaller, for production releases)
./build-arti.sh --release
# Clean rebuild (re-clone Arti source)
./build-arti.sh --clean
```
The script will:
1. Clone official Arti from https://gitlab.torproject.org/tpo/core/arti
2. Checkout the version specified in `ARTI_VERSION`
3. Copy our JNI wrapper into the cloned repo
4. Build with `cargo ndk`
5. Copy `.so` files to `app/src/main/jniLibs/`
### Verification
After building, verify the libraries:
```bash
# Check file sizes
ls -lh ../../app/src/main/jniLibs/*/libarti_android.so
# Verify JNI symbols are exported
nm -gU ../../app/src/main/jniLibs/arm64-v8a/libarti_android.so | grep Java_org_torproject
# Verify 16KB page alignment
readelf -l ../../app/src/main/jniLibs/arm64-v8a/libarti_android.so | grep LOAD
# Look for: Align 0x4000 (16KB)
```
## Updating Arti Version
1. **Check available versions**:
```bash
git ls-remote --tags https://gitlab.torproject.org/tpo/core/arti.git | grep arti-v
```
2. **Update the version file**:
```bash
echo "arti-v1.8.0" > ARTI_VERSION
```
3. **Rebuild from scratch**:
```bash
./build-arti.sh --clean
```
4. **Test the build**:
```bash
cd ../..
./gradlew clean assembleDebug
./gradlew installDebug
# Enable Tor in app and verify it works
```
5. **Commit the new libraries**:
```bash
git add app/src/main/jniLibs/ tools/arti-build/ARTI_VERSION
git commit -m "chore: update Arti to v1.8.0"
```
## JNI Wrapper Architecture
The `src/lib.rs` file implements a JNI bridge between Kotlin and Rust:
```
Kotlin (ArtiNative.kt)
↓ JNI
Rust (lib.rs)
Arti Client (TorClient)
SOCKS5 Proxy (localhost:9060)
```
**Exported JNI Functions**:
- `getVersion()` - Returns Arti version string
- `setLogCallback(callback)` - Registers log listener for bootstrap progress
- `initialize(dataDir)` - Creates Tokio runtime and TorClient
- `startSocksProxy(port)` - Starts SOCKS5 proxy on specified port
- `stop()` - Stops SOCKS proxy (TorClient is reused)
**Key Design Decisions**:
- Global `TorClient` persists across stop/start cycles (fixes Nov 2024 toggle bug)
- Tokio runtime created once and never destroyed
- Log messages bridged to Java via `GlobalRef` callback
## Feature Configuration
Edit `Cargo.toml` to customize Arti features:
```toml
[dependencies]
arti-client = {
path = "../crates/arti-client",
default-features = false,
features = [
"tokio", # Required: async runtime
"rustls", # Required: pure Rust TLS (no OpenSSL)
"compression", # Optional: directory compression
"bridge-client", # Optional: Tor bridge support
"onion-service-client", # Optional: .onion site support
"static-sqlite" # Required: bundled SQLite
]
}
```
## Size Comparison
| Configuration | arm64-v8a | x86_64 | Total | APK Size |
|---------------|-----------|--------|-------|----------|
| Guardian Project AAR | - | - | ~140 MB | ~150 MB |
| Custom (both arch) | 5.3 MB | 6.2 MB | 11.5 MB | ~15 MB |
| Custom (ARM-only) | 5.3 MB | - | 5.3 MB | ~10 MB |
**28x size reduction** vs Guardian Project implementation.
## Troubleshooting
### "cargo-ndk not found"
```bash
cargo install cargo-ndk
```
### "Android NDK not found"
```bash
export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/27.0.12077973"
```
### "Rust target not installed"
```bash
rustup target add aarch64-linux-android x86_64-linux-android
```
### "Version not found"
Check available versions:
```bash
git ls-remote --tags https://gitlab.torproject.org/tpo/core/arti.git | grep arti-v | tail -10
```
### Library too large
1. Ensure building with `--release` flag
2. Verify `strip = true` in `Cargo.toml` `[profile.release]`
3. Consider removing optional features
## References
- [Arti Documentation](https://gitlab.torproject.org/tpo/core/arti/-/blob/main/doc/README.md)
- [cargo-ndk](https://github.com/bbqsrc/cargo-ndk)
- [Android NDK Guide](https://developer.android.com/ndk/guides)
- [Google Play 16KB Page Size](https://developer.android.com/guide/practices/page-sizes)
+570
View File
@@ -0,0 +1,570 @@
#!/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/<version>)."
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")
else
TARGETS=("aarch64-linux-android" "x86_64-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"
)
# 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"
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
+493
View File
@@ -0,0 +1,493 @@
use jni::JNIEnv;
use jni::objects::{JClass, JString, JObject, GlobalRef};
use jni::sys::{jint, jstring};
use jni::JavaVM;
use arti_client::TorClient;
use arti_client::config::TorClientConfigBuilder;
use tor_rtcompat::PreferredRuntime;
use std::sync::{Arc, Mutex, Once};
use std::path::PathBuf;
use anyhow::Result;
// ============================================================================
// Global State
// ============================================================================
/// Global Arti client instance
static ARTI_CLIENT: Mutex<Option<Arc<TorClient<PreferredRuntime>>>> = Mutex::new(None);
/// Global Tokio runtime (must persist for Arti to work)
static TOKIO_RUNTIME: Mutex<Option<tokio::runtime::Runtime>> = Mutex::new(None);
/// Global JavaVM reference (cached on first JNI call)
static JAVA_VM: Mutex<Option<JavaVM>> = Mutex::new(None);
/// Global log callback reference
static LOG_CALLBACK: Mutex<Option<GlobalRef>> = Mutex::new(None);
/// Handle to SOCKS server task (for graceful shutdown)
static SOCKS_TASK: Mutex<Option<tokio::task::JoinHandle<()>>> = Mutex::new(None);
/// Initialization flag
static INIT_ONCE: Once = Once::new();
// ============================================================================
// Logging Integration
// ============================================================================
/// Send log message to Java callback
fn send_log_to_java(message: String) {
let vm_opt = JAVA_VM.lock().unwrap();
let callback_opt = LOG_CALLBACK.lock().unwrap();
if let (Some(vm), Some(callback)) = (vm_opt.as_ref(), callback_opt.as_ref()) {
if let Ok(mut env) = vm.attach_current_thread() {
if let Ok(jmessage) = env.new_string(&message) {
let _ = env.call_method(
callback.as_obj(),
"onLogLine",
"(Ljava/lang/String;)V",
&[(&jmessage).into()]
);
}
}
}
}
/// Macro for logging to both Android logcat and Java callback
macro_rules! log_info {
($($arg:tt)*) => {{
let msg = format!($($arg)*);
android_logger::log(&format!("Arti: {}", msg));
send_log_to_java(msg);
}};
}
macro_rules! log_error {
($($arg:tt)*) => {{
let msg = format!("ERROR: {}", format!($($arg)*));
android_logger::log(&format!("Arti: {}", msg));
send_log_to_java(msg);
}};
}
// ============================================================================
// JNI Functions
// ============================================================================
/// Get Arti version string
#[no_mangle]
pub extern "C" fn Java_org_torproject_arti_ArtiNative_getVersion(
env: JNIEnv,
_class: JClass,
) -> jstring {
// Cache JavaVM on first call
if JAVA_VM.lock().unwrap().is_none() {
if let Ok(vm) = env.get_java_vm() {
*JAVA_VM.lock().unwrap() = Some(vm);
}
}
let version = format!("Arti {} (custom build with rustls)", env!("CARGO_PKG_VERSION"));
let output = env.new_string(version).expect("Couldn't create java string!");
output.into_raw()
}
/// Set log callback for Arti logs
#[no_mangle]
pub extern "C" fn Java_org_torproject_arti_ArtiNative_setLogCallback(
env: JNIEnv,
_class: JClass,
callback: JObject,
) {
// Cache JavaVM if not already cached
if JAVA_VM.lock().unwrap().is_none() {
if let Ok(vm) = env.get_java_vm() {
*JAVA_VM.lock().unwrap() = Some(vm);
}
}
// Store global reference to callback
if let Ok(global_ref) = env.new_global_ref(callback) {
*LOG_CALLBACK.lock().unwrap() = Some(global_ref);
log_info!("Log callback registered");
}
}
/// Initialize Arti runtime
#[no_mangle]
pub extern "C" fn Java_org_torproject_arti_ArtiNative_initialize(
mut env: JNIEnv,
_class: JClass,
data_dir: JString,
) -> jint {
// Cache JavaVM if not already cached
if JAVA_VM.lock().unwrap().is_none() {
if let Ok(vm) = env.get_java_vm() {
*JAVA_VM.lock().unwrap() = Some(vm);
}
}
let data_dir_str: String = match env.get_string(&data_dir) {
Ok(s) => s.into(),
Err(e) => {
log_error!("Failed to convert data_dir: {:?}", e);
return -1;
}
};
log_info!("AMEx: state changed to Initialized");
log_info!("Initializing Arti with data directory: {}", data_dir_str);
// Initialize Tokio runtime (once)
INIT_ONCE.call_once(|| {
match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => {
log_info!("Tokio runtime created successfully");
*TOKIO_RUNTIME.lock().unwrap() = Some(rt);
}
Err(e) => {
log_error!("Failed to create Tokio runtime: {:?}", e);
}
}
});
// Check if runtime exists
let runtime_guard = TOKIO_RUNTIME.lock().unwrap();
let runtime = match runtime_guard.as_ref() {
Some(rt) => rt,
None => {
log_error!("Tokio runtime not initialized");
return -2;
}
};
// Create config with explicit Android paths
let data_path = PathBuf::from(data_dir_str);
let cache_dir = data_path.join("cache");
let state_dir = data_path.join("state");
// Create directories if they don't exist
std::fs::create_dir_all(&cache_dir).ok();
std::fs::create_dir_all(&state_dir).ok();
let result: Result<()> = runtime.block_on(async {
log_info!("Creating Arti client...");
log_info!("Cache dir: {:?}", cache_dir);
log_info!("State dir: {:?}", state_dir);
// Create config with Android-specific directories
let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
.build()?;
// Create client with Android-specific config
let client = TorClient::create_bootstrapped(config).await?;
log_info!("Arti client created successfully");
// Store client globally
*ARTI_CLIENT.lock().unwrap() = Some(Arc::new(client));
Ok(())
});
match result {
Ok(_) => {
log_info!("Arti initialized successfully");
0
}
Err(e) => {
log_error!("Failed to initialize Arti: {:?}", e);
-3
}
}
}
/// Start SOCKS proxy on specified port
#[no_mangle]
pub extern "C" fn Java_org_torproject_arti_ArtiNative_startSocksProxy(
_env: JNIEnv,
_class: JClass,
port: jint,
) -> jint {
log_info!("AMEx: state changed to Starting");
log_info!("Starting SOCKS proxy on port {}", port);
// Stop any existing SOCKS server first
if let Some(handle) = SOCKS_TASK.lock().unwrap().take() {
log_info!("Aborting previous SOCKS server task");
handle.abort();
}
let client_guard = ARTI_CLIENT.lock().unwrap();
let client = match client_guard.as_ref() {
Some(c) => Arc::clone(c),
None => {
log_error!("Arti client not initialized - call initialize() first");
return -1;
}
};
drop(client_guard);
let runtime_guard = TOKIO_RUNTIME.lock().unwrap();
let runtime = match runtime_guard.as_ref() {
Some(rt) => rt,
None => {
log_error!("Tokio runtime not initialized");
return -2;
}
};
// Try to bind IMMEDIATELY to detect port conflicts before returning
let addr = format!("127.0.0.1:{}", port);
// Use block_on to synchronously attempt binding
let bind_result = runtime.block_on(async {
tokio::net::TcpListener::bind(&addr).await
});
let listener = match bind_result {
Ok(l) => {
log_info!("SOCKS proxy bound to {}", addr);
l
}
Err(e) => {
log_error!("Failed to bind SOCKS proxy to {}: {:?}", addr, e);
return -3;
}
};
// Now spawn the background task with the already-bound listener
let handle = runtime.spawn(async move {
log_info!("SOCKS proxy listening on {}", addr);
log_info!("Sufficiently bootstrapped; system SOCKS now functional");
// Signal bootstrap completion to bitchat-android (expected by ArtiTorManager)
// This sets bootstrapPercent to 100% and stops inactivity restarts
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
log_info!("We have found that guard [scrubbed] is usable.");
// Accept connections
loop {
match listener.accept().await {
Ok((stream, peer_addr)) => {
log_info!("SOCKS connection from: {}", peer_addr);
let client_clone = Arc::clone(&client);
tokio::spawn(async move {
if let Err(e) = handle_socks_connection(stream, client_clone).await {
log_error!("SOCKS connection error: {:?}", e);
}
});
}
Err(e) => {
log_error!("Failed to accept SOCKS connection: {:?}", e);
break; // Exit loop on error
}
}
}
log_info!("SOCKS proxy task exiting");
});
// Store handle for cleanup
*SOCKS_TASK.lock().unwrap() = Some(handle);
log_info!("SOCKS proxy started on port {}", port);
0
}
/// Handle a single SOCKS connection
async fn handle_socks_connection(
mut stream: tokio::net::TcpStream,
client: Arc<TorClient<PreferredRuntime>>,
) -> Result<()> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Simple SOCKS5 handshake
let mut buf = [0u8; 512];
// Read version + methods
let n = stream.read(&mut buf).await?;
if n < 2 {
return Err(anyhow::anyhow!("Invalid SOCKS handshake"));
}
// Send "no auth required" response
stream.write_all(&[0x05, 0x00]).await?;
// Read request
let n = stream.read(&mut buf).await?;
if n < 10 {
return Err(anyhow::anyhow!("Invalid SOCKS request"));
}
// Parse SOCKS5 request: VER(1) CMD(1) RSV(1) ATYP(1) DST.ADDR DST.PORT(2)
let version = buf[0];
let cmd = buf[1];
let atyp = buf[3];
if version != 0x05 {
return Err(anyhow::anyhow!("Unsupported SOCKS version: {}", version));
}
if cmd != 0x01 {
// Only support CONNECT command
stream.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
return Err(anyhow::anyhow!("Unsupported SOCKS command: {}", cmd));
}
// Parse target address and port
let (target_host, target_port) = match atyp {
0x01 => {
// IPv4: 4 bytes
let ip = format!("{}.{}.{}.{}", buf[4], buf[5], buf[6], buf[7]);
let port = u16::from_be_bytes([buf[8], buf[9]]);
(ip, port)
}
0x03 => {
// Domain name: length byte + domain
let len = buf[4] as usize;
if n < 5 + len + 2 {
return Err(anyhow::anyhow!("Invalid domain name length"));
}
let domain = String::from_utf8_lossy(&buf[5..5 + len]).to_string();
let port = u16::from_be_bytes([buf[5 + len], buf[5 + len + 1]]);
(domain, port)
}
0x04 => {
// IPv6: 16 bytes + 2 bytes port = 22 bytes total
if n < 22 {
stream.write_all(&[0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
return Err(anyhow::anyhow!("Truncated IPv6 request"));
}
let ip = format!(
"{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}",
buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11],
buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19]
);
let port = u16::from_be_bytes([buf[20], buf[21]]);
(ip, port)
}
_ => {
stream.write_all(&[0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
return Err(anyhow::anyhow!("Unsupported address type: {}", atyp));
}
};
log_info!("SOCKS5 CONNECT to {}:{}", target_host, target_port);
// Establish Tor connection
let tor_stream = match client.connect((target_host.as_str(), target_port)).await {
Ok(s) => s,
Err(e) => {
log_error!("Failed to connect through Tor: {:?}", e);
// Send SOCKS5 error: general failure
stream.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
return Err(e.into());
}
};
log_info!("Tor connection established to {}:{}", target_host, target_port);
// Send SOCKS5 success response
stream.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
// Bidirectional data forwarding
let (mut client_read, mut client_write) = stream.split();
let (mut tor_read, mut tor_write) = tor_stream.split();
let client_to_tor = async {
tokio::io::copy(&mut client_read, &mut tor_write).await
};
let tor_to_client = async {
tokio::io::copy(&mut tor_read, &mut client_write).await
};
// Run both directions concurrently, exit when either completes
tokio::select! {
result = client_to_tor => {
if let Err(ref e) = result {
log_error!("Client->Tor copy error: {:?}", e);
}
}
result = tor_to_client => {
if let Err(ref e) = result {
log_error!("Tor->Client copy error: {:?}", e);
}
}
};
log_info!("SOCKS connection closed for {}:{}", target_host, target_port);
Ok(())
}
/// Stop Arti and cleanup
#[no_mangle]
pub extern "C" fn Java_org_torproject_arti_ArtiNative_stop(
_env: JNIEnv,
_class: JClass,
) -> jint {
log_info!("AMEx: state changed to Stopping");
log_info!("Stopping Arti...");
// Abort SOCKS proxy task (releases the port)
if let Some(handle) = SOCKS_TASK.lock().unwrap().take() {
log_info!("Aborting SOCKS server task");
handle.abort();
}
// Give the abort a moment to complete and release the port
if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref() {
rt.block_on(async {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
});
}
// NOTE: We do NOT clear ARTI_CLIENT here!
// The TorClient can be reused for multiple SOCKS proxy start/stop cycles.
// Only clear it if you want to force full reinitialization.
// Uncomment this line only if you want to force reinitialization on every start:
// *ARTI_CLIENT.lock().unwrap() = None;
log_info!("AMEx: state changed to Stopped");
log_info!("Arti stopped successfully");
0
}
// ============================================================================
// Android Logger (simple implementation)
// ============================================================================
mod android_logger {
use std::ffi::CString;
#[allow(non_camel_case_types)]
type c_int = i32;
#[allow(non_camel_case_types)]
type c_char = i8;
extern "C" {
fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int;
}
const ANDROID_LOG_INFO: c_int = 4;
pub fn log(message: &str) {
unsafe {
let tag = CString::new("ArtiNative").unwrap();
let text = CString::new(message).unwrap();
__android_log_write(ANDROID_LOG_INFO, tag.as_ptr() as *const c_char, text.as_ptr() as *const c_char);
}
}
}