#!/usr/bin/env bash # build-android.sh — gomobile bind sing-box libbox → dist/android/libbox.aar # # 与 scripts/build-libbox.sh android 的关系(两条都能产 app/kernel/dist/android/libbox.aar): # - 本脚本:裸 gomobile bind 本地 .build/singbox 源,tag 精简(无 tailscale/cronet)→ # 体积小、NDK 27 也能链接。**必须带 -javapkg io.nekohasekai**(见下方 bind),否则 # 产出包名是默认 `libbox`,与 Kotlin 的 io.nekohasekai.libbox 对不上、APK 编不过。 # - scripts/build-libbox.sh:走上游官方 build_libbox,带 tailscale/cronet → 体积大、需 NDK ≥ 28。 # 两者产出包名一致(io.nekohasekai.libbox),择一即可;product 是 gitignore 的,清掉就得重建。 # # 用法: ./build-android.sh [--force] # --force 忽略已有产物,强制重新构建 # # 依赖: # - Go : VERSION 中 GO_VERSION 指定的版本($GOROOT 或 PATH 中的 go) # - gomobile: 脚本自动安装至 $GOPATH/bin,依赖 GOMOBILE_VERSION # - Android NDK: 需设置 ANDROID_NDK_HOME 或 ANDROID_HOME(含 ndk-bundle/ndk/) # # 产物: dist/android/libbox.aar # 包含三个 ABI: arm64-v8a / armeabi-v7a / x86_64 # # Build tags 说明(对齐官方 SFA — sing-box for Android): # with_quic — QUIC/HTTP3 协议支持(Hysteria2 必须) # with_utls — uTLS 指纹模拟(TLS 防检测) # with_clash_api — Clash 兼容 API(面板控制必须) # with_gvisor — gVisor TUN 网络栈(高性能 TUN 回退) # # 裁剪开关(按需取消注释): # with_grpc — gRPC 入站(约 +4 MB so 体积) # with_ech — TLS ECH 扩展(约 +0.5 MB) # with_wireguard — WireGuard 出站(约 +2 MB) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ── 加载版本锚点 ───────────────────────────────────────────────────────────── # shellcheck source=VERSION . "${SCRIPT_DIR}/VERSION" # ── 参数解析 ───────────────────────────────────────────────────────────────── FORCE=false for arg in "$@"; do case "${arg}" in --force) FORCE=true ;; *) printf 'Unknown argument: %s\n' "${arg}" >&2; exit 1 ;; esac done # ── 路径配置 ───────────────────────────────────────────────────────────────── DIST_DIR="${SCRIPT_DIR}/dist/android" OUT_AAR="${DIST_DIR}/libbox.aar" BUILD_SRC="${SCRIPT_DIR}/.build/singbox" # ── Build tags ──────────────────────────────────────────────────────────────── # 功能 tags(最小集:REALITY/VLESS + Hysteria2(quic) + uTLS + Clash API + gVisor)。 # badlinkname,tfogo_checklinkname0 = Go 1.23+ 的 //go:linkname 限制放行 # (对齐 sing-box cmd/internal/build_libbox 的 sharedTags,缺则 Go 1.24 编译失败)。 BUILD_TAGS="with_quic,with_utls,with_clash_api,with_gvisor,badlinkname,tfogo_checklinkname0" # 取消注释以增加功能(会增大 .so 体积): # BUILD_TAGS="${BUILD_TAGS},with_grpc" # BUILD_TAGS="${BUILD_TAGS},with_ech" # BUILD_TAGS="${BUILD_TAGS},with_wireguard" # ── 幂等检查 ───────────────────────────────────────────────────────────────── if [[ "${FORCE}" == false && -f "${OUT_AAR}" ]]; then printf '✓ %s 已存在,跳过构建(传 --force 强制重建)\n' "${OUT_AAR}" exit 0 fi # ── 前置检查 ───────────────────────────────────────────────────────────────── printf '==> 检查前置依赖…\n' if ! command -v go >/dev/null 2>&1; then printf '✗ go 未找到,请安装 Go %s\n' "${GO_VERSION}" >&2 exit 1 fi if ! command -v git >/dev/null 2>&1; then printf '✗ git 未找到\n' >&2 exit 1 fi ACTUAL_GO="$(go version | awk '{print $3}' | sed 's/go//')" printf ' Go 版本: %s(期望 %s)\n' "${ACTUAL_GO}" "${GO_VERSION}" if [[ -n "${ANDROID_NDK_HOME:-}" ]]; then printf ' NDK: %s\n' "${ANDROID_NDK_HOME}" elif [[ -n "${ANDROID_HOME:-}" ]]; then printf ' Android SDK: %s(gomobile 将自动定位 NDK)\n' "${ANDROID_HOME}" else printf '⚠ 未设置 ANDROID_NDK_HOME / ANDROID_HOME,gomobile init 可能失败\n' >&2 fi # ── JDK(gomobile bind 末段要 javac/jar 编译 Java 绑定)───────────────────────── # macOS 的 /usr/bin/javac 仅为桩(无系统 JDK 时报 "Unable to locate a Java Runtime")。 # 若未显式设 JAVA_HOME,优先用 Android Studio 自带 JBR(OpenJDK 21)。 if [[ -z "${JAVA_HOME:-}" ]]; then AS_JBR="/Applications/Android Studio.app/Contents/jbr/Contents/Home" if [[ -x "${AS_JBR}/bin/javac" ]]; then export JAVA_HOME="${AS_JBR}" export PATH="${JAVA_HOME}/bin:${PATH}" printf ' JDK: %s(Android Studio JBR)\n' "${JAVA_HOME}" else printf '⚠ 未设 JAVA_HOME 且未找到 Android Studio JBR,gomobile bind 末段 javac 可能失败\n' >&2 fi else printf ' JDK: %s(JAVA_HOME)\n' "${JAVA_HOME}" fi # ── 安装 gomobile(SagerNet fork,见 VERSION 注释)──────────────────────────── printf '==> 安装 %s/cmd/gomobile@%s…\n' "${GOMOBILE_MODULE}" "${GOMOBILE_VERSION}" go install "${GOMOBILE_MODULE}/cmd/gomobile@${GOMOBILE_VERSION}" go install "${GOMOBILE_MODULE}/cmd/gobind@${GOMOBILE_VERSION}" printf '==> gomobile init…\n' gomobile init # ── 准备 sing-box 源码 ──────────────────────────────────────────────────────── printf '==> 准备 sing-box %s 源码…\n' "${SINGBOX_VERSION}" EXISTING_TAG="" if [[ -d "${BUILD_SRC}/.git" ]]; then EXISTING_TAG="$(git -C "${BUILD_SRC}" describe --tags --exact-match 2>/dev/null)" || EXISTING_TAG="unknown" fi if [[ "${EXISTING_TAG}" == "${SINGBOX_VERSION}" ]]; then printf ' 源码已是 %s,跳过克隆\n' "${SINGBOX_VERSION}" else if [[ -d "${BUILD_SRC}" ]]; then printf ' 已有源码版本 %s ≠ %s,清理后重新克隆…\n' "${EXISTING_TAG}" "${SINGBOX_VERSION}" rm -rf "${BUILD_SRC}" fi mkdir -p "${BUILD_SRC%/*}" git clone --depth 1 --branch "${SINGBOX_VERSION}" \ https://github.com/SagerNet/sing-box.git "${BUILD_SRC}" fi # ── 构建 AAR ────────────────────────────────────────────────────────────────── printf '==> 构建 libbox.aar\n' printf ' target : android/arm64,android/arm,android/amd64\n' printf ' api : 21\n' printf ' tags : %s\n' "${BUILD_TAGS}" mkdir -p "${DIST_DIR}" # ldflags 对齐 sing-box cmd/internal/build_libbox 的 sharedFlags: # -checklinkname=0 放行 badlinkname 的 //go:linkname(Go 1.23+ 默认 checklinkname=1 # 会拒 os.checkPidfdOnce 等内部符号引用,缺则链接失败) # defaultGODEBUG=multipathtcp=0 关 MPTCP(与官方一致) # -X ...constant.Version 版本戳 # -s -w -buildid= 裁剪符号表,产物更小 LDFLAGS="-X github.com/sagernet/sing-box/constant.Version=${SINGBOX_VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0" ( cd "${BUILD_SRC}" go mod download gomobile bind \ -v \ -trimpath \ -buildvcs=false \ -ldflags "${LDFLAGS}" \ -target android/arm64,android/arm,android/amd64 \ -androidapi 21 \ -tags "${BUILD_TAGS}" \ -javapkg io.nekohasekai \ -o "${OUT_AAR}" \ ./experimental/libbox ) printf '\n✓ 构建完成: %s\n' "${OUT_AAR}" printf ' ABI 验证(.so 文件列表):\n' unzip -l "${OUT_AAR}" | grep -E '\.so$' | awk '{print " " $NF}' || true