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

#!/bin/bash
# =================================================================
# Voitrans 多渠道 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="Voitrans"
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 "║ Voitrans 多渠道打包工具 ║"
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 "║ Voitrans 多渠道打包工具 ║"
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 "$@"