name: Android Build & Publish # 每次提交触发 (所有分支 push; 主分支提交会同时触发 docker-build.yml 自动部署) # 构建 release APK (keystore 签名) 并上传到 Cloudflare R2 (S3 兼容 API) # R2 凭据从配置注册中心 (bh.vps.honor3.com:8008, CommonExternalService, 分组 cloudflare) 读取 # 参考: D:\workbench\iboard\.gitea\workflows\android-build.yml on: push: jobs: build-android: runs-on: ubuntu-latest permissions: contents: read env: # 固定工具链版本, 便于 actions/cache 命中 NDK_VERSION: '27.2.12479018' # libduckdb-sys 0.10.2 build.rs 同时添加 -stdlib=libc++ 和 -stdlib=libstdc++, # 在 clang 上两者都被支持, libstdc++ 覆盖 libc++ 成为生效选项, 但 NDK 不含 libstdc++ 头文件, # 导致 fatal error: 'string' file not found. 设置 CXXFLAGS 确保 -stdlib=libc++ 为最终生效值 # (cc 1.4.5 在 compile_object 最后追加 CXXFLAGS, 能覆盖 build.rs 的 flag_if_supported) CXXFLAGS: '-stdlib=libc++' # 配置中心地址与 token (Gitea secrets; URL 未设时回退默认地址) CONFIG_CENTER_URL: '${{ secrets.CONFIG_CENTER_URL }}' CONFIG_CENTER_TOKEN: '${{ secrets.CONFIG_CENTER_TOKEN }}' REGISTRY_URL_FALLBACK: 'http://bh.vps.honor3.com:8008' # 配置中心项目 ID: 签名密钥在 service-wardrobe/signing, R2 在 CommonExternalService/cloudflare CFG_PROJECT_SIGNING: 'proj-mtwjnvir-k52zvp' CFG_PROJECT_R2: 'proj-mts0s5jo-fb3rz7' steps: - name: Checkout uses: https://gitea.com/actions/checkout@v4 - name: Setup Node uses: https://gitea.com/actions/setup-node@v4 with: node-version: '22' - name: Setup JDK 17 uses: https://gitea.com/actions/setup-java@v4 with: distribution: temurin java-version: '17' - name: Setup Rust toolchain # 不用 gitea.com/actions/mise (该公共 action 需鉴权), 直接装 rustup run: | set -euo pipefail if ! command -v rustup >/dev/null 2>&1; then curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal fi echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" "$HOME/.cargo/bin/rustup" default stable "$HOME/.cargo/bin/rustc" --version "$HOME/.cargo/bin/cargo" --version # ---------- 尽量复用 Android SDK ---------- # 优先复用 runner 预装的 SDK (/usr/local/lib/android/sdk), 其次命中 CI 缓存, # 最后才重新下载。NDK 不在预装中, 单独用缓存复用。 - name: Locate reusable Android SDK id: sdk run: | CANDIDATES=( "$ANDROID_HOME" "/usr/local/lib/android/sdk" # GitHub ubuntu-latest 预装 "$HOME/android-sdk" ) for c in "${CANDIDATES[@]}"; do if [ -n "$c" ] && [ -d "$c/platforms" ]; then echo "ANDROID_ROOT=${c}" >> "$GITHUB_ENV" echo "found=${c}" >> "$GITHUB_OUTPUT" echo "reusing existing Android SDK: ${c}" break fi done echo "found=${found:-}" - name: Cache Android NDK id: cache-ndk uses: https://gitea.com/actions/cache@v4 with: path: ~/android-sdk/ndk key: android-ndk-${{ env.NDK_VERSION }}-${{ runner.os }} - name: Cache Rust toolchain id: cache-rust uses: https://gitea.com/actions/cache@v4 with: path: | ~/.cargo ~/.rustup key: rustup-stable-android-v2-${{ runner.os }} - name: Cache Tauri cargo target uses: https://gitea.com/actions/cache@v4 with: path: src-tauri/target key: tauri-target-android-${{ runner.os }} # ---------- 安装缺失组件 (仅未复用/未命中时) ---------- - name: Ensure Android SDK & NDK if: steps.sdk.outputs.found == '' || steps.cache-ndk.outputs.cache-hit != 'true' run: | set -euo pipefail BASE="${ANDROID_ROOT:-$HOME/android-sdk}" COMMANDLINE_TOOLS="$BASE/cmdline-tools/latest" echo "SDK base: $BASE" mkdir -p "$BASE/cmdline-tools" # 预装 SDK 目录常归 root, 兜底给当前用户写权限 sudo chown -R "$(whoami)" "$BASE" 2>/dev/null || true if [ ! -x "$COMMANDLINE_TOOLS/bin/sdkmanager" ]; then sudo apt-get update sudo apt-get install -y --no-install-recommends unzip wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip unzip -q /tmp/cmdline-tools.zip -d /tmp/ mv /tmp/cmdline-tools "$COMMANDLINE_TOOLS" fi export PATH="$COMMANDLINE_TOOLS/bin:$BASE/platform-tools:$PATH" yes | sdkmanager --licenses >/dev/null || true # 复用预装但缺平台时补装; 全新安装时补全组件 if [ ! -d "$BASE/platforms/android-36" ]; then sdkmanager --install "platforms;android-36" "build-tools;36.0.0" "platform-tools" fi if [ ! -d "$BASE/ndk/${{ env.NDK_VERSION }}" ]; then sdkmanager --install "ndk;${{ env.NDK_VERSION }}" else echo "NDK ${{ env.NDK_VERSION }} already present" fi - name: Add Android Rust targets run: | rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android cargo install cargo-ndk --locked || true - name: Install frontend deps run: npm ci --no-audit --no-fund # ---------- 全局 Android 环境 ---------- - name: Set Android env run: | echo "ANDROID_HOME=${ANDROID_ROOT:-$HOME/android-sdk}" >> "$GITHUB_ENV" echo "ANDROID_NDK_HOME=${ANDROID_ROOT:-$HOME/android-sdk}/ndk/${{ env.NDK_VERSION }}" >> "$GITHUB_ENV" echo "${ANDROID_ROOT:-$HOME/android-sdk}/cmdline-tools/latest/bin" >> "$GITHUB_PATH" echo "${ANDROID_ROOT:-$HOME/android-sdk}/platform-tools" >> "$GITHUB_PATH" # ---------- 构建 ---------- - name: Init Android project run: | cd src-tauri npx tauri android init --ci - name: Fetch config center (signing + R2) run: | set -euo pipefail export CONFIG_CENTER_URL="${CONFIG_CENTER_URL:-$REGISTRY_URL_FALLBACK}" python3 - <<'PYEOF' import json, os, urllib.request url = os.environ["CONFIG_CENTER_URL"] token = os.environ.get("CONFIG_CENTER_TOKEN", "") or "" wanted_keys = { "CFG_PROJECT_SIGNING": ("ANDROID_KEYSTORE_B64", "ANDROID_KEY_ALIAS", "ANDROID_KEY_PASSWORD"), "CFG_PROJECT_R2": ("CF_API_TOKEN", "CF_R2_PUBLIC_URL"), } wanted = {} # 兼容 key 带分组前缀 (如 CommonExternalService 下是 r2/CF_API_TOKEN) def match(cfg_key, token): return cfg_key == token or cfg_key.rsplit("/", 1)[-1] == token for pid_env, keys in wanted_keys.items(): pid = os.environ[pid_env] req = urllib.request.Request(f"{url}/api/projects/{pid}/configs") if token: req.add_header("Authorization", "Bearer " + token) with urllib.request.urlopen(req, timeout=20) as r: data = json.load(r).get("data", []) for cfg in data: cfg_key = cfg.get("key", "") for wanted_key in keys: if match(cfg_key, wanted_key): wanted[wanted_key] = cfg.get("value", "") with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as f: for k, v in wanted.items(): f.write(f"{k}={v}\n") missing = [k for k in ("ANDROID_KEYSTORE_B64", "ANDROID_KEY_ALIAS", "ANDROID_KEY_PASSWORD", "CF_API_TOKEN") if k not in wanted] if missing: raise SystemExit("配置中心缺少密钥: " + ", ".join(missing)) PYEOF - name: Inject release signing config run: bash scripts/ci/android-signing.sh - name: Build release APK run: | export ANDROID_HOME="${ANDROID_ROOT:-$HOME/android-sdk}" export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/${{ env.NDK_VERSION }}" export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH" cd src-tauri npx tauri android build --apk # 断言签名配置仍在且 release 块实际引用了它 (防止 tauri 重新生成 gen/android 或正则未命中) if ! grep -q 'create("release")' gen/android/app/build.gradle.kts || ! grep -q 'signingConfig = signingConfigs.getByName("release")' gen/android/app/build.gradle.kts; then echo "ERROR: 构建后签名配置丢失/未生效, APK 未签名" >&2 exit 1 fi - name: Locate APK id: apk run: | APK="" UNIVERSAL=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name '*.apk' ! -name '*unsigned*' 2>/dev/null | head -1) if [ -n "$UNIVERSAL" ]; then APK="$UNIVERSAL" else APK=$(find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' ! -name '*unsigned*' | head -1) fi if [ -z "$APK" ]; then echo "ERROR: 未找到已签名 APK" >&2 exit 1 fi echo "path=$APK" >> "$GITHUB_OUTPUT" ls -lh "$APK" - name: Upload APK to Cloudflare R2 env: APK_PATH: ${{ steps.apk.outputs.path }} run: | bash scripts/upload-r2.sh "$APK_PATH" data/android/wardrobe.apk