#!/bin/bash # ================================================================= # Eaimar 多渠道 Android 打包脚本 # 用法: # ./build_channels.sh # 打包所有渠道 # ./build_channels.sh huawei google # 只打包指定渠道 # ./build_channels.sh --clean # 清理后再打包所有渠道 # ./build_channels.sh --list # 列出所有支持的渠道 # ================================================================= set -euo pipefail # ---------- 常量定义 ---------- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" PUBSPEC_FILE="${SCRIPT_DIR}/pubspec.yaml" # 产物输出目录。默认落在仓库内的 build_outputs/(已在 .gitignore 里), # 需要放别处时用环境变量覆盖:BUILD_OUTPUT_DIR=/path/to/out ./build_channels.sh google OUTPUT_DIR="${BUILD_OUTPUT_DIR:-${SCRIPT_DIR}/build_outputs}" APP_NAME="EAIMAR" BUILD_DATE=$(date +"%m%d") # ---------- 渠道配置 ---------- # 渠道名列表(保证顺序) CHANNEL_NAMES="huawei xiaomi oppo vivo honor google" # 根据渠道名获取 .env 中的 Channel 值 get_channel_env_value() { case "$1" in huawei) echo "CHINA_HUAWEI" ;; xiaomi) echo "CHINA_XIAOMI" ;; oppo) echo "CHINA_OPPO" ;; vivo) echo "CHINA_VIVO" ;; honor) echo "CHINA_HONOR" ;; google) echo "GLOBAL_GOOGLE" ;; *) echo "" ;; esac } # 根据渠道名获取构建类型 get_build_type() { case "$1" in google) echo "aab" ;; *) echo "apk" ;; esac } # 校验渠道名是否有效 is_valid_channel() { case "$1" in huawei|xiaomi|oppo|vivo|honor|google) return 0 ;; *) return 1 ;; esac } # 构建结果追踪 SUCCESS_CHANNELS="" FAILED_CHANNELS="" # ---------- 颜色输出 ---------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # ---------- 辅助函数 ---------- log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[OK]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # 提取版本号(从 pubspec.yaml 的 version 字段,去掉构建号) get_version() { grep '^version:' "$PUBSPEC_FILE" | sed 's/version: *//' | cut -d'+' -f1 } # 备份 .env backup_env() { cp "$ENV_FILE" "${ENV_FILE}.bak" log_info "已备份 .env -> .env.bak" } # 恢复 .env restore_env() { if [ -f "${ENV_FILE}.bak" ]; then mv "${ENV_FILE}.bak" "$ENV_FILE" log_info "已恢复 .env 文件" fi } # 修改 .env 中的 Channel 值(兼容 macOS sed) set_channel() { local channel_value="$1" if [ "$(uname)" = "Darwin" ]; then sed -i '' "s/^Channel=.*/Channel=${channel_value}/" "$ENV_FILE" else sed -i "s/^Channel=.*/Channel=${channel_value}/" "$ENV_FILE" fi } # 列出所有渠道 list_channels() { echo "" echo "支持的渠道列表:" echo " 序号 渠道名 .env值 构建格式" echo " ---- -------- ---------------- --------" local idx=1 for ch in $CHANNEL_NAMES; do local env_val env_val=$(get_channel_env_value "$ch") local build_type build_type=$(get_build_type "$ch") printf " [%d] %-10s %-18s %s\n" "$idx" "$ch" "$env_val" "$build_type" idx=$((idx + 1)) done echo "" } # 交互式选择渠道(上下键移动,空格勾选,Enter确认) interactive_select() { # 构建菜单项数组 local items="全部打包" for ch in $CHANNEL_NAMES; do local bt bt=$(get_build_type "$ch") items="${items}|${ch} (${bt})" done local total_items=7 # 全部打包 + 6个渠道 local cursor=0 # 当前光标位置 # 选中状态:0=未选 1=已选(用空格分隔的列表,索引0-6) local selected="0 0 0 0 0 0 0" # 获取第n项的选中状态 get_selected() { echo "$selected" | cut -d' ' -f$(($1 + 1)) } # 切换第n项的选中状态 toggle_selected() { local idx=$1 local new_selected="" local i=0 for val in $selected; do if [ $i -eq $idx ]; then if [ "$val" = "0" ]; then # 如果选中"全部打包",取消其他所有 if [ $idx -eq 0 ]; then selected="1 0 0 0 0 0 0" return fi val="1" else val="0" fi fi # 选中单个渠道时,取消"全部打包" if [ $idx -ne 0 ] && [ $i -eq 0 ]; then val="0" fi if [ -z "$new_selected" ]; then new_selected="$val" else new_selected="$new_selected $val" fi i=$((i + 1)) done selected="$new_selected" } # 绘制菜单 draw_menu() { # 移动光标到菜单起始位置 local i=0 printf "\033[${total_items}A" # 向上移动 local item_idx=0 # 保存IFS并用|分割 local OLD_IFS="$IFS" IFS='|' for item in $items; do local marker=" " local sel sel=$(get_selected $item_idx) if [ "$sel" = "1" ]; then marker="✓" fi if [ $item_idx -eq $cursor ]; then # 高亮当前行 printf "\033[2K ${GREEN}▸ [${marker}] ${item}${NC}\n" else printf "\033[2K [${marker}] ${item}\n" fi item_idx=$((item_idx + 1)) done IFS="$OLD_IFS" } # 显示标题 echo "" echo "╔══════════════════════════════════════════╗" echo "║ Eaimar 多渠道打包工具 ║" echo "╚══════════════════════════════════════════╝" echo "" echo " ↑/↓ 移动 空格 勾选 Enter 确认 q 退出" echo "" # 首次绘制菜单(先输出占位行) local i=0 while [ $i -lt $total_items ]; do echo "" i=$((i + 1)) done draw_menu # 读取键盘输入 while true; do # 读取单个字符(静默模式) IFS= read -rsn1 key case "$key" in $'\x1b') # 读取转义序列的剩余部分 read -rsn2 seq case "$seq" in '[A') # 上箭头 if [ $cursor -gt 0 ]; then cursor=$((cursor - 1)) else cursor=$((total_items - 1)) fi ;; '[B') # 下箭头 if [ $cursor -lt $((total_items - 1)) ]; then cursor=$((cursor + 1)) else cursor=0 fi ;; esac ;; ' ') # 空格:切换选中 toggle_selected $cursor ;; '') # Enter:确认 break ;; 'q'|'Q') # 退出 echo "" echo "已取消" exit 0 ;; esac draw_menu done # 根据选中状态构建渠道列表 if [ "$(get_selected 0)" = "1" ]; then SELECTED_CHANNELS="$CHANNEL_NAMES" else SELECTED_CHANNELS="" local idx=1 for ch in $CHANNEL_NAMES; do if [ "$(get_selected $idx)" = "1" ]; then SELECTED_CHANNELS="${SELECTED_CHANNELS} ${ch}" fi idx=$((idx + 1)) done SELECTED_CHANNELS=$(echo "$SELECTED_CHANNELS" | sed 's/^ *//') fi if [ -z "$SELECTED_CHANNELS" ]; then log_error "未选择任何渠道" exit 1 fi echo "" log_info "已选择: ${SELECTED_CHANNELS}" } # ---------- 核心构建函数 ---------- build_channel() { local channel_name="$1" local env_value env_value=$(get_channel_env_value "$channel_name") local build_type build_type=$(get_build_type "$channel_name") local version version=$(get_version) local output_filename="${APP_NAME}_${BUILD_DATE}_${version}_${channel_name}.${build_type}" echo "" echo "========================================" log_info "开始构建渠道: ${channel_name} (${env_value})" log_info "构建类型: ${build_type}" log_info "输出文件: ${output_filename}" echo "========================================" # 1. 修改 .env 中的 Channel 值 set_channel "$env_value" log_info "已设置 Channel=${env_value}" # 2. 执行 flutter build local build_start build_start=$(date +%s) local build_result=0 local source_file="" if [ "$build_type" = "apk" ]; then flutter build apk \ --obfuscate \ --split-debug-info=./build/app/outputs/symbols \ 2>&1 | tee "${OUTPUT_DIR}/build_${channel_name}.log" || build_result=$? source_file="${SCRIPT_DIR}/build/app/outputs/flutter-apk/app-release.apk" else flutter build appbundle \ --obfuscate \ --split-debug-info=./build/app/outputs/symbols \ 2>&1 | tee "${OUTPUT_DIR}/build_${channel_name}.log" || build_result=$? source_file="${SCRIPT_DIR}/build/app/outputs/bundle/release/app-release.aab" fi local build_end build_end=$(date +%s) local build_duration=$((build_end - build_start)) # 3. 检查构建结果 if [ $build_result -ne 0 ]; then log_error "渠道 ${channel_name} 构建失败!(耗时 ${build_duration}秒)" FAILED_CHANNELS="${FAILED_CHANNELS} ${channel_name}" return 1 fi # 4. 复制并重命名输出文件 if [ -f "$source_file" ]; then cp "$source_file" "${OUTPUT_DIR}/${output_filename}" local file_size file_size=$(du -h "${OUTPUT_DIR}/${output_filename}" | cut -f1) # 构建成功,删除日志文件 rm -f "${OUTPUT_DIR}/build_${channel_name}.log" log_success "渠道 ${channel_name} 构建成功!(耗时 ${build_duration}秒, 大小 ${file_size})" log_success "输出: ${OUTPUT_DIR}/${output_filename}" SUCCESS_CHANNELS="${SUCCESS_CHANNELS} ${channel_name}:${build_duration}" else log_error "构建产物未找到: ${source_file}" FAILED_CHANNELS="${FAILED_CHANNELS} ${channel_name}" return 1 fi } # ---------- 主流程 ---------- main() { local total_start total_start=$(date +%s) # 参数解析 local do_clean=false local channels_to_build="" for arg in "$@"; do case "$arg" in --list|-l) list_channels exit 0 ;; --clean|-c) do_clean=true ;; --help|-h) echo "用法: $0 [选项] [渠道名...]" echo "" echo "选项:" echo " --list, -l 列出所有支持的渠道" echo " --clean, -c 构建前先执行 flutter clean" echo " --help, -h 显示帮助信息" echo "" echo "示例:" echo " $0 # 打包所有渠道" echo " $0 huawei google # 只打包华为和谷歌" echo " $0 --clean # 清理后打包所有渠道" exit 0 ;; *) if ! is_valid_channel "$arg"; then log_error "未知渠道: $arg" list_channels exit 1 fi channels_to_build="${channels_to_build} ${arg}" ;; esac done # 如果没有指定渠道,进入交互选择模式 if [ -z "$channels_to_build" ]; then interactive_select channels_to_build="$SELECTED_CHANNELS" fi # 去掉前导空格 channels_to_build=$(echo "$channels_to_build" | sed 's/^ *//') local version version=$(get_version) # 计算渠道总数 local total=0 for _ in $channels_to_build; do total=$((total + 1)) done # 显示构建计划 echo "" echo "╔══════════════════════════════════════════╗" echo "║ Eaimar 多渠道打包工具 ║" echo "╚══════════════════════════════════════════╝" echo "" log_info "构建渠道: ${channels_to_build}" log_info "版本号: ${version}" log_info "构建日期: ${BUILD_DATE}" log_info "输出目录: ${OUTPUT_DIR}" log_info "共 ${total} 个渠道" # 前置检查 if ! command -v flutter &> /dev/null; then log_error "flutter 命令未找到,请确保 Flutter SDK 已安装并配置 PATH" exit 1 fi if [ ! -f "$ENV_FILE" ]; then log_error ".env 文件不存在: $ENV_FILE" exit 1 fi # 创建输出目录 mkdir -p "$OUTPUT_DIR" # 可选:flutter clean if [ "$do_clean" = true ]; then log_info "执行 flutter clean..." flutter clean log_info "执行 flutter pub get..." flutter pub get fi # 备份 .env 并注册 trap(确保异常退出也能恢复) backup_env trap restore_env EXIT # 遍历构建 local current=0 for channel in $channels_to_build; do current=$((current + 1)) echo "" echo "████████████████████████████████████████" echo " 进度: [${current}/${total}] 渠道: ${channel}" echo "████████████████████████████████████████" build_channel "$channel" || true done # ---------- 构建结果汇总 ---------- local total_end total_end=$(date +%s) local total_duration=$((total_end - total_start)) local total_min=$((total_duration / 60)) local total_sec=$((total_duration % 60)) # 统计成功/失败数量 local success_count=0 local fail_count=0 for _ in $SUCCESS_CHANNELS; do success_count=$((success_count + 1)) done for _ in $FAILED_CHANNELS; do fail_count=$((fail_count + 1)) done echo "" echo "╔══════════════════════════════════════════╗" echo "║ 构 建 结 果 汇 总 ║" echo "╠══════════════════════════════════════════╣" if [ $success_count -gt 0 ]; then echo "║ 成功: ║" for item in $SUCCESS_CHANNELS; do local name="${item%%:*}" local dur="${item##*:}" printf "║ ✅ %-12s (%s秒) ║\n" "$name" "$dur" done fi if [ $fail_count -gt 0 ]; then echo "║ 失败: ║" for name in $FAILED_CHANNELS; do printf "║ ❌ %-12s ║\n" "$name" done fi echo "╠══════════════════════════════════════════╣" printf "║ 成功: %d/%d 总耗时: %d分%d秒 ║\n" \ "$success_count" "$total" "$total_min" "$total_sec" echo "╚══════════════════════════════════════════╝" # 列出输出目录中的安装包文件 if [ $success_count -gt 0 ]; then echo "" log_info "输出文件列表:" ls -lh "${OUTPUT_DIR}/${APP_NAME}_${BUILD_DATE}"_* 2>/dev/null || true fi # 如果有失败的渠道,退出码为 1 if [ $fail_count -gt 0 ]; then exit 1 fi } main "$@"