ci(android): 新增 APK 构建流水线,并修核心库脚本的跨平台断点
起因:本地 jniLibs 里躺着改名前的 libpmon_core.so(10-30 产物),
而 Dart 侧只认 libinkwatch_core.so —— 改完名没重编安卓库这条断链
一直没人发现(jniLibs 未入库,本机构建会带错库)。
新增 .gitea/workflows/android-build.yml:
- JDK 21(华为云直下,不用 apt —— 探针里 apt-get update 4 分钟无输出)
- Android SDK platform 36 / build-tools 36 / NDK 28.2.13676358
(版本取自 FlutterExtension.kt,与 Flutter 3.47.9 对齐)
- Rust + cargo-ndk + aarch64-linux-android(rsproxy)
- 每次生成一次性 keystore(仓库要求 release 必须有 key.properties,
缺了会明确失败,拒绝回落 debug 签名);job 末尾连同失败情况一并删除
- Verify APK contents:断言包内有 lib/arm64-v8a/libinkwatch_core.so、
且不含 libpmon_core.so,否则红
build_android_core.sh 三处跨平台修:
- NDK 定位增加 ANDROID_SDK_ROOT / ANDROID_HOME 回退(原先写死 macOS 路径)
- stat -f %z 在 Linux 上会失败并中断脚本 → 改用兼容取法
- nm -D --defined-only 是 GNU 参数,Darwin 用 nm -gU
- 编译前删掉旧名 libpmon_core.so,防止它被打进 APK
settings.gradle.kts 把阿里云镜像排在 google() 之前:探针实测
maven.google.com 在 runner 上连接超时,不换镜像每个插件都要先超时一轮。
This commit is contained in:
1 parent
a9bdee363d
commit
25861a5c0c
3 files changed
+253
-6
No files matched your search
@@ -0,0 +1,210 @@
|
||||
# 墨观 InkWatch · Android APK(arm64)构建验证
|
||||
#
|
||||
# 目的不是出可发行包,而是回答一个问题:**核心库(Rust)能不能被交叉编译、
|
||||
# 并真的打进 APK**。2026-11-10 实测发现本地 jniLibs 里躺着改名前的
|
||||
# libpmon_core.so(10-30 的产物),而 Dart 侧只认 libinkwatch_core.so ——
|
||||
# 也就是说"改完名没重编安卓库"这条断链一直没人发现。
|
||||
#
|
||||
# ── 触发 ────────────────────────────────────────────────────
|
||||
# ① 手动:Actions → Run workflow(需要出包时用)
|
||||
# ② push:只限 rust-core/** 与 client/android/** 与自身
|
||||
# (Dart 改动由 design-system-guard 的 Dart 测试覆盖;
|
||||
# 每次 push 都跑 20 分钟的重活没有收益,只会让人学会忽略红灯)
|
||||
#
|
||||
# ── 环境事实(2026-11-10 探针实测,勿凭猜改)─────────────────
|
||||
# dl.google.com 200 / 171ms,直连下载约 14MB/s → SDK/NDK 可直接下
|
||||
# maven.google.com 连接超时 → Gradle 仓库必须走阿里云
|
||||
# maven.aliyun.com 200 / 93~134ms → 用它的 google/public/gradle-plugin
|
||||
# mirrors.cloud.tencent 200(gradle 分发)/ gradle-wrapper 已指向它
|
||||
# repo.maven.apache.org 200 / 2s(可用但慢,作为兜底)
|
||||
# deb.debian.org 200 / 2.6s,但 apt-get update 实测 4 分钟无输出 → 不用 apt
|
||||
# rsproxy.cn 200 / 121ms → rustup 与 crates 走它
|
||||
#
|
||||
# ── 版本锚点(与 Flutter 3.47.9 / 本机对齐,改前先核对)───────
|
||||
# compileSdk 36 / targetSdk 36 / minSdk 24 / NDK 28.2.13676358
|
||||
# 来源:flutter_tools/gradle/.../FlutterExtension.kt
|
||||
#
|
||||
# ── 签名 ──────────────────────────────────────────────────
|
||||
# 仓库的 app/build.gradle.kts 明确要求 release 必须有 key.properties,
|
||||
# 缺失即失败(拒绝回落 debug 签名)。CI 里每次生成**一次性密钥**,
|
||||
# 产物仅供验证,不可发行 —— 日志与步骤名都标了 NOT FOR RELEASE。
|
||||
# 密钥材料在 job 末尾(含失败时)删除。
|
||||
|
||||
name: Android APK (arm64, CI-verified)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'rust-core/**'
|
||||
- 'client/android/**'
|
||||
- 'client/scripts/build_android_core.sh'
|
||||
- 'scripts/ci/install-flutter.sh'
|
||||
- '.gitea/workflows/android-build.yml'
|
||||
|
||||
jobs:
|
||||
apk:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
RUSTUP_DIST_SERVER: https://rsproxy.cn
|
||||
RUSTUP_UPDATE_ROOT: https://rsproxy.cn/rustup
|
||||
ANDROID_NDK_VERSION: '28.2.13676358'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Flutter (mirror archive, no jq)
|
||||
run: bash scripts/ci/install-flutter.sh
|
||||
|
||||
# 不用 apt:探针里 apt-get update 四分钟无输出,不可靠。
|
||||
# 华为云 OpenJDK 21 单文件直下(本机也是 21,与 AGP 9.1 / Kotlin 2.4 对齐)。
|
||||
- name: Setup JDK 21 (NOT FOR RELEASE)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 20 \
|
||||
-o /tmp/jdk.tar.gz \
|
||||
"https://repo.huaweicloud.com/openjdk/21.0.2/openjdk-21.0.2_linux-x64_bin.tar.gz"
|
||||
mkdir -p "$HOME/jdk"
|
||||
tar -xzf /tmp/jdk.tar.gz -C "$HOME/jdk" --strip-components=1
|
||||
rm -f /tmp/jdk.tar.gz
|
||||
echo "JAVA_HOME=$HOME/jdk" >> "$GITHUB_ENV"
|
||||
echo "$HOME/jdk/bin" >> "$GITHUB_PATH"
|
||||
export JAVA_HOME="$HOME/jdk"
|
||||
"$JAVA_HOME/bin/java" -version
|
||||
|
||||
- name: Setup Android SDK (platform 36 + build-tools 36 + NDK)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SDK="$HOME/android-sdk"
|
||||
mkdir -p "$SDK/cmdline-tools"
|
||||
curl -fsSL --retry 3 --retry-delay 5 -o /tmp/clt.zip \
|
||||
"https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip"
|
||||
unzip -q /tmp/clt.zip -d /tmp/clt
|
||||
mv /tmp/clt/cmdline-tools "$SDK/cmdline-tools/latest"
|
||||
rm -rf /tmp/clt /tmp/clt.zip
|
||||
|
||||
echo "ANDROID_SDK_ROOT=$SDK" >> "$GITHUB_ENV"
|
||||
echo "ANDROID_HOME=$SDK" >> "$GITHUB_ENV"
|
||||
echo "$SDK/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
|
||||
echo "$SDK/platform-tools" >> "$GITHUB_PATH"
|
||||
|
||||
# yes 被 sdkmanager 提前关闭会触发 SIGPIPE,故吞掉退出码
|
||||
yes | "$SDK/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null 2>&1 || true
|
||||
"$SDK/cmdline-tools/latest/bin/sdkmanager" --install \
|
||||
"platform-tools" \
|
||||
"platforms;android-36" \
|
||||
"build-tools;36.0.0" \
|
||||
"ndk;${ANDROID_NDK_VERSION}"
|
||||
echo "ANDROID_NDK_HOME=$SDK/ndk/${ANDROID_NDK_VERSION}" >> "$GITHUB_ENV"
|
||||
ls "$SDK/ndk"
|
||||
|
||||
- name: Setup Rust + cargo-ndk + android target
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.cargo"
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
'replace-with = "rsproxy-sparse"' \
|
||||
'' \
|
||||
'[source.rsproxy-sparse]' \
|
||||
'registry = "sparse+https://rsproxy.cn/index/"' \
|
||||
> "$HOME/.cargo/config.toml"
|
||||
curl -fsSL --retry 3 --retry-delay 5 -o /tmp/rustup-init.sh https://rsproxy.cn/rustup-init.sh
|
||||
sh /tmp/rustup-init.sh -y --profile minimal --default-toolchain stable
|
||||
. "$HOME/.cargo/env"
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
rustup target add aarch64-linux-android
|
||||
cargo install cargo-ndk --locked
|
||||
cargo ndk --version
|
||||
|
||||
# 仓库的 settings.gradle.kts 已把阿里云镜像排在 google() 之前(治本)。
|
||||
# 这里再补一层 init.gradle,只为覆盖**子项目(含 Flutter 的 included build)
|
||||
# 的 buildscript / 依赖仓库** —— 那些仓库不在 settings 脚本管辖内,
|
||||
# 仍会先试 google() 并超时。
|
||||
# 注意:不要在这里 clear settings.pluginManagement —— settingsEvaluated
|
||||
# 触发时插件早已开始解析,改了也来不及,反而误导后来人。
|
||||
- name: Point Gradle repos to Aliyun mirror
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.gradle"
|
||||
cat > "$HOME/.gradle/init.gradle" <<'GRADLE'
|
||||
def MIRRORS = [
|
||||
'https://maven.aliyun.com/repository/google',
|
||||
'https://maven.aliyun.com/repository/public',
|
||||
'https://maven.aliyun.com/repository/gradle-plugin',
|
||||
'https://repo.maven.apache.org/maven2',
|
||||
]
|
||||
allprojects { p ->
|
||||
[p.buildscript.repositories, p.repositories].each { handler ->
|
||||
MIRRORS.each { u -> handler.maven { url u } }
|
||||
}
|
||||
}
|
||||
GRADLE
|
||||
sed -i 's/^[[:space:]]*//' "$HOME/.gradle/init.gradle"
|
||||
echo "--- init.gradle ---"
|
||||
cat "$HOME/.gradle/init.gradle"
|
||||
|
||||
- name: Write local.properties (flutter.sdk is required by settings.gradle.kts)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf 'flutter.sdk=%s\nsdk.dir=%s\n' \
|
||||
"$HOME/development/flutter" "$HOME/android-sdk" \
|
||||
> client/android/local.properties
|
||||
cat client/android/local.properties
|
||||
|
||||
- name: Generate throwaway keystore (NOT FOR RELEASE)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
KS=/tmp/inkwatch-ci-release.jks
|
||||
"$JAVA_HOME/bin/keytool" -genkeypair -v -keystore "$KS" -storetype JKS \
|
||||
-keyalg RSA -keysize 2048 -validity 3650 -alias ci \
|
||||
-storepass ci-temp-only -keypass ci-temp-only \
|
||||
-dname "CN=CI TEMPORARY - NOT FOR RELEASE, OU=ci, O=inkwatch, L=NA, ST=NA, C=CN" \
|
||||
>/dev/null
|
||||
printf 'storeFile=%s\nstorePassword=ci-temp-only\nkeyAlias=ci\nkeyPassword=ci-temp-only\n' \
|
||||
"$KS" > client/android/key.properties
|
||||
echo "已生成一次性密钥(仅本次 job 有效,不入库、不可发行)"
|
||||
|
||||
- name: Build Rust core → jniLibs (arm64-v8a)
|
||||
run: bash client/scripts/build_android_core.sh arm64-v8a
|
||||
|
||||
- name: Build APK (release, arm64 only)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd client
|
||||
flutter pub get
|
||||
flutter build apk --release --target-platform android-arm64
|
||||
|
||||
# 关键校验:APK 里必须真有 lib/arm64-v8a/libinkwatch_core.so。
|
||||
# 2026-11-10 之前这里躺着的是改名前的 libpmon_core.so,
|
||||
# 构建全绿但装到机器上会加载失败 —— 静态校验拦这条。
|
||||
- name: Verify APK contents
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK="client/build/app/outputs/flutter-apk/app-release.apk"
|
||||
test -f "$APK" || { echo "APK 未生成" >&2; exit 1; }
|
||||
echo "=== 体积 ==="
|
||||
du -h "$APK"
|
||||
echo "=== sha256 ==="
|
||||
sha256sum "$APK"
|
||||
echo "=== 核心库 ==="
|
||||
LIST="$(unzip -l "$APK")"
|
||||
echo "$LIST" | grep -E 'lib/arm64-v8a/.*\.so' || true
|
||||
if ! echo "$LIST" | grep -q 'lib/arm64-v8a/libinkwatch_core.so'; then
|
||||
echo "✗ APK 里没有 libinkwatch_core.so —— Dart 侧会在加载阶段失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
if echo "$LIST" | grep -q 'libpmon_core.so'; then
|
||||
echo "✗ APK 里出现了改名前的 libpmon_core.so(旧产物没清干净)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ 核心库已正确打包"
|
||||
|
||||
- name: Clean up key material (always)
|
||||
if: always()
|
||||
run: |
|
||||
rm -f /tmp/inkwatch-ci-release.jks client/android/key.properties
|
||||
echo "一次性密钥材料已删除"
|
||||
@@ -11,6 +11,13 @@ pluginManagement {
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
// 阿里云镜像放在官方仓库之前(2026-11-10 加)。
|
||||
// 起因:Gitea runner 上 maven.google.com 连接超时,而 AGP/Kotlin 插件
|
||||
// 就在 google() 里 —— 若不放镜像,CI 上每个插件都要先超时一轮。
|
||||
// 本地开发机也走这里,只是更快;官方仓库留作兜底,等价可回退。
|
||||
maven("https://maven.aliyun.com/repository/google")
|
||||
maven("https://maven.aliyun.com/repository/public")
|
||||
maven("https://maven.aliyun.com/repository/gradle-plugin")
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -28,11 +28,21 @@ if [ ${#ABIS[@]} -eq 0 ]; then
|
||||
ABIS=(arm64-v8a)
|
||||
fi
|
||||
|
||||
: "${ANDROID_NDK_HOME:=$HOME/Library/Android/sdk/ndk/$(ls "$HOME/Library/Android/sdk/ndk" 2>/dev/null | sort -V | tail -1)}"
|
||||
# NDK 位置:优先显式 ANDROID_NDK_HOME,其次按 ANDROID_SDK_ROOT/ANDROID_HOME 推导,
|
||||
# 最后才回落到 macOS 的默认安装路径(2026-11-10 加后两者,为在 Linux CI 上可直接跑)。
|
||||
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||
for root in "${ANDROID_SDK_ROOT:-}" "${ANDROID_HOME:-}" "$HOME/Library/Android/sdk" "$HOME/Android/Sdk"; do
|
||||
[ -n "$root" ] || continue
|
||||
if [ -d "$root/ndk" ]; then
|
||||
ANDROID_NDK_HOME="$root/ndk/$(ls "$root/ndk" | sort -V | tail -1)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
export ANDROID_NDK_HOME
|
||||
if [ ! -d "$ANDROID_NDK_HOME" ]; then
|
||||
echo "找不到 NDK: $ANDROID_NDK_HOME" >&2
|
||||
echo "请先安装:sdkmanager --install 'ndk/30.0.16248370'" >&2
|
||||
if [ ! -d "${ANDROID_NDK_HOME:-}" ]; then
|
||||
echo "找不到 NDK: ${ANDROID_NDK_HOME:-<未设置>}" >&2
|
||||
echo "请先安装:sdkmanager --install 'ndk/28.2.13676358'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -44,6 +54,13 @@ fi
|
||||
JNI_LIBS="$JNI_LIBS"
|
||||
mkdir -p "$JNI_LIBS"
|
||||
|
||||
# 删掉改名前的旧产物(pmon → inkwatch 时漏了重编 Android 库,2026-11-10 发现):
|
||||
# Gradle 会把 jniLibs 下**所有** .so 打进 APK,留着旧名只会让包变大、
|
||||
# 且给排查"到底加载了哪个库"添乱 —— Dart 侧只认 libinkwatch_core.so。
|
||||
for abi in "${ABIS[@]}"; do
|
||||
rm -f "$JNI_LIBS/$abi/libpmon_core.so"
|
||||
done
|
||||
|
||||
echo "NDK : $ANDROID_NDK_HOME"
|
||||
echo "目标 : ${ABIS[*]}(平台 API 24)"
|
||||
echo "输出 : $JNI_LIBS"
|
||||
@@ -58,12 +75,25 @@ cargo ndk -P 24 "${TARGETS[@]}" -o "$JNI_LIBS" build --release --lib
|
||||
|
||||
echo
|
||||
echo "产物:"
|
||||
# 跨平台取文件大小 / 导出符号数(macOS 的 stat 与 nm 参数与 GNU 版不同,
|
||||
# 2026-11-10 加:原先硬写 `stat -f`,在 Linux CI 上会直接报错并中断脚本)
|
||||
fsize_bytes() {
|
||||
if stat -f %z "$1" >/dev/null 2>&1; then stat -f %z "$1"; else stat -c %s "$1"; fi
|
||||
}
|
||||
count_exported() {
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
nm -gU "$1" 2>/dev/null
|
||||
else
|
||||
nm -D --defined-only "$1" 2>/dev/null
|
||||
fi | grep -c 'inkwatch_' || true
|
||||
}
|
||||
|
||||
for abi in "${ABIS[@]}"; do
|
||||
so="$JNI_LIBS/$abi/libinkwatch_core.so"
|
||||
if [ -f "$so" ]; then
|
||||
printf " %-14s %6s KB %s\n" "$abi" "$(( $(stat -f %z "$so") / 1024 ))" \
|
||||
printf " %-14s %6s KB %s\n" "$abi" "$(( $(fsize_bytes "$so") / 1024 ))" \
|
||||
"$(file -b "$so" | cut -d, -f1-2)"
|
||||
printf " %-14s 导出符号 %s 个\n" "" "$(nm -D --defined-only "$so" | grep -c ' inkwatch_')"
|
||||
printf " %-14s 导出符号 %s 个\n" "" "$(count_exported "$so")"
|
||||
else
|
||||
printf " %-14s ✗ 未生成\n" "$abi"
|
||||
fi
|
||||
|
||||
Reference in new issue
Block a user