#!/usr/bin/env bash # ibit Flutter 自动打包脚本 # # 用法: # ./scripts/build.sh 构建 Android + iOS Release (默认) # ./scripts/build.sh android 仅构建 Android Release # ./scripts/build.sh android-debug 仅构建 Android Debug # ./scripts/build.sh ios 仅构建 iOS Release # ./scripts/build.sh ios-adhoc 仅构建 iOS Ad-hoc # ./scripts/build.sh --clean 构建前先执行 flutter clean # ./scripts/build.sh -h 显示帮助 # # 产物归档位置: releases/v<版本>_<时间戳>/ # ├── app-release.apk # ├── *.ipa # ├── symbols-android/ (Dart 混淆符号,用于 flutter symbolize 反解) # ├── symbols-ios/ (同上) # └── BUILD_INFO.txt set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$PROJECT_ROOT" RELEASES_DIR="releases" SYMBOLS_ANDROID_DIR="build/debug-info/android" SYMBOLS_IOS_DIR="build/debug-info/ios" if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; NC='' fi log_info() { printf "${BLUE}==>${NC} %s\n" "$*"; } log_ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } log_warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } log_err() { printf "${RED}✗${NC} %s\n" "$*" >&2; } log_step() { printf "\n${CYAN}${BOLD}▶ %s${NC}\n" "$*"; } usage() { cat <_<时间戳>/ EOF } MODE="all" DO_CLEAN=false while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; --clean) DO_CLEAN=true ;; all|android|android-debug|ios|ios-adhoc) MODE="$1" ;; *) log_err "未知参数: $1"; echo; usage; exit 1 ;; esac shift done if ! command -v flutter >/dev/null 2>&1; then log_err "未找到 flutter 命令,请检查 PATH" exit 1 fi if [ ! -f pubspec.yaml ]; then log_err "未在 pubspec.yaml 所在目录执行,当前: $(pwd)" exit 1 fi VERSION="$(grep '^version:' pubspec.yaml | awk '{print $2}' | tr -d '[:space:]')" if [ -z "$VERSION" ]; then log_err "无法从 pubspec.yaml 读取版本号" exit 1 fi TIMESTAMP="$(date +%Y%m%d_%H%M)" RELEASE_DIR="${RELEASES_DIR}/v${VERSION}_${TIMESTAMP}" log_step "ibit 打包" log_info "版本 : v${VERSION}" log_info "模式 : ${MODE}" log_info "归档目录 : ${RELEASE_DIR}" log_info "Git 分支 : $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')" log_info "Git 提交 : $(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')" DIRTY_COUNT=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') if [ "$DIRTY_COUNT" -gt 0 ]; then log_warn "当前有 $DIRTY_COUNT 个未提交的改动,构建的产物可能包含未追踪代码" fi if [ "$DO_CLEAN" = true ]; then log_step "flutter clean" flutter clean log_ok "clean 完成" fi log_step "flutter pub get" flutter pub get >/dev/null log_ok "pub get 完成" mkdir -p "$RELEASE_DIR" copy_android_symbols() { if [ -d "$SYMBOLS_ANDROID_DIR" ] && [ "$(ls -A "$SYMBOLS_ANDROID_DIR" 2>/dev/null)" ]; then mkdir -p "$RELEASE_DIR/symbols-android" cp -r "$SYMBOLS_ANDROID_DIR"/* "$RELEASE_DIR/symbols-android/" log_ok "Android 符号 → $RELEASE_DIR/symbols-android/" fi } copy_ios_symbols() { if [ -d "$SYMBOLS_IOS_DIR" ] && [ "$(ls -A "$SYMBOLS_IOS_DIR" 2>/dev/null)" ]; then mkdir -p "$RELEASE_DIR/symbols-ios" cp -r "$SYMBOLS_IOS_DIR"/* "$RELEASE_DIR/symbols-ios/" log_ok "iOS 符号 → $RELEASE_DIR/symbols-ios/" fi } build_android_debug() { log_step "构建 Android Debug APK" flutter build apk --debug \ --target-platform android-arm,android-arm64 local apk="build/app/outputs/flutter-apk/app-debug.apk" if [ ! -f "$apk" ]; then log_err "未找到产物: $apk" return 1 fi cp "$apk" "$RELEASE_DIR/app-debug.apk" local size; size=$(du -h "$apk" | cut -f1) log_ok "Debug APK (${size}) → $RELEASE_DIR/app-debug.apk" } build_android_release() { log_step "构建 Android Release APK" flutter build apk --release \ --target-platform android-arm,android-arm64 \ --obfuscate \ --split-debug-info="$SYMBOLS_ANDROID_DIR" local apk="build/app/outputs/flutter-apk/app-release.apk" if [ ! -f "$apk" ]; then log_err "未找到产物: $apk" return 1 fi cp "$apk" "$RELEASE_DIR/app-release.apk" copy_android_symbols local size; size=$(du -h "$apk" | cut -f1) log_ok "Release APK (${size}) → $RELEASE_DIR/app-release.apk" } build_ios_release() { log_step "构建 iOS Release IPA" flutter build ipa --release \ --obfuscate \ --split-debug-info="$SYMBOLS_IOS_DIR" local ipa ipa="$(find build/ios/ipa -maxdepth 1 -name "*.ipa" -type f 2>/dev/null | head -1)" if [ -z "$ipa" ]; then log_err "未找到 IPA 产物,请检查 Xcode 签名配置" return 1 fi local dst="$RELEASE_DIR/$(basename "$ipa" .ipa)-release.ipa" cp "$ipa" "$dst" copy_ios_symbols local size; size=$(du -h "$ipa" | cut -f1) log_ok "Release IPA (${size}) → $dst" } build_ios_adhoc() { log_step "构建 iOS Ad-hoc IPA" flutter build ipa --release --export-method=ad-hoc \ --obfuscate \ --split-debug-info="$SYMBOLS_IOS_DIR" local ipa ipa="$(find build/ios/ipa -maxdepth 1 -name "*.ipa" -type f 2>/dev/null | head -1)" if [ -z "$ipa" ]; then log_err "未找到 IPA 产物,请检查 Xcode 签名配置" return 1 fi local dst="$RELEASE_DIR/$(basename "$ipa" .ipa)-adhoc.ipa" cp "$ipa" "$dst" copy_ios_symbols local size; size=$(du -h "$ipa" | cut -f1) log_ok "Ad-hoc IPA (${size}) → $dst" } case "$MODE" in all) build_android_release; build_ios_release ;; android) build_android_release ;; android-debug) build_android_debug ;; ios) build_ios_release ;; ios-adhoc) build_ios_adhoc ;; esac { echo "版本 : v${VERSION}" echo "构建时间 : $(date '+%Y-%m-%d %H:%M:%S')" echo "构建模式 : ${MODE}" echo "Git 分支 : $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')" echo "Git 提交 : $(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')" echo "未提交改动 : ${DIRTY_COUNT}" echo "" echo "Flutter 版本:" flutter --version 2>&1 | sed 's/^/ /' } > "$RELEASE_DIR/BUILD_INFO.txt" log_step "完成" log_info "归档目录: $(pwd)/${RELEASE_DIR}" echo "" ls -lh "$RELEASE_DIR" if [ -f .gitignore ] && ! grep -qE "^/?releases/?$" .gitignore; then echo "" log_warn "建议将 '${RELEASES_DIR}/' 加入 .gitignore,避免构建产物进入 Git" fi