#!/usr/bin/env bash # local_test.sh — 本地编译一份「指向线上后端」的 app 用于发版前自测。 # # 用法: # sh scripts/local_test.sh # 编译 macOS + 启动 # sh scripts/local_test.sh --no-open # 只编译 macOS 不启动 # sh scripts/local_test.sh --ios # 编译 iOS 并装到 USB iPhone(开发签名) # sh scripts/local_test.sh --android # 编译 APK 并装到 USB Android 设备(无密钥回退 debug 签名) # # 后端固定指向生产 https://jiu.51yanmei.com(与 CI compile 脚本一致)。 # 版本号 = 最新 client-v* tag 的 patch+1 加 -dev 后缀(如 v1.1.4-dev), # App 内显示走 APP_VERSION dart-define(update_provider.currentAppVersion), # 与 CI 同一读取链路——本地构建不再显示 pubspec 旧版本(2026-07-04 修复)。 # # 本脚本用到 bash 专属特性(数组、进程替换 `>(...)`)。若被 `sh` 调起(macOS 的 # /bin/sh 实为 bash --posix:会设 BASH_VERSION 但禁用进程替换,导致下方 xcodebuild # 那行报 `syntax error near unexpected token '>'`),或被 dash 等非 bash 调起,则在 # 解析到那行之前先重执行到普通模式 bash。此判断须是第一条可执行语句。 _posix=no case ":${SHELLOPTS:-}:" in *:posix:*) _posix=yes ;; esac if [ -z "${BASH_VERSION:-}" ] || [ "$_posix" = yes ]; then exec bash "$0" "$@" fi unset _posix set -euo pipefail export PATH="/opt/homebrew/bin:$PATH" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" LATEST_TAG="$(git describe --tags --match 'client-v*' --abbrev=0 2>/dev/null || true)" if [ -n "$LATEST_TAG" ]; then BASE_VER="${LATEST_TAG#client-v}" MAJOR_MINOR="${BASE_VER%.*}" PATCH="${BASE_VER##*.}" VER="${MAJOR_MINOR}.$((PATCH + 1))-dev" else VER="0.0.0-dev" fi DEFINES=( "--dart-define=BASE_URL=https://jiu.51yanmei.com" "--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" "--dart-define=APP_VERSION=v${VER}" ) if [ "${1:-}" = "--ios" ]; then # ── iOS:开发签名构建 + devicectl 装到 USB iPhone ── # Release 配置是 CI 的 Manual+App Store 签名,本机没有分发证书, # 故 xcodebuild 用 Automatic + Apple Development 覆盖(不改工程文件)。 echo "==> 编译 iOS(线上后端,版本 v${VER})" cd client flutter build ios --release --config-only "${DEFINES[@]}" cd ios # 已配对可安装的 iPhone:新版 Xcode(devicectl) USB 直连并解锁显示 "connected", # 旧版曾显示 "available (paired)";两者都收。-w 词匹配避免误命中 disconnected/unavailable。 CD_ID="$(xcrun devicectl list devices 2>/dev/null | grep 'iPhone' | grep -Ew 'connected|available' | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -1 || true)" if [ -z "$CD_ID" ]; then echo "!! 未检测到已配对 iPhone(插线并解锁后重试)" >&2 exit 1 fi # stderr 滤掉锁屏 iPhone 的握手噪音(DTDK "passcode protected" 每 3s 刷一条, # 构建目标是 generic/platform=iOS 不需要真机,纯噪音;真错误仍透传) xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release \ -destination "generic/platform=iOS" \ CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=BYL4KQHMTN \ CODE_SIGN_IDENTITY="Apple Development" PROVISIONING_PROFILE_SPECIFIER= \ -allowProvisioningUpdates -allowProvisioningDeviceRegistration build \ 2> >(grep -vE "DTDKRemoteDeviceConnection|DVTDeviceOperation|passcode protected|dtdevicekit|DTDeviceKitBase|DVTFoundation|libdispatch|libsystem_pthread|^\)|^\s+[0-9]+ " >&2) \ | grep -E "BUILD (SUCCEEDED|FAILED)" APP_PATH="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path '*Release-iphoneos/Runner.app' -maxdepth 6 2>/dev/null | head -1)" if [ -z "$APP_PATH" ]; then echo "!! 找不到构建产物 Runner.app" >&2 exit 1 fi echo "==> 安装到 iPhone(${CD_ID})" xcrun devicectl device install app --device "$CD_ID" "$APP_PATH" | grep -m1 databaseSequenceNumber || true echo "==> 完成:解锁手机打开 岩美酒库(版本 v${VER})" exit 0 fi if [ "${1:-}" = "--android" ]; then # ── Android:release 构建(无 key.properties 时 Gradle 回退 debug 签名,可本地安装, # 见 android/app/build.gradle.kts)+ adb 装到 USB Android 设备并拉起 ── echo "==> 编译 Android APK(线上后端,版本 v${VER})" cd client flutter build apk --release "${DEFINES[@]}" cd "$REPO_ROOT" APK="client/build/app/outputs/flutter-apk/app-release.apk" if [ ! -f "$APK" ]; then echo "!! 找不到构建产物 $APK" >&2 exit 1 fi # 解析 adb:优先 PATH,回退常见 Android SDK 位置 ADB="$(command -v adb || true)" if [ -z "$ADB" ]; then for c in "$HOME/Library/Android/sdk/platform-tools/adb" \ "${ANDROID_HOME:-}/platform-tools/adb" \ "${ANDROID_SDK_ROOT:-}/platform-tools/adb"; do if [ -x "$c" ]; then ADB="$c"; break; fi done fi if [ -z "$ADB" ]; then echo "!! 未找到 adb(装 Android SDK platform-tools,或将其加入 PATH)" >&2 exit 1 fi # 取第一台「已授权」设备(状态列 = device,排除 unauthorized/offline/未授权) AD_ID="$("$ADB" devices | awk 'NR>1 && $2=="device" {print $1; exit}')" if [ -z "$AD_ID" ]; then echo "!! 未检测到已授权的 Android 设备(插线、解锁、允许「USB 调试」授权后重试)" >&2 exit 1 fi echo "==> 安装到 Android(${AD_ID})" # -r 覆盖安装保留数据;若曾装过不同签名(如商店版)会报 INSTALL_FAILED_UPDATE_INCOMPATIBLE, # 需先在设备上卸载旧版再重试(本地 debug 签名与商店 release 签名不同)。 "$ADB" -s "$AD_ID" install -r "$APK" echo "==> 拉起 岩美酒库" "$ADB" -s "$AD_ID" shell monkey -p com.yanmei.jiu -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 || true echo "==> 完成:Android 上打开 岩美酒库(版本 v${VER})" exit 0 fi APP="client/build/macos/Build/Products/Release/jiu_client.app" echo "==> 编译 macOS app(线上后端,版本 v${VER})" cd client flutter build macos --release "${DEFINES[@]}" cd "$REPO_ROOT" echo "==> 产物:$APP" if [ "${1:-}" = "--no-open" ]; then echo "==> 跳过启动(--no-open)" else echo "==> 前台启动 app(终端附着日志,退出 app 才返回;Ctrl-C 可终止)" exec "$APP/Contents/MacOS/jiu_client" fi