flutter-tsnet/build_go.sh
Mathias Beaulieu-Duncan f070bcdb86 Add Android and Linux platform support (local gRPC working, tsnet WIP)
- Android: Dart FFI → Go c-shared (.so) in jniLibs (arm64-v8a + x86_64)
- Linux: Dart FFI → Go c-shared (.so) via Docker cross-compilation (amd64)
- Dart API: TsnetFlutter uses MethodChannel on iOS/macOS, FFI on Android/Linux
- Add ffi package dependency for native function bindings
- Build script: ./build_go.sh [ios|macos|android|linux|apple|all]
- Android RegisterInterfaceGetter to bypass netlink CAP_NET_ADMIN restriction
- Make TailscaleStart() idempotent and add GODEBUG=netdns=go for Android

Known: Android tsnet tunnel blocked by Go stdlib net.Interfaces() netlink
call — local gRPC works, Tailscale fallback needs libtailscale integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 07:49:11 -04:00

272 lines
9.7 KiB
Bash
Executable File

#!/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" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>io.svrnty.TailscaleKit</string>
<key>CFBundleName</key>
<string>TailscaleKit</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
</dict>
</plist>
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