#!/bin/bash
#
# Build arti-bitchat for iOS/macOS with aggressive size optimization
#
# Output: Frameworks/arti.xcframework containing static libraries for:
# - aarch64-apple-ios (iOS device)
# - aarch64-apple-ios-sim (iOS simulator, Apple Silicon)
# - x86_64-apple-ios (iOS simulator, Intel)
# - aarch64-apple-darwin (macOS, Apple Silicon)
# - x86_64-apple-darwin (macOS, Intel)
#
set -e
# rustup/cargo install tools here on standard setups; GUI/Xcode shells often
# omit it from PATH even though `cargo` itself is available elsewhere.
export PATH="$HOME/.cargo/bin:$PATH"
RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-1.96.0}"
RUSTC_VERSION="${RUSTC_VERSION:-1.96.0}"
CBINDGEN_VERSION="${CBINDGEN_VERSION:-0.29.4}"
export RUSTC="$(rustup which --toolchain "$RUST_TOOLCHAIN" rustc)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Configuration
CRATE_NAME="arti-bitchat"
LIB_NAME="libarti_bitchat.a"
FRAMEWORK_NAME="arti"
OUTPUT_DIR="$SCRIPT_DIR/Frameworks"
# Targets to build
TARGETS=(
"aarch64-apple-ios" # iOS device
"aarch64-apple-ios-sim" # iOS simulator (Apple Silicon)
"x86_64-apple-ios" # iOS simulator (Intel)
"aarch64-apple-darwin" # macOS (Apple Silicon)
"x86_64-apple-darwin" # macOS (Intel)
)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
if ! command -v rustc &> /dev/null; then
log_error "Rust is not installed. Please install via rustup."
exit 1
fi
if ! rustup run "$RUST_TOOLCHAIN" cargo --version &> /dev/null; then
log_error "Cargo is not installed in rustup toolchain $RUST_TOOLCHAIN."
exit 1
fi
local actual_rustc
actual_rustc="$(rustup run "$RUST_TOOLCHAIN" rustc --version)"
if [[ "$actual_rustc" != "rustc $RUSTC_VERSION "* ]]; then
log_error "Expected rustc $RUSTC_VERSION in $RUST_TOOLCHAIN; found $actual_rustc."
exit 1
fi
# Check/install targets
for target in "${TARGETS[@]}"; do
if ! rustup target list --toolchain "$RUST_TOOLCHAIN" --installed | grep -q "$target"; then
log_info "Installing target: $target"
rustup target add --toolchain "$RUST_TOOLCHAIN" "$target"
fi
done
# Install cbindgen if needed
if ! command -v cbindgen &> /dev/null; then
log_info "Installing cbindgen $CBINDGEN_VERSION..."
rustup run "$RUST_TOOLCHAIN" cargo install cbindgen --version "$CBINDGEN_VERSION" --locked
fi
if [[ "$(cbindgen --version)" != "cbindgen $CBINDGEN_VERSION" ]]; then
log_error "Expected cbindgen $CBINDGEN_VERSION; found $(cbindgen --version)."
exit 1
fi
log_info "Prerequisites OK"
}
# Set up aggressive size optimization flags and deployment targets
setup_rustflags() {
local target="$1"
# Base flags for size optimization
export RUSTFLAGS="-C opt-level=z -C lto=fat -C codegen-units=1 -C panic=abort -C strip=symbols"
# Set deployment targets to suppress linker warnings about version mismatches
case "$target" in
*-apple-ios-sim*)
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
# Simulator uses iPhone SDK but needs the sim target
;;
*-apple-ios*)
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
;;
*-apple-darwin*)
export MACOSX_DEPLOYMENT_TARGET="13.0"
;;
esac
log_info "RUSTFLAGS: $RUSTFLAGS"
log_info "Deployment target: MACOSX=$MACOSX_DEPLOYMENT_TARGET IPHONEOS=$IPHONEOS_DEPLOYMENT_TARGET"
}
# Build for a single target
build_target() {
local target="$1"
log_info "Building for target: $target"
setup_rustflags "$target"
# Build release
rustup run "$RUST_TOOLCHAIN" cargo build --release --target "$target" -p "$CRATE_NAME"
# Check output
local lib_path="target/$target/release/$LIB_NAME"
if [[ -f "$lib_path" ]]; then
local size=$(du -h "$lib_path" | cut -f1)
log_info "Built $lib_path ($size)"
else
log_error "Build failed: $lib_path not found"
exit 1
fi
}
# Create xcframework from built libraries
create_xcframework() {
log_info "Creating xcframework..."
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
# Remove existing xcframework
rm -rf "$xcframework_path"
mkdir -p "$OUTPUT_DIR"
# Normalize each architecture before combining the universal slices.
for target in "${TARGETS[@]}"; do
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
if [[ -f "$lib_path" ]]; then
# Strip the library for additional size reduction
log_info "Stripping $target library..."
strip -x "$lib_path" 2>/dev/null || true
# Normalize archive metadata so repeated rebuilds are byte-stable.
local normalized_path="$lib_path.normalized"
xcrun libtool -static -D -no_warning_for_no_symbols "$lib_path" -o "$normalized_path"
mv "$normalized_path" "$lib_path"
else
log_error "Missing required library: $lib_path"
exit 1
fi
done
local simulator_dir="$SCRIPT_DIR/target/ios-universal-simulator/release"
local simulator_lib="$simulator_dir/$LIB_NAME"
mkdir -p "$simulator_dir"
xcrun lipo -create \
"$SCRIPT_DIR/target/aarch64-apple-ios-sim/release/$LIB_NAME" \
"$SCRIPT_DIR/target/x86_64-apple-ios/release/$LIB_NAME" \
-output "$simulator_lib"
# Normalize the universal archive after lipo so repeated rebuilds remain
# byte-stable just like the single-architecture inputs.
local normalized_simulator="$simulator_lib.normalized"
xcrun libtool -static -D -no_warning_for_no_symbols "$simulator_lib" -o "$normalized_simulator"
mv "$normalized_simulator" "$simulator_lib"
local macos_dir="$SCRIPT_DIR/target/macos-universal/release"
local macos_lib="$macos_dir/$LIB_NAME"
mkdir -p "$macos_dir"
xcrun lipo -create \
"$SCRIPT_DIR/target/aarch64-apple-darwin/release/$LIB_NAME" \
"$SCRIPT_DIR/target/x86_64-apple-darwin/release/$LIB_NAME" \
-output "$macos_lib"
local normalized_macos="$macos_lib.normalized"
xcrun libtool -static -D -no_warning_for_no_symbols "$macos_lib" -o "$normalized_macos"
mv "$normalized_macos" "$macos_lib"
local header_dir="$OUTPUT_DIR/include"
local cmd="xcodebuild -create-xcframework"
cmd="$cmd -library $SCRIPT_DIR/target/aarch64-apple-ios/release/$LIB_NAME -headers $header_dir"
cmd="$cmd -library $simulator_lib -headers $header_dir"
cmd="$cmd -library $macos_lib -headers $header_dir"
cmd="$cmd -output $xcframework_path"
log_info "Running: $cmd"
eval "$cmd"
if [[ -d "$xcframework_path" ]]; then
cat > "$xcframework_path/Info.plist" << 'EOF'
AvailableLibraries
BinaryPath
libarti_bitchat.a
HeadersPath
Headers
LibraryIdentifier
ios-arm64
LibraryPath
libarti_bitchat.a
SupportedArchitectures
arm64
SupportedPlatform
ios
BinaryPath
libarti_bitchat.a
HeadersPath
Headers
LibraryIdentifier
ios-arm64_x86_64-simulator
LibraryPath
libarti_bitchat.a
SupportedArchitectures
arm64
x86_64
SupportedPlatform
ios
SupportedPlatformVariant
simulator
BinaryPath
libarti_bitchat.a
HeadersPath
Headers
LibraryIdentifier
macos-arm64_x86_64
LibraryPath
libarti_bitchat.a
SupportedArchitectures
arm64
x86_64
SupportedPlatform
macos
CFBundlePackageType
XFWK
XCFrameworkFormatVersion
1.0
EOF
local size=$(du -sh "$xcframework_path" | cut -f1)
log_info "Created $xcframework_path ($size)"
else
log_error "Failed to create xcframework"
exit 1
fi
}
# Generate C header using cbindgen
generate_header() {
log_info "Generating C header..."
local header_dir="$OUTPUT_DIR/include"
local header_path="$header_dir/arti.h"
mkdir -p "$header_dir"
# Create cbindgen.toml if it doesn't exist
if [[ ! -f "$CRATE_NAME/cbindgen.toml" ]]; then
cat > "$CRATE_NAME/cbindgen.toml" << 'EOF'
language = "C"
include_guard = "ARTI_H"
no_includes = true
sys_includes = ["stdint.h", "stdbool.h"]
[export]
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
[fn]
args = "Auto"
[parse]
parse_deps = false
EOF
fi
cbindgen --config "$CRATE_NAME/cbindgen.toml" \
--crate "$CRATE_NAME" \
--output "$header_path"
if [[ -f "$header_path" ]]; then
log_info "Generated $header_path"
cat "$header_path"
else
log_warn "cbindgen did not generate header, creating manually..."
# Fallback: create header manually
cat > "$header_path" << 'EOF'
#ifndef ARTI_H
#define ARTI_H
#include
#ifdef __cplusplus
extern "C" {
#endif
/**
* Start Arti with a SOCKS5 proxy.
*
* @param data_dir Path to data directory for Tor state (C string)
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
* @return 0 on success, negative on error
*/
int32_t arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* @return 0 on success, -1 if not running
*/
int32_t arti_stop(void);
/**
* Check if Arti is currently running.
*
* @return 1 if running, 0 if not running
*/
int32_t arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*
* @return Progress percentage
*/
int32_t arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* @param buf Buffer to write the summary into
* @param len Length of the buffer
* @return Number of bytes written, -1 on error
*/
int32_t arti_bootstrap_summary(char *buf, int32_t len);
/**
* Signal Arti to go dormant (reduce resource usage).
*
* @return 0 on success, -1 if not running
*/
int32_t arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* @return 0 on success, -1 if not running
*/
int32_t arti_wake(void);
#ifdef __cplusplus
}
#endif
#endif /* ARTI_H */
EOF
log_info "Created manual header at $header_path"
fi
}
# Print size report
print_size_report() {
log_info "=== Size Report ==="
for target in "${TARGETS[@]}"; do
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
if [[ -f "$lib_path" ]]; then
local size=$(du -h "$lib_path" | cut -f1)
echo " $target: $size"
fi
done
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
if [[ -d "$xcframework_path" ]]; then
local total_size=$(du -sh "$xcframework_path" | cut -f1)
echo " xcframework total: $total_size"
fi
}
# Main
main() {
log_info "Building arti-bitchat for iOS/macOS"
log_info "=================================="
check_prerequisites
generate_header
for target in "${TARGETS[@]}"; do
build_target "$target"
done
create_xcframework
print_size_report
log_info "Build complete!"
log_info "xcframework: $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
}
# Run
main "$@"