feat(android): T-AND-001 工程骨架(AGP 8.7.3 + Gradle 8.9 + Compose)

This commit is contained in:
2026-09-14 20:40:16 +08:00
parent 568664a9aa
commit 2f6212d5bf
21 changed files with 1091 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# ============================================================
# OpenDance Studio —— Android APK 打包(T-AND-009)
#
# 文档 §6.3 方式二:命令行 Gradle
#
# 前置:
# JAVA_HOME 指向 JDK 17(AGP 8.7 要求)
# ANDROID_HOME / ANDROID_SDK_ROOT 指向 Android SDK
# 需要 platforms;android-34 与 build-tools;34.0.0
#
# 用法:
# bash build/build-apk.sh # 同时产出 debug 与 release
# bash build/build-apk.sh debug
# bash build/build-apk.sh release
# ============================================================
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
TARGET="${1:-all}"
case "$TARGET" in
debug|release|all) ;;
*) echo "用法:bash build/build-apk.sh [debug|release|all]" >&2; exit 2 ;;
esac
log() { printf '\033[1;34m[apk]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
# ---- 环境检查 ----
# 优先使用 JAVA_HOME 指定的 JDK:PATH 上的 java 可能是更新的版本,
# AGP 8.7 需要 17~21,检测时必须与 Gradle 实际使用的 JVM 一致。
JAVA_BIN="java"
if [ -n "${JAVA_HOME:-}" ] && [ -x "$JAVA_HOME/bin/java" ]; then
JAVA_BIN="$JAVA_HOME/bin/java"
fi
command -v "$JAVA_BIN" >/dev/null 2>&1 || die "未找到 java,请安装 JDK 17 或设置 JAVA_HOME"
JAVA_MAJOR="$("$JAVA_BIN" -version 2>&1 | head -1 | sed -E 's/.*version "([0-9]+).*/\1/')"
log "Java:$JAVA_BIN (主版本 $JAVA_MAJOR,AGP 8.7 需要 17+)"
if [ "$JAVA_MAJOR" -lt 17 ] 2>/dev/null; then
die "JDK 版本过低:需要 17 或更高(当前 $JAVA_MAJOR)"
fi
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [ -z "$SDK" ]; then
for candidate in "$HOME/Android/Sdk" /opt/android-sdk /usr/lib/android-sdk; do
[ -d "$candidate" ] && SDK="$candidate" && break
done
fi
[ -n "$SDK" ] && [ -d "$SDK" ] || die "未找到 Android SDK,请设置 ANDROID_HOME"
log "Android SDK:$SDK"
if [ ! -f "$PROJECT_DIR/local.properties" ]; then
echo "sdk.dir=$SDK" > "$PROJECT_DIR/local.properties"
log "已写入 local.properties"
fi
[ -f "$PROJECT_DIR/gradlew" ] || die "缺少 gradlew(请先执行 gradle wrapper)"
# ---- 签名检查 ----
if [ "$TARGET" != "debug" ]; then
if [ -f "$PROJECT_DIR/keystore.properties" ]; then
log "使用 keystore.properties 中的发布签名"
else
log "提示:未找到 keystore.properties,release 将回退为 debug 签名"
log " 正式发布请先执行:bash build/gen-keystore.sh"
fi
fi
# ---- 构建 ----
if [ "$TARGET" = "debug" ] || [ "$TARGET" = "all" ]; then
log "构建 debug APK…"
./gradlew assembleDebug --console=plain
fi
if [ "$TARGET" = "release" ] || [ "$TARGET" = "all" ]; then
log "构建 release APK…"
./gradlew assembleRelease --console=plain
fi
# ---- 校验产物 ----
DEBUG_APK="app/build/outputs/apk/debug/app-debug.apk"
RELEASE_APK="app/build/outputs/apk/release/app-release.apk"
verify_apk() {
local apk="$1"
local label="$2"
[ -f "$apk" ] || { echo " ✗ 未生成 $label:$apk"; return 1; }
local size
size=$(stat -c%s "$apk")
# APK 是 zip,校验魔数 PK
local magic
magic=$(head -c 2 "$apk" | od -An -tx1 | tr -d ' \n')
[ "$magic" = "504b" ] || { echo " ✗ $label 不是合法 APK(魔数 $magic)"; return 1; }
# 校验是否已签名:APK v2 签名块附近含 "APK Sig Block 42"
local signed="未检出 v2 签名块"
if grep -qa "APK Sig Block 42" "$apk"; then
signed="已签名(APK Signature Scheme v2)"
elif unzip -l "$apk" 2>/dev/null | grep -q "META-INF/.*\.RSA"; then
signed="已签名(v1 JAR 签名)"
fi
echo " ✓ $label:$apk"
echo " 大小 $((size / 1024)) KiB | $signed"
return 0
}
echo ""
log "产物校验:"
FAILED=0
if [ "$TARGET" = "debug" ] || [ "$TARGET" = "all" ]; then
verify_apk "$DEBUG_APK" "Debug APK" || FAILED=1
fi
if [ "$TARGET" = "release" ] || [ "$TARGET" = "all" ]; then
verify_apk "$RELEASE_APK" "Release APK" || FAILED=1
fi
[ "$FAILED" -eq 0 ] || die "有产物未通过校验"
echo ""
log "安装到已连接设备:"
echo " adb install -r $DEBUG_APK"
echo " adb install -r $RELEASE_APK"
echo ""
log "查看签名信息:"
echo " \$ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs $RELEASE_APK"
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# ============================================================
# OpenDance Studio —— 生成发布签名密钥库
#
# 生成的 keystore 与 keystore.properties **不会入库**(见 .gitignore),
# 请自行妥善备份;丢失后无法对已发布 APK 做覆盖安装升级。
#
# 用法:
# bash build/gen-keystore.sh # 交互式,使用默认文件名
# CN=yourname bash build/gen-keystore.sh # 指定证书 CN
# KEYSTORE_PASSWORD=xxx KEY_PASSWORD=yyy bash build/gen-keystore.sh
# ============================================================
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
KEYSTORE_DIR="$PROJECT_DIR/keystore"
KEYSTORE_FILE="$KEYSTORE_DIR/opendance-release.jks"
PROPS_FILE="$PROJECT_DIR/keystore.properties"
ALIAS="${KEY_ALIAS:-opendance}"
STORE_PASS="${KEYSTORE_PASSWORD:-}"
KEY_PASS="${KEY_PASSWORD:-}"
CN="${CN:-OpenDance Studio}"
DNAME="${DNAME:-CN=$CN, OU=OpenDance, O=OpenDance Studio, L=Unknown, ST=Unknown, C=CN}"
VALIDITY_DAYS="${VALIDITY_DAYS:-10000}"
if ! command -v keytool >/dev/null 2>&1; then
echo "错误:未找到 keytool。请安装 JDK(17+)并确保 keytool 在 PATH 中。" >&2
exit 1
fi
if [ -f "$KEYSTORE_FILE" ]; then
echo "密钥库已存在:$KEYSTORE_FILE"
echo "如需重新生成,请先删除该文件(注意:会导致已发布 APK 无法升级安装)。"
exit 0
fi
if [ -z "$STORE_PASS" ]; then
read -r -s -p "请输入密钥库口令(至少 6 位):" STORE_PASS; echo
read -r -s -p "请再次输入: " STORE_PASS_CONFIRM; echo
[ "$STORE_PASS" = "$STORE_PASS_CONFIRM" ] || { echo "两次输入不一致" >&2; exit 1; }
fi
if [ -z "$KEY_PASS" ]; then
KEY_PASS="$STORE_PASS"
fi
if [ ${#STORE_PASS} -lt 6 ]; then
echo "错误:密钥库口令至少 6 位。" >&2
exit 1
fi
mkdir -p "$KEYSTORE_DIR"
echo "[keystore] 生成 $KEYSTORE_FILE"
keytool -genkeypair \
-keystore "$KEYSTORE_FILE" \
-alias "$ALIAS" \
-keyalg RSA \
-keysize 2048 \
-validity "$VALIDITY_DAYS" \
-storetype PKCS12 \
-storepass "$STORE_PASS" \
-keypass "$KEY_PASS" \
-dname "$DNAME" \
-v >/dev/null
cat > "$PROPS_FILE" <<EOF
# 由 build/gen-keystore.sh 生成;**禁止提交到仓库**(已在 .gitignore 中)
storeFile=$KEYSTORE_FILE
storePassword=$STORE_PASS
keyAlias=$ALIAS
keyPassword=$KEY_PASS
EOF
chmod 600 "$PROPS_FILE" "$KEYSTORE_FILE"
echo ""
echo "[keystore] 完成:"
echo " 密钥库:$KEYSTORE_FILE"
echo " 配置: $PROPS_FILE"
echo " 别名: $ALIAS"
echo " 有效期:$VALIDITY_DAYS 天"
echo ""
echo "请备份这两个文件。之后 ./gradlew assembleRelease 会自动使用它们签名。"
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# ============================================================
# OpenDance Studio —— Android 辅助 App 发布打包
#
# 产物(文档 §6.3):
# opendance-android-v0.1.0.zip
# ├── app-release.apk
# ├── THIRD_PARTY.md
# ├── README.txt
# └── contracts/ 契约快照
#
# 用法:bash build/package-release.sh [版本号]
# ============================================================
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
VERSION="${1:-0.1.0}"
NAME="opendance-android-v${VERSION}"
OUT_DIR="$PROJECT_DIR/dist-release"
STAGE="$OUT_DIR/$NAME"
APK="app/build/outputs/apk/release/app-release.apk"
log() { printf '\033[1;34m[package]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
[ -f "$APK" ] || die "未找到 $APK,请先执行 bash build/build-apk.sh release"
# APK 是 zip,校验魔数
MAGIC=$(head -c 2 "$APK" | od -An -tx1 | tr -d ' \n')
[ "$MAGIC" = "504b" ] || die "$APK 不是合法 APK(魔数 $MAGIC)"
log "整理产物到 $STAGE …"
rm -rf "$STAGE"
mkdir -p "$STAGE"
cp "$APK" "$STAGE/app-release.apk"
cp THIRD_PARTY.md README.txt "$STAGE/"
cp -r contracts "$STAGE/contracts"
log "生成压缩包…"
( cd "$OUT_DIR" && zip -qr "$NAME.zip" "$NAME" )
log "完成:$OUT_DIR/$NAME.zip($(du -h "$OUT_DIR/$NAME.zip" | cut -f1))"
echo ""
echo "产物内容:"
unzip -l "$OUT_DIR/$NAME.zip" | sed -n '4,20p'
echo ""
echo "分发:把 APK 交给用户(Android 10+),或解压后 adb install -r app-release.apk"
+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# ============================================================
# OpenDance Studio —— Android 辅助 App 一键验证
#
# 依次执行:单元测试 → debug 构建 → release 构建 → 产物校验
# 任一环节失败即退出非 0。
#
# 用法:
# JAVA_HOME=/path/to/jdk17 ANDROID_HOME=/path/to/sdk bash build/verify.sh
# ============================================================
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
FAILED=0
echo "============================================================"
echo " 1/3 单元测试(JVM,无需设备)"
echo "============================================================"
if ./gradlew testDebugUnitTest --console=plain -q 2>&1 | tail -20; then
echo "✓ 单元测试通过"
else
echo "✗ 单元测试失败"
FAILED=1
fi
echo " 测试报告:app/build/reports/tests/testDebugUnitTest/index.html"
echo ""
echo "============================================================"
echo " 2/3 构建 debug + release APK"
echo "============================================================"
if bash build/build-apk.sh all 2>&1 | grep -vE "^> Task|^$" | tail -20; then
:
else
FAILED=1
fi
echo ""
echo "============================================================"
echo " 3/3 产物校验(签名 / 包信息 / 模型资源)"
echo "============================================================"
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
# 优先选用与 compileSdk 匹配的 build-tools,缺失时退回最新版
if [ -d "$SDK/build-tools/34.0.0" ]; then
BT="$SDK/build-tools/34.0.0"
else
BT="$(ls -d "$SDK"/build-tools/* 2>/dev/null | sort -V | tail -1)"
fi
RELEASE_APK="app/build/outputs/apk/release/app-release.apk"
if [ ! -f "$RELEASE_APK" ]; then
echo "✗ 未生成 $RELEASE_APK"
FAILED=1
elif [ -z "$BT" ]; then
echo "✗ 未找到 build-tools,无法校验"
FAILED=1
else
# 以**退出码**为准:不同 build-tools 版本的 apksigner 输出文案不一致
# (34.0.0 会打印 "Verifies",36.0.0 配 --print-certs 时只打印证书信息),
# 依赖日志字符串判断会导致误判。
"$BT/apksigner" verify "$RELEASE_APK" > /tmp/apk_verify.txt 2>&1
VERIFY_STATUS=$?
"$BT/apksigner" verify --print-certs "$RELEASE_APK" > /tmp/apk_sign.txt 2>/dev/null
if [ "$VERIFY_STATUS" -eq 0 ]; then
echo "✓ 签名校验通过(apksigner 退出码 0)"
grep -E "Signer #1 certificate DN" /tmp/apk_sign.txt | sed 's/^/ /' || true
if grep -q "APK Signature Scheme v2" /tmp/apk_verify.txt 2>/dev/null; then
echo " v2 签名方案:已启用"
fi
else
echo "✗ 签名校验失败(apksigner 退出码 $VERIFY_STATUS)"
cat /tmp/apk_verify.txt
FAILED=1
fi
"$BT/aapt2" dump badging "$RELEASE_APK" 2>/dev/null | head -1 | sed 's/^/ /'
if unzip -l "$RELEASE_APK" | grep -q "assets/pose_landmarker_lite.task"; then
echo "✓ MediaPipe 模型已随包分发"
else
echo "✗ APK 中缺少 MediaPipe 模型"
FAILED=1
fi
ABIS=$(unzip -l "$RELEASE_APK" | grep -oE "lib/[^/]+/" | sort -u | tr '\n' ' ')
echo "✓ 包含 ABI:$ABIS"
fi
echo ""
if [ "$FAILED" -ne 0 ]; then
echo "============================================================"
echo " 验证失败"
echo "============================================================"
exit 1
fi
echo "============================================================"
echo " 验证全部通过"
echo "============================================================"