build.sh 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. #!/usr/bin/env bash
  2. # ibit Flutter 自动打包脚本
  3. #
  4. # 用法:
  5. # ./scripts/build.sh 构建 Android + iOS Release (默认)
  6. # ./scripts/build.sh android 仅构建 Android Release
  7. # ./scripts/build.sh android-debug 仅构建 Android Debug
  8. # ./scripts/build.sh ios 仅构建 iOS Release
  9. # ./scripts/build.sh ios-adhoc 仅构建 iOS Ad-hoc
  10. # ./scripts/build.sh --clean 构建前先执行 flutter clean
  11. # ./scripts/build.sh -h 显示帮助
  12. #
  13. # 产物归档位置: releases/v<版本>_<时间戳>/
  14. # ├── app-release.apk
  15. # ├── *.ipa
  16. # ├── symbols-android/ (Dart 混淆符号,用于 flutter symbolize 反解)
  17. # ├── symbols-ios/ (同上)
  18. # └── BUILD_INFO.txt
  19. set -euo pipefail
  20. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  21. PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
  22. cd "$PROJECT_ROOT"
  23. RELEASES_DIR="releases"
  24. SYMBOLS_ANDROID_DIR="build/debug-info/android"
  25. SYMBOLS_IOS_DIR="build/debug-info/ios"
  26. if [ -t 1 ]; then
  27. RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
  28. BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
  29. else
  30. RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; NC=''
  31. fi
  32. log_info() { printf "${BLUE}==>${NC} %s\n" "$*"; }
  33. log_ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
  34. log_warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
  35. log_err() { printf "${RED}✗${NC} %s\n" "$*" >&2; }
  36. log_step() { printf "\n${CYAN}${BOLD}▶ %s${NC}\n" "$*"; }
  37. usage() {
  38. cat <<EOF
  39. ibit Flutter 自动打包脚本
  40. 用法:
  41. ./scripts/build.sh [模式] [选项]
  42. 模式:
  43. all 构建 Android + iOS Release (默认)
  44. android 仅构建 Android Release
  45. android-debug 仅构建 Android Debug
  46. ios 仅构建 iOS Release
  47. ios-adhoc 仅构建 iOS Ad-hoc
  48. 选项:
  49. --clean 构建前执行 flutter clean
  50. -h, --help 显示此帮助
  51. 示例:
  52. ./scripts/build.sh # 同时打 Android + iOS Release
  53. ./scripts/build.sh android # 只打 Android Release
  54. ./scripts/build.sh ios-adhoc --clean # 清理后打 iOS Ad-hoc
  55. 产物归档位置: ${RELEASES_DIR}/v<版本>_<时间戳>/
  56. EOF
  57. }
  58. MODE="all"
  59. DO_CLEAN=false
  60. while [ $# -gt 0 ]; do
  61. case "$1" in
  62. -h|--help) usage; exit 0 ;;
  63. --clean) DO_CLEAN=true ;;
  64. all|android|android-debug|ios|ios-adhoc) MODE="$1" ;;
  65. *) log_err "未知参数: $1"; echo; usage; exit 1 ;;
  66. esac
  67. shift
  68. done
  69. if ! command -v flutter >/dev/null 2>&1; then
  70. log_err "未找到 flutter 命令,请检查 PATH"
  71. exit 1
  72. fi
  73. if [ ! -f pubspec.yaml ]; then
  74. log_err "未在 pubspec.yaml 所在目录执行,当前: $(pwd)"
  75. exit 1
  76. fi
  77. VERSION="$(grep '^version:' pubspec.yaml | awk '{print $2}' | tr -d '[:space:]')"
  78. if [ -z "$VERSION" ]; then
  79. log_err "无法从 pubspec.yaml 读取版本号"
  80. exit 1
  81. fi
  82. TIMESTAMP="$(date +%Y%m%d_%H%M)"
  83. RELEASE_DIR="${RELEASES_DIR}/v${VERSION}_${TIMESTAMP}"
  84. log_step "ibit 打包"
  85. log_info "版本 : v${VERSION}"
  86. log_info "模式 : ${MODE}"
  87. log_info "归档目录 : ${RELEASE_DIR}"
  88. log_info "Git 分支 : $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')"
  89. log_info "Git 提交 : $(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')"
  90. DIRTY_COUNT=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
  91. if [ "$DIRTY_COUNT" -gt 0 ]; then
  92. log_warn "当前有 $DIRTY_COUNT 个未提交的改动,构建的产物可能包含未追踪代码"
  93. fi
  94. if [ "$DO_CLEAN" = true ]; then
  95. log_step "flutter clean"
  96. flutter clean
  97. log_ok "clean 完成"
  98. fi
  99. log_step "flutter pub get"
  100. flutter pub get >/dev/null
  101. log_ok "pub get 完成"
  102. mkdir -p "$RELEASE_DIR"
  103. copy_android_symbols() {
  104. if [ -d "$SYMBOLS_ANDROID_DIR" ] && [ "$(ls -A "$SYMBOLS_ANDROID_DIR" 2>/dev/null)" ]; then
  105. mkdir -p "$RELEASE_DIR/symbols-android"
  106. cp -r "$SYMBOLS_ANDROID_DIR"/* "$RELEASE_DIR/symbols-android/"
  107. log_ok "Android 符号 → $RELEASE_DIR/symbols-android/"
  108. fi
  109. }
  110. copy_ios_symbols() {
  111. if [ -d "$SYMBOLS_IOS_DIR" ] && [ "$(ls -A "$SYMBOLS_IOS_DIR" 2>/dev/null)" ]; then
  112. mkdir -p "$RELEASE_DIR/symbols-ios"
  113. cp -r "$SYMBOLS_IOS_DIR"/* "$RELEASE_DIR/symbols-ios/"
  114. log_ok "iOS 符号 → $RELEASE_DIR/symbols-ios/"
  115. fi
  116. }
  117. build_android_debug() {
  118. log_step "构建 Android Debug APK"
  119. flutter build apk --debug \
  120. --target-platform android-arm,android-arm64
  121. local apk="build/app/outputs/flutter-apk/app-debug.apk"
  122. if [ ! -f "$apk" ]; then
  123. log_err "未找到产物: $apk"
  124. return 1
  125. fi
  126. cp "$apk" "$RELEASE_DIR/app-debug.apk"
  127. local size; size=$(du -h "$apk" | cut -f1)
  128. log_ok "Debug APK (${size}) → $RELEASE_DIR/app-debug.apk"
  129. }
  130. build_android_release() {
  131. log_step "构建 Android Release APK"
  132. flutter build apk --release \
  133. --target-platform android-arm,android-arm64 \
  134. --obfuscate \
  135. --split-debug-info="$SYMBOLS_ANDROID_DIR"
  136. local apk="build/app/outputs/flutter-apk/app-release.apk"
  137. if [ ! -f "$apk" ]; then
  138. log_err "未找到产物: $apk"
  139. return 1
  140. fi
  141. cp "$apk" "$RELEASE_DIR/app-release.apk"
  142. copy_android_symbols
  143. local size; size=$(du -h "$apk" | cut -f1)
  144. log_ok "Release APK (${size}) → $RELEASE_DIR/app-release.apk"
  145. }
  146. build_ios_release() {
  147. log_step "构建 iOS Release IPA"
  148. flutter build ipa --release \
  149. --obfuscate \
  150. --split-debug-info="$SYMBOLS_IOS_DIR"
  151. local ipa
  152. ipa="$(find build/ios/ipa -maxdepth 1 -name "*.ipa" -type f 2>/dev/null | head -1)"
  153. if [ -z "$ipa" ]; then
  154. log_err "未找到 IPA 产物,请检查 Xcode 签名配置"
  155. return 1
  156. fi
  157. local dst="$RELEASE_DIR/$(basename "$ipa" .ipa)-release.ipa"
  158. cp "$ipa" "$dst"
  159. copy_ios_symbols
  160. local size; size=$(du -h "$ipa" | cut -f1)
  161. log_ok "Release IPA (${size}) → $dst"
  162. }
  163. build_ios_adhoc() {
  164. log_step "构建 iOS Ad-hoc IPA"
  165. flutter build ipa --release --export-method=ad-hoc \
  166. --obfuscate \
  167. --split-debug-info="$SYMBOLS_IOS_DIR"
  168. local ipa
  169. ipa="$(find build/ios/ipa -maxdepth 1 -name "*.ipa" -type f 2>/dev/null | head -1)"
  170. if [ -z "$ipa" ]; then
  171. log_err "未找到 IPA 产物,请检查 Xcode 签名配置"
  172. return 1
  173. fi
  174. local dst="$RELEASE_DIR/$(basename "$ipa" .ipa)-adhoc.ipa"
  175. cp "$ipa" "$dst"
  176. copy_ios_symbols
  177. local size; size=$(du -h "$ipa" | cut -f1)
  178. log_ok "Ad-hoc IPA (${size}) → $dst"
  179. }
  180. case "$MODE" in
  181. all) build_android_release; build_ios_release ;;
  182. android) build_android_release ;;
  183. android-debug) build_android_debug ;;
  184. ios) build_ios_release ;;
  185. ios-adhoc) build_ios_adhoc ;;
  186. esac
  187. {
  188. echo "版本 : v${VERSION}"
  189. echo "构建时间 : $(date '+%Y-%m-%d %H:%M:%S')"
  190. echo "构建模式 : ${MODE}"
  191. echo "Git 分支 : $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')"
  192. echo "Git 提交 : $(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')"
  193. echo "未提交改动 : ${DIRTY_COUNT}"
  194. echo ""
  195. echo "Flutter 版本:"
  196. flutter --version 2>&1 | sed 's/^/ /'
  197. } > "$RELEASE_DIR/BUILD_INFO.txt"
  198. log_step "完成"
  199. log_info "归档目录: $(pwd)/${RELEASE_DIR}"
  200. echo ""
  201. ls -lh "$RELEASE_DIR"
  202. if [ -f .gitignore ] && ! grep -qE "^/?releases/?$" .gitignore; then
  203. echo ""
  204. log_warn "建议将 '${RELEASES_DIR}/' 加入 .gitignore,避免构建产物进入 Git"
  205. fi