#!/usr/bin/env bash # Build Go static libraries for iOS and macOS using go build -buildmode=c-archive. # # Prerequisites: # - Go 1.23+ # - Xcode with iOS + macOS SDKs # # Usage: # ./build_go.sh # build all platforms # ./build_go.sh ios # iOS only # ./build_go.sh macos # macOS only set -euo pipefail export PATH="${GOPATH:-$HOME/go}/bin:$PATH" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" GO_DIR="$SCRIPT_DIR/ios/Go" MIN_IOS="14.0" MIN_MACOS="12.0" # Omit Tailscale features we don't need — we only use tsnet.Dial() for proxying. OMIT_TAGS="ts_omit_ssh,ts_omit_drive,ts_omit_taildrop,ts_omit_serve" OMIT_TAGS="$OMIT_TAGS,ts_omit_webclient,ts_omit_capture,ts_omit_appconnectors" OMIT_TAGS="$OMIT_TAGS,ts_omit_debug,ts_omit_doctor,ts_omit_portlist,ts_omit_posture" OMIT_TAGS="$OMIT_TAGS,ts_omit_cli,ts_omit_kube,ts_omit_aws,ts_omit_bird" OMIT_TAGS="$OMIT_TAGS,ts_omit_synology,ts_omit_tpm,ts_omit_tap,ts_omit_conn25" OMIT_TAGS="$OMIT_TAGS,ts_omit_qrcodes,ts_omit_relayserver,ts_omit_systray" OMIT_TAGS="$OMIT_TAGS,ts_omit_webbrowser,ts_omit_completion,ts_omit_completion_scripts" OMIT_TAGS="$OMIT_TAGS,ts_omit_colorable,ts_omit_clientupdate,ts_omit_wakeonlan" OMIT_TAGS="$OMIT_TAGS,ts_omit_usermetrics,ts_omit_desktop_sessions,ts_omit_ace" OMIT_TAGS="$OMIT_TAGS,ts_omit_acme,ts_omit_hujsonconf,ts_omit_tailnetlock" OMIT_TAGS="$OMIT_TAGS,ts_omit_netlog,ts_omit_syspolicy,ts_omit_cachenetmap" PLATFORM="${1:-all}" cd "$GO_DIR" echo "==> Tidying Go modules..." go mod tidy # Helper: create a framework bundle from a static library create_framework() { local lib_path="$1" local header_path="$2" local framework_dir="$3" mkdir -p "$framework_dir/Headers" "$framework_dir/Modules" cp "$lib_path" "$framework_dir/TailscaleKit" cp "$header_path" "$framework_dir/Headers/tailscale.h" cat > "$framework_dir/Modules/module.modulemap" <<'MODMAP' framework module TailscaleKit { header "tailscale.h" export * } MODMAP cat > "$framework_dir/Info.plist" < CFBundleIdentifier io.svrnty.TailscaleKit CFBundleName TailscaleKit CFBundleVersion 1 CFBundlePackageType FMWK PLIST } # ============================================================ # iOS # ============================================================ build_ios() { local BUILD_DIR="$SCRIPT_DIR/ios/build" local OUTPUT="$SCRIPT_DIR/ios/TailscaleKit.xcframework" rm -rf "$BUILD_DIR" "$OUTPUT" mkdir -p "$BUILD_DIR" # iOS device (arm64) echo "==> [iOS] Building for device (arm64)..." local IPHONEOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path) local IPHONEOS_CC=$(xcrun --sdk iphoneos --find clang) CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \ CC="$IPHONEOS_CC" \ CGO_CFLAGS="-isysroot $IPHONEOS_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS" \ CGO_LDFLAGS="-isysroot $IPHONEOS_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS" \ go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \ -o "$BUILD_DIR/ios-arm64/libtailscale.a" . create_framework \ "$BUILD_DIR/ios-arm64/libtailscale.a" \ "$BUILD_DIR/ios-arm64/libtailscale.h" \ "$BUILD_DIR/ios-arm64/TailscaleKit.framework" # iOS simulator (arm64) echo "==> [iOS] Building for simulator (arm64)..." local SIM_SDK=$(xcrun --sdk iphonesimulator --show-sdk-path) local SIM_CC=$(xcrun --sdk iphonesimulator --find clang) CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \ CC="$SIM_CC" \ CGO_CFLAGS="-isysroot $SIM_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS -target arm64-apple-ios${MIN_IOS}-simulator" \ CGO_LDFLAGS="-isysroot $SIM_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS -target arm64-apple-ios${MIN_IOS}-simulator" \ go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \ -o "$BUILD_DIR/ios-sim-arm64/libtailscale.a" . create_framework \ "$BUILD_DIR/ios-sim-arm64/libtailscale.a" \ "$BUILD_DIR/ios-sim-arm64/libtailscale.h" \ "$BUILD_DIR/ios-sim-arm64/TailscaleKit.framework" # Create xcframework echo "==> [iOS] Creating xcframework..." xcodebuild -create-xcframework \ -framework "$BUILD_DIR/ios-arm64/TailscaleKit.framework" \ -framework "$BUILD_DIR/ios-sim-arm64/TailscaleKit.framework" \ -output "$OUTPUT" rm -rf "$BUILD_DIR" cp "$OUTPUT/ios-arm64/TailscaleKit.framework/Headers/tailscale.h" "$SCRIPT_DIR/ios/Classes/tailscale.h" echo "==> [iOS] Done: $OUTPUT" du -sh "$OUTPUT" } # ============================================================ # macOS # ============================================================ build_macos() { local BUILD_DIR="$SCRIPT_DIR/macos/build" local OUTPUT="$SCRIPT_DIR/macos/TailscaleKit.xcframework" rm -rf "$BUILD_DIR" "$OUTPUT" mkdir -p "$BUILD_DIR" local MACOS_SDK=$(xcrun --sdk macosx --show-sdk-path) local MACOS_CC=$(xcrun --sdk macosx --find clang) # macOS arm64 echo "==> [macOS] Building for arm64..." CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \ CC="$MACOS_CC" \ CGO_CFLAGS="-isysroot $MACOS_SDK -arch arm64 -mmacosx-version-min=$MIN_MACOS" \ CGO_LDFLAGS="-isysroot $MACOS_SDK -arch arm64 -mmacosx-version-min=$MIN_MACOS" \ go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \ -o "$BUILD_DIR/macos-arm64/libtailscale.a" . # macOS amd64 echo "==> [macOS] Building for amd64..." CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \ CC="$MACOS_CC" \ CGO_CFLAGS="-isysroot $MACOS_SDK -arch x86_64 -mmacosx-version-min=$MIN_MACOS" \ CGO_LDFLAGS="-isysroot $MACOS_SDK -arch x86_64 -mmacosx-version-min=$MIN_MACOS" \ go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \ -o "$BUILD_DIR/macos-amd64/libtailscale.a" . # Create universal binary with lipo echo "==> [macOS] Creating universal binary (arm64 + x86_64)..." mkdir -p "$BUILD_DIR/macos-universal" lipo -create \ "$BUILD_DIR/macos-arm64/libtailscale.a" \ "$BUILD_DIR/macos-amd64/libtailscale.a" \ -output "$BUILD_DIR/macos-universal/libtailscale.a" create_framework \ "$BUILD_DIR/macos-universal/libtailscale.a" \ "$BUILD_DIR/macos-arm64/libtailscale.h" \ "$BUILD_DIR/macos-universal/TailscaleKit.framework" # Create xcframework (single macOS slice) echo "==> [macOS] Creating xcframework..." xcodebuild -create-xcframework \ -framework "$BUILD_DIR/macos-universal/TailscaleKit.framework" \ -output "$OUTPUT" rm -rf "$BUILD_DIR" echo "==> [macOS] Done: $OUTPUT" du -sh "$OUTPUT" } # ============================================================ # Android (c-shared → .so in jniLibs) # ============================================================ build_android() { local NDK_HOME="${ANDROID_NDK_HOME:-$HOME/Library/Android/sdk/ndk/$(ls $HOME/Library/Android/sdk/ndk/ 2>/dev/null | sort -V | tail -1)}" if [ ! -d "$NDK_HOME" ]; then echo "Error: Android NDK not found. Set ANDROID_NDK_HOME." exit 1 fi local TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64" local OUTPUT_DIR="$SCRIPT_DIR/android/src/main/jniLibs" mkdir -p "$OUTPUT_DIR/arm64-v8a" "$OUTPUT_DIR/x86_64" # Android needs the "android" build tag for proper platform detection # (e.g., netmon_polling.go instead of netmon_linux.go which requires CAP_NET_ADMIN) local ANDROID_TAGS="android,$OMIT_TAGS" # Android arm64 echo "==> [Android] Building for arm64-v8a..." CGO_ENABLED=1 GOOS=android GOARCH=arm64 \ CC="$TOOLCHAIN/bin/aarch64-linux-android21-clang" \ go build -buildmode=c-shared -tags "$ANDROID_TAGS" -ldflags="-s -w" \ -o "$OUTPUT_DIR/arm64-v8a/libtailscale.so" . # Android x86_64 (emulator) echo "==> [Android] Building for x86_64..." CGO_ENABLED=1 GOOS=android GOARCH=amd64 \ CC="$TOOLCHAIN/bin/x86_64-linux-android21-clang" \ go build -buildmode=c-shared -tags "$ANDROID_TAGS" -ldflags="-s -w" \ -o "$OUTPUT_DIR/x86_64/libtailscale.so" . # Clean up generated headers (not needed for .so) rm -f "$OUTPUT_DIR"/*/libtailscale.h echo "==> [Android] Done:" ls -lh "$OUTPUT_DIR/arm64-v8a/libtailscale.so" ls -lh "$OUTPUT_DIR/x86_64/libtailscale.so" } # ============================================================ # Linux (c-shared → .so) # ============================================================ build_linux() { local OUTPUT_DIR="$SCRIPT_DIR/linux" # Linux amd64 — requires a Linux cross-compiler or building on Linux. # On macOS, use Docker: docker run --rm -v $PWD:/src -w /src golang:1.23 ./build_go.sh linux echo "==> [Linux] Building for amd64..." if [ "$(uname)" = "Linux" ]; then CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ go build -buildmode=c-shared -tags "$OMIT_TAGS" -ldflags="-s -w" \ -o "$OUTPUT_DIR/libtailscale.so" . else echo " Cross-compiling Linux from $(uname) via Docker..." docker run --rm --platform linux/amd64 -v "$SCRIPT_DIR:/src" -w /src/ios/Go \ golang:latest bash -c " CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ go build -buildmode=c-shared -tags '$OMIT_TAGS' -ldflags='-s -w' \ -o /src/linux/libtailscale.so . " fi rm -f "$OUTPUT_DIR/libtailscale.h" echo "==> [Linux] Done:" ls -lh "$OUTPUT_DIR/libtailscale.so" } # ============================================================ # Main # ============================================================ case "$PLATFORM" in ios) build_ios ;; macos) build_macos ;; android) build_android ;; linux) build_linux ;; all) build_ios; build_macos; build_android; build_linux ;; apple) build_ios; build_macos ;; *) echo "Usage: $0 [ios|macos|android|linux|apple|all]"; exit 1 ;; esac