From 7a9f480ae33eb2455a2279c1f0abd2cab5a21a0f Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 11 Jul 2026 12:55:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20local=5Ftest.sh=20=E5=A2=9E?= =?UTF-8?q?=E9=87=8F=E6=9E=84=E5=BB=BA(=E8=B7=B3=E8=BF=87=E6=97=A0?= =?UTF-8?q?=E5=8F=98=E5=8C=96=E7=BC=96=E8=AF=91)=20+=20Android=20install?= =?UTF-8?q?=20-d=20=E5=85=81=E8=AE=B8=E9=99=8D=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增量构建:每平台记全源码指纹(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 --- scripts/local_test.sh | 151 ++++++++++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 35 deletions(-) diff --git a/scripts/local_test.sh b/scripts/local_test.sh index 3fe58f5..841ce62 100755 --- a/scripts/local_test.sh +++ b/scripts/local_test.sh @@ -6,12 +6,17 @@ # 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-define(update_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 调起,则在 @@ -30,6 +35,17 @@ 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}" @@ -46,14 +62,49 @@ DEFINES=( "--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 <产物路径>; 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 覆盖(不改工程文件)。 - echo "==> 编译 iOS(线上后端,版本 v${VER})" - cd client - flutter build ios --release --config-only "${DEFINES[@]}" - cd ios + _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)" @@ -61,20 +112,33 @@ if [ "${1:-}" = "--ios" ]; 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 + + 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})" @@ -84,16 +148,7 @@ 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)" @@ -109,29 +164,55 @@ if [ "${1:-}" = "--android" ]; then exit 1 fi - # 取第一台「已授权」设备(状态列 = device,排除 unauthorized/offline/未授权) + # 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 覆盖安装保留数据;若曾装过不同签名(如商店版)会报 INSTALL_FAILED_UPDATE_INCOMPATIBLE, - # 需先在设备上卸载旧版再重试(本地 debug 签名与商店 release 签名不同)。 - "$ADB" -s "$AD_ID" install -r "$APK" + # -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" -echo "==> 编译 macOS app(线上后端,版本 v${VER})" -cd client -flutter build macos --release "${DEFINES[@]}" -cd "$REPO_ROOT" +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"