ci(android): 新增 APK 构建流水线,并修核心库脚本的跨平台断点
Android APK (arm64, CI-verified) / apk (push) Failing after 7m55s
Design System Guard / guard (push) Successful in 5m49s

起因:本地 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:
PMon Dev committed 2026-11-10 15:50:20 +08:00
1 parent a9bdee363d
commit 25861a5c0c
3 files changed
+253 -6

No files matched your search

+210
View File
@@ -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 "一次性密钥材料已删除"
+7
View File
@@ -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()
+36 -6
View File
@@ -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