You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
519 lines
16 KiB
519 lines
16 KiB
#!/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"
|
|
OUTPUT_DIR="/Users/liuxiamin/work/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 aab \
|
|
--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 "$@"
|
|
|