Files
huangguo_android/build_apk.sh
T

320 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <target> [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")
# 混淆 + 抽离调试信息:
# --obfuscate 混淆 Dart 符号;--split-debug-info 把调试信息(含 libapp.so 里
# 嵌入的本机绝对路径 .../huangguo_android/.dart_tool/.../dart_plugin_registrant.dart)
# 抽到单独目录,不再残留在产物里。反编译后搜不到本机用户名/工程路径。
# 仅 release/profile 生效(debug 为 JIT,不支持也无需)。symbols 目录务必保留、勿外传,
# 崩溃堆栈符号化要用它。
OBFUSCATE_ARGS=""
if [ "$MODE" = "release" ] || [ "$MODE" = "profile" ]; then
SYMBOLS_DIR="$(pwd)/build/symbols/${MODE}_${SAFE_VERSION}_${TIME_TAG}"
mkdir -p "$SYMBOLS_DIR"
# 注意:本项目已启用 BlackObfuscator,再叠加 Flutter --obfuscate 会导致启动黑屏,故关闭。
# 去除 libapp.so 内嵌构建路径改由出包后的 strip_build_path 二进制替换完成,与此无关。
OBFUSCATE_ARGS=""
# OBFUSCATE_ARGS="--obfuscate --split-debug-info=$SYMBOLS_DIR"
fi
# 读取应用内 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
# 清除 libapp.so 里嵌入的本机工程绝对路径(.../<工程>/.dart_tool/.../dart_plugin_registrant.dart)
# 做法:对该 URI 字符串做「等长」替换为中性路径(不含用户名/工程名),再 zipalign + 用原 keystore 重签。
# 等长替换保证 Dart String 长度字段不变、运行时安全;该 URI 仅为元数据,不参与运行逻辑。
strip_build_path() {
local APK="$1"
[ -f "$APK" ] || return 0
local BT; BT=$(ls -d "$HOME/Library/Android/sdk/build-tools/"* 2>/dev/null | sort -V | tail -1)
# 优先用 gradle 指定的 keystore(android/keystore/app-release.jks),避免误选到同目录备份
local KS=""
for cand in android/keystore/app-release.jks android/app/../keystore/app-release.jks; do
[ -f "$cand" ] && { KS="$cand"; break; }
done
[ -z "$KS" ] && KS=$(find android -name '*.jks' ! -name '*old*' ! -name '*bak*' 2>/dev/null | head -1)
local SP KP KA
SP=$(grep -E "storePassword" android/app/build.gradle | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/")
KP=$(grep -E "keyPassword" android/app/build.gradle | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/")
KA=$(grep -E "keyAlias" android/app/build.gradle | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/")
if [ -z "$BT" ] || [ -z "$KS" ] || [ -z "$SP" ] || [ -z "$KA" ]; then
echo "⚠️ 跳过路径清理:缺少 build-tools / keystore / 签名信息"; return 0
fi
echo "🧹 清理 libapp.so 内嵌构建路径..."
local TMP; TMP=$(mktemp -d)
PROJ="$(pwd)" python3 - "$APK" "$TMP/patched.apk" <<'PY'
import sys, os, zipfile
from urllib.parse import quote
src, dst = sys.argv[1], sys.argv[2]
TAIL = b'/.dart_tool/flutter_build/dart_plugin_registrant.dart'
proj = os.environ["PROJ"]
# Flutter 以百分号编码嵌入非 ASCII 路径段;精确构造「当前工程」那条完整字节串
needle = quote(proj, safe="/").encode() + TAIL
# 等长中性替换:保留结构性尾巴,头部用 /aaa.../ 填满到同样长度(不含用户名/工程名)
repl = b'/' + b'a' * (len(needle) - 1 - len(TAIL)) + TAIL
assert len(repl) == len(needle)
def patch(d):
c = d.count(needle)
return (d.replace(needle, repl), c) if c else (d, 0)
zin = zipfile.ZipFile(src); zout = zipfile.ZipFile(dst, 'w'); total = 0
for it in zin.infolist():
data = zin.read(it.filename)
if it.filename.endswith('/libapp.so'):
data, n = patch(data); total += n
zi = zipfile.ZipInfo(it.filename, date_time=it.date_time)
zi.compress_type = it.compress_type; zi.external_attr = it.external_attr
zi.internal_attr = it.internal_attr; zi.create_system = it.create_system
zout.writestr(zi, data)
zout.close(); zin.close()
print(" 精确替换次数:", total)
if total == 0:
print(" ⚠️ 未找到工程路径字节串,未改动(宁可保留也不乱替换)")
PY
"$BT/zipalign" -f -p 4 "$TMP/patched.apk" "$TMP/aligned.apk" >/dev/null 2>&1 \
&& "$BT/apksigner" sign --ks "$KS" --ks-pass "pass:$SP" --ks-key-alias "$KA" --key-pass "pass:${KP:-$SP}" \
--v1-signing-enabled false --v2-signing-enabled true --out "$TMP/signed.apk" "$TMP/aligned.apk" >/dev/null 2>&1 \
&& "$BT/apksigner" verify "$TMP/signed.apk" >/dev/null 2>&1
if [ $? -eq 0 ]; then
cp "$TMP/signed.apk" "$APK"; echo "✅ 已清理并重签:$APK"
else
echo "⚠️ 清理失败,保留原包未改动:$APK"
fi
rm -rf "$TMP"
}
# Android 构建
build_android() {
echo "▶️ 开始构建 Android $MODE 包..."
if [ "$MODE" = "release" ]; then
flutter build apk --release --target-platform android-arm64 $OBFUSCATE_ARGS
APK_SOURCE="build/app/outputs/flutter-apk/app-release.apk"
elif [ "$MODE" = "profile" ]; then
flutter build apk --profile --target-platform android-arm64 $OBFUSCATE_ARGS
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"
# 注意:本 App 有运行时自校验,任何出包后修改 libapp.so(即使等长精确替换并重签)都会
# 导致启动卡在启动页。故不再对产物做二进制补丁。去除内嵌构建路径改用“中性目录构建”方案。
# strip_build_path 函数保留备查,但默认不调用。
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 $OBFUSCATE_ARGS
elif [ -n "$EXPORT_OPTIONS_PLIST" ]; then
flutter build ipa --release --export-options-plist="$EXPORT_OPTIONS_PLIST" $OBFUSCATE_ARGS
else
flutter build ipa --release --export-method "$EXPORT_METHOD" $OBFUSCATE_ARGS
fi
else
if [ "$SIGN_MODE" = "nocodesign" ]; then
flutter build ipa --profile --no-codesign $OBFUSCATE_ARGS
elif [ -n "$EXPORT_OPTIONS_PLIST" ]; then
flutter build ipa --profile --export-options-plist="$EXPORT_OPTIONS_PLIST" $OBFUSCATE_ARGS
else
flutter build ipa --profile --export-method "$EXPORT_METHOD" $OBFUSCATE_ARGS
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 "🎉 完成"