Files
wangjia 7a9f480ae3 feat(scripts): local_test.sh 增量构建(跳过无变化编译) + Android install -d 允许降级
- 增量构建:每平台记全源码指纹(client/ 下所有源文件内容 + 版本号 VER + 平台名,排除
  build/.dart_tool/ephemeral/.gradle/Pods 等生成物);产物在且指纹未变则跳过 flutter
  编译直接安装/启动。--force/-f 强制重编。指纹存 client/build/.local_test_fp_*(gitignore)。
- 三平台均先 fail-fast 设备检测再判断编译。
- Android 安装加 -d 允许 versionCode 降级(修 INSTALL_FAILED_VERSION_DOWNGRADE)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:55:53 +08:00

225 lines
9.4 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 签名)
# 附加 --force / -f # 强制重编,忽略「代码无变化」缓存
#
# 增量构建:每个平台记一个源码指纹(client/ 下全部源文件内容 + 版本号),产物在且指纹
# 未变则跳过编译直接安装/启动,省掉无谓的重编。指纹存 client/build/.local_test_fp_*(已 gitignore)。
#
# 后端固定指向生产 https://jiu.51yanmei.com(与 CI compile 脚本一致)。
# 版本号 = 最新 client-v* tag 的 patch+1 加 -dev 后缀(如 v1.1.4-dev),
# App 内显示走 APP_VERSION dart-defineupdate_provider.currentAppVersion),
# 与 CI 同一读取链路——本地构建不再显示 pubspec 旧版本(2026-07-04 修复)。
set -euo pipefail
# 本脚本用到 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"
# ── 参数预扫描:抽出 --force/-f,其余(平台/--no-open)原样保留到位置参数 ──
FORCE=no
_args=()
for a in "$@"; do
case "$a" in
--force|-f) FORCE=yes ;;
*) _args+=("$a") ;;
esac
done
set -- ${_args[@]+"${_args[@]}"} # 空数组 + set -u 在 bash 3.2 下的兼容展开
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}"
)
# ── 增量构建指纹(全源码口径)──
# 指纹 = client/ 下所有源文件内容 + 版本号 VER + 目标平台;排除 build/.dart_tool/
# ephemeral/.gradle/Pods/.git 等生成物。任何影响产物的源码/依赖/原生工程改动都会变指纹。
# 须在 REPO_ROOT 下调用(find client 用相对路径)。
_fingerprint() {
local target="$1"
{
printf 'target=%s ver=%s\n' "$target" "$VER"
find client \
-type d \( -name build -o -name .dart_tool -o -name ephemeral \
-o -name .gradle -o -name Pods -o -name .git -o -name .symlinks \) -prune -o \
-type f -print0 \
| LC_ALL=C sort -z | xargs -0 shasum 2>/dev/null
} | shasum | awk '{print $1}'
}
# 指纹未变且产物在 → 跳过编译(返回 1);否则需重编(返回 0)。--force 时恒需重编。
# 用法:if _need_build <target> <产物路径>; then 编译; else 跳过; fi
_FP_FILE=""
_FP_NOW=""
_need_build() {
local target="$1" product="$2"
_FP_FILE="client/build/.local_test_fp_${target}"
_FP_NOW="$(_fingerprint "$target")"
if [ "$FORCE" = no ] && { [ -f "$product" ] || [ -d "$product" ]; } \
&& [ -f "$_FP_FILE" ] && [ "$(cat "$_FP_FILE")" = "$_FP_NOW" ]; then
return 1 # 可跳过
fi
return 0 # 需重编
}
# 编译成功后调用,落盘本次指纹
_mark_built() {
mkdir -p "$(dirname "$_FP_FILE")"
printf '%s\n' "$_FP_NOW" > "$_FP_FILE"
}
if [ "${1:-}" = "--ios" ]; then
# ── iOS:开发签名构建 + devicectl 装到 USB iPhone ──
# Release 配置是 CI 的 Manual+App Store 签名,本机没有分发证书,
# 故 xcodebuild 用 Automatic + Apple Development 覆盖(不改工程文件)。
_find_ios_app() { find "$HOME/Library/Developer/Xcode/DerivedData" -path '*Release-iphoneos/Runner.app' -maxdepth 6 2>/dev/null | head -1; }
# fail-fast:设备没连就别浪费编译。
# 已配对可安装的 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
APP_PATH="$(_find_ios_app)"
if _need_build ios "$APP_PATH"; then
echo "==> 编译 iOS(线上后端,版本 v${VER}"
cd client
flutter build ios --release --config-only "${DEFINES[@]}"
cd ios
# 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)"
cd "$REPO_ROOT"
APP_PATH="$(_find_ios_app)"
if [ -z "$APP_PATH" ]; then
echo "!! 找不到构建产物 Runner.app" >&2
exit 1
fi
_mark_built
else
echo "==> 代码无变化,跳过编译,直接安装现有 iOS 构建(--force 可强制重编)"
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
# ── Androidrelease 构建(无 key.properties 时 Gradle 回退 debug 签名,可本地安装,
# 见 android/app/build.gradle.kts+ adb 装到 USB Android 设备并拉起 ──
APK="client/build/app/outputs/flutter-apk/app-release.apk"
# 解析 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
# fail-fast:取第一台「已授权」设备(状态列 = 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
if _need_build android "$APK"; then
echo "==> 编译 Android APK(线上后端,版本 v${VER}"
cd client
flutter build apk --release "${DEFINES[@]}"
cd "$REPO_ROOT"
if [ ! -f "$APK" ]; then
echo "!! 找不到构建产物 $APK" >&2
exit 1
fi
_mark_built
else
echo "==> 代码无变化,跳过编译,直接安装现有 APK(--force 可强制重编)"
fi
echo "==> 安装到 Android${AD_ID}"
# -r 覆盖安装保留数据;-d 允许 versionCode 降级(本地 dev 构建的 build number 常低于
# 设备上已装版本,否则报 INSTALL_FAILED_VERSION_DOWNGRADE=「已装更高版本,不让安装」)。
# 若仍报 INSTALL_FAILED_UPDATE_INCOMPATIBLE,是签名不同(如设备上是商店 release 版),
# 需先在设备上卸载旧版再重试(本地 debug 签名与商店 release 签名不兼容覆盖)。
"$ADB" -s "$AD_ID" install -r -d "$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
# ── 默认:macOS ──
APP="client/build/macos/Build/Products/Release/jiu_client.app"
if _need_build macos "$APP"; then
echo "==> 编译 macOS app(线上后端,版本 v${VER}"
cd client
flutter build macos --release "${DEFINES[@]}"
cd "$REPO_ROOT"
if [ ! -d "$APP" ]; then
echo "!! 找不到构建产物 $APP" >&2
exit 1
fi
_mark_built
else
echo "==> 代码无变化,跳过编译,直接启动现有 app(--force 可强制重编)"
fi
echo "==> 产物:$APP"
if [ "${1:-}" = "--no-open" ]; then
echo "==> 跳过启动(--no-open"
else
echo "==> 前台启动 app(终端附着日志,退出 app 才返回;Ctrl-C 可终止)"
exec "$APP/Contents/MacOS/jiu_client"
fi