#!/bin/bash # 出错即停 set -e # 自动使用 .fvmrc 中指定的 Flutter 版本(避免使用 PATH 里的系统 flutter 导致依赖冲突) if [ -f "./.fvmrc" ]; then FVM_VERSION=$(grep -o '"flutter"[[:space:]]*:[[:space:]]*"[^"]*"' .fvmrc | sed -E 's/.*"([^"]+)"$/\1/') if [ -n "$FVM_VERSION" ] && [ -x "$HOME/fvm/versions/$FVM_VERSION/bin/flutter" ]; then export PATH="$HOME/fvm/versions/$FVM_VERSION/bin:$PATH" echo "🔧 使用 FVM Flutter 版本: $FVM_VERSION" fi fi # 参数说明 if [ $# -lt 1 ]; then printf -- "Build hgdj release package.\n\n" printf -- "Usage: $0 [mode] [export_method] [export_options_plist] [sign_mode]\n" printf -- " target : a(android) | i(ios) | all\n" printf -- " mode : release(默认) | debug | profile\n" printf -- " export_method (iOS) : app-store(默认) | ad-hoc | development | enterprise\n" printf -- " export_options_plist : ExportOptions.plist 路径(iOS 可选,留空用默认)\n" printf -- " sign_mode (iOS) : codesign(默认) | nocodesign\n\n" printf -- "Examples:\n" printf -- " $0 a # Android release\n" printf -- " $0 a debug # Android debug\n" printf -- " $0 i # iOS release + app-store\n" printf -- " $0 i release ad-hoc # iOS release + ad-hoc\n" printf -- " $0 i release app-store ios/ExportOptions.plist\n" printf -- " $0 i release app-store \"\" nocodesign # iOS 不签名出包\n" printf -- " $0 all # Android + iOS release\n" exit 1 fi TARGET=$1 MODE=${2:-release} EXPORT_METHOD=${3:-app-store} EXPORT_OPTIONS_PLIST=${4:-} SIGN_MODE=${5:-codesign} # 参数校验 if [ "$TARGET" != "a" ] && [ "$TARGET" != "i" ] && [ "$TARGET" != "all" ]; then echo "❌ 参数错误:target 只支持 a | i | all,你传的是 '$TARGET'" exit 1 fi if [ "$MODE" != "release" ] && [ "$MODE" != "debug" ] && [ "$MODE" != "profile" ]; then echo "❌ 参数错误:mode 只支持 release | debug | profile,你传的是 '$MODE'" exit 1 fi case "$EXPORT_METHOD" in app-store|ad-hoc|development|enterprise) ;; *) echo "❌ 参数错误:export_method 只支持 app-store | ad-hoc | development | enterprise,你传的是 '$EXPORT_METHOD'" exit 1 ;; esac if [ "$SIGN_MODE" != "codesign" ] && [ "$SIGN_MODE" != "nocodesign" ]; then echo "❌ 参数错误:sign_mode 只支持 codesign | nocodesign,你传的是 '$SIGN_MODE'" exit 1 fi if [ -n "$EXPORT_OPTIONS_PLIST" ] && [ ! -f "$EXPORT_OPTIONS_PLIST" ]; then echo "❌ 未找到 ExportOptions.plist:$EXPORT_OPTIONS_PLIST" exit 1 fi # 项目根路径校验 if [ ! -f "./pubspec.yaml" ]; then echo "❌ 请在项目根目录下执行此脚本" exit 1 fi PROJECT_PATH=$(pwd) PUBSPEC_FILE="pubspec.yaml" # 提取项目信息 APP_NAME=$(grep '^name:' "$PUBSPEC_FILE" | awk '{print $2}') RAW_VERSION=$(grep '^version:' "$PUBSPEC_FILE" | awk '{print $2}') SAFE_VERSION=${RAW_VERSION//+/_} TIME_TAG=$(date +"%m%d-%H%M") # 读取应用内 Config.isDebug(lib/config/config.dart),决定产物命名标识: # isDebug=true → 命名标识固定为 debug(如 hgdj_debug_xxxx),方便区分内测包 # isDebug=false → 正式环境,命名沿用构建模式(release/profile),保持不变 CONFIG_FILE="lib/config/config.dart" CONFIG_DEBUG=$(grep -E 'static[[:space:]]+const[[:space:]]+isDebug[[:space:]]*=' "$CONFIG_FILE" 2>/dev/null | grep -oE 'true|false' | head -1) if [ "$CONFIG_DEBUG" = "true" ]; then BUILD_TAG="debug" elif [ "$CONFIG_DEBUG" = "false" ]; then BUILD_TAG="$MODE" else echo "⚠️ 未能从 $CONFIG_FILE 解析 Config.isDebug,命名回退为构建模式:$MODE" BUILD_TAG="$MODE" fi DIST_DIR="./dist" mkdir -p "$DIST_DIR" # 打印信息并二次确认 echo "==========================================" echo "📦 准备打包" echo "📌 项目名: $APP_NAME" echo "📌 版本号: $RAW_VERSION" echo "📌 目标平台: $TARGET" echo "📌 构建模式: $MODE" echo "📌 Config.isDebug: ${CONFIG_DEBUG:-未知} → 命名标识: $BUILD_TAG" if [ "$TARGET" = "i" ] || [ "$TARGET" = "all" ]; then echo "📌 iOS 导出: $EXPORT_METHOD" echo "📌 iOS 签名: $SIGN_MODE" if [ -n "$EXPORT_OPTIONS_PLIST" ]; then echo "📌 ExportOptions.plist: $EXPORT_OPTIONS_PLIST" fi fi echo "📌 工程路径: $PROJECT_PATH" echo "==========================================" echo "确认无误?(按回车继续,Ctrl+C 取消)" read -r CONFIRM # Android 构建 build_android() { echo "▶️ 开始构建 Android $MODE 包..." if [ "$MODE" = "release" ]; then flutter build apk --release --target-platform android-arm64 APK_SOURCE="build/app/outputs/flutter-apk/app-release.apk" elif [ "$MODE" = "profile" ]; then flutter build apk --profile --target-platform android-arm64 APK_SOURCE="build/app/outputs/flutter-apk/app-profile.apk" else flutter build apk --debug --target-platform android-arm64 APK_SOURCE="build/app/outputs/flutter-apk/app-debug.apk" fi if [ ! -f "$APK_SOURCE" ]; then echo "❌ 构建失败,未找到 APK:$APK_SOURCE" exit 1 fi APK_NAME="${APP_NAME}_${BUILD_TAG}_${SAFE_VERSION}_${TIME_TAG}.apk" APK_DEST="${DIST_DIR}/${APK_NAME}" cp "$APK_SOURCE" "$APK_DEST" APK_SIZE=$(du -h "$APK_DEST" | awk '{print $1}') echo "✅ Android 构建成功!" echo "📁 输出路径: $APK_DEST" echo "📏 文件大小: $APK_SIZE" LAST_BUILT_FILE="$APK_DEST" } # iOS 构建(基于 flutter build ipa,一步出 ipa) build_ios() { echo "▶️ 开始构建 iOS $MODE 包..." if [ "$MODE" = "debug" ]; then echo "⚠️ iOS 不支持 debug 模式打包,请使用 release 或 profile(debug 建议直接 flutter run)" exit 1 fi IPA_OUTPUT_DIR="build/ios/ipa" ARCHIVE_OUTPUT_DIR="build/ios/archive" if [ "$MODE" = "release" ]; then if [ "$SIGN_MODE" = "nocodesign" ]; then flutter build ipa --release --no-codesign elif [ -n "$EXPORT_OPTIONS_PLIST" ]; then flutter build ipa --release --export-options-plist="$EXPORT_OPTIONS_PLIST" else flutter build ipa --release --export-method "$EXPORT_METHOD" fi else if [ "$SIGN_MODE" = "nocodesign" ]; then flutter build ipa --profile --no-codesign elif [ -n "$EXPORT_OPTIONS_PLIST" ]; then flutter build ipa --profile --export-options-plist="$EXPORT_OPTIONS_PLIST" else flutter build ipa --profile --export-method "$EXPORT_METHOD" fi fi # 查找产物 ipa IPA_SOURCE="" for candidate in "$IPA_OUTPUT_DIR"/*.ipa; do if [ -f "$candidate" ]; then IPA_SOURCE="$candidate" break fi done if [ -z "$IPA_SOURCE" ]; then if [ "$SIGN_MODE" = "nocodesign" ]; then if [ -d "$ARCHIVE_OUTPUT_DIR" ]; then echo "⚠️ 未签名模式下 Flutter 通常不会导出可安装的 IPA" echo "✅ 未签名构建产物目录:$ARCHIVE_OUTPUT_DIR" echo "📌 如需可安装/可上传的 IPA,请在 Xcode 配好 Team 和证书后用 codesign 模式重新打包" LAST_BUILT_FILE="" return fi echo "❌ 未签名构建完成,但未找到 archive 目录:$ARCHIVE_OUTPUT_DIR" exit 1 fi echo "❌ 构建结束,但未找到 IPA 文件:$IPA_OUTPUT_DIR/*.ipa" echo "📌 通常是签名证书、Provisioning Profile 或 ExportOptions 配置有问题" echo "📌 可以先试不签名出包:$0 i $MODE $EXPORT_METHOD \"$EXPORT_OPTIONS_PLIST\" nocodesign" exit 1 fi # 与 apk 命名对齐:去掉冗余的 ios 标识(后缀已区分平台),导出方式去连字符(ad-hoc→adhoc) IPA_NAME="${APP_NAME}_${BUILD_TAG}_${EXPORT_METHOD//-/}_${SAFE_VERSION}_${TIME_TAG}.ipa" IPA_DEST="${DIST_DIR}/${IPA_NAME}" cp "$IPA_SOURCE" "$IPA_DEST" IPA_SIZE=$(du -h "$IPA_DEST" | awk '{print $1}') echo "✅ iOS 构建成功!" echo "📁 输出路径: $IPA_DEST" echo "📏 文件大小: $IPA_SIZE" LAST_BUILT_FILE="$IPA_DEST" } # 分发 case "$TARGET" in a) build_android ;; i) build_ios ;; all) build_android; build_ios ;; esac # 自动在 Finder 中打开并选中产物(仅 macOS) if [[ "$OSTYPE" == "darwin"* ]]; then if [ -n "$LAST_BUILT_FILE" ] && [ -f "$LAST_BUILT_FILE" ]; then open -R "$LAST_BUILT_FILE" else open "$DIST_DIR" fi fi echo "🎉 完成"