Compare commits
7 Commits
44d60471ca
...
1b330bdd4d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b330bdd4d | |||
| 5822a01ed7 | |||
| b0bc1e9727 | |||
| 68998fa07e | |||
| d5a31e5366 | |||
| 05ad191913 | |||
| 9ecbf8d8e0 |
177
.gitea/workflows/android-build.yml
Normal file
177
.gitea/workflows/android-build.yml
Normal file
@ -0,0 +1,177 @@
|
||||
name: Android Build & Publish
|
||||
|
||||
# 仅在 android-* 分支 push 时触发 (如 android/release, android-1.0)
|
||||
# 构建 release APK (keystore 签名) 并上传到 Gitea Generic Package Registry
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'android-*'
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
# 需要写权限: 上传 packages
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
# 固定工具链版本, 便于 actions/cache 命中
|
||||
# Android 后端地址 (bake 进 APK); 与 scripts/wsl/build-apk.sh 保持一致
|
||||
VITE_API_BASE: http://bh.vps.honor3.com:3001
|
||||
|
||||
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
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: https://gitea.com/actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
# ---------- 缓存: 命中后跳过下面的安装 ----------
|
||||
- name: Cache Android SDK & NDK
|
||||
id: cache-android
|
||||
uses: https://gitea.com/actions/cache@v4
|
||||
with:
|
||||
path: ~/android-sdk
|
||||
key: android-sdk-ndk-27.2-${{ runner.os }}
|
||||
|
||||
- name: Cache Rust toolchain
|
||||
id: cache-rust
|
||||
uses: https://gitea.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
# 工具链升级时 bump 版本号使缓存失效重装 (含 cargo-ndk)
|
||||
key: rust-android-v1-${{ 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: Install Rust + Android targets
|
||||
if: steps.cache-rust.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
. "$HOME/.cargo/env"
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
|
||||
cargo install cargo-ndk --locked
|
||||
|
||||
- name: Install Android SDK & NDK
|
||||
if: steps.cache-android.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export ANDROID_HOME="$HOME/android-sdk"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends unzip
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
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 "$ANDROID_HOME/cmdline-tools/latest"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
||||
yes | sdkmanager --licenses >/dev/null || true
|
||||
sdkmanager --install \
|
||||
"platforms;android-36" \
|
||||
"build-tools;36.0.0" \
|
||||
"platform-tools" \
|
||||
"ndk;27.2.12479018" \
|
||||
"cmake;3.22.1"
|
||||
|
||||
- name: Install frontend deps
|
||||
run: npm ci --no-audit --no-fund
|
||||
|
||||
# ---------- 全局 Android 环境 (对后续所有 step 生效, 无论缓存是否命中) ----------
|
||||
- name: Set Android env
|
||||
run: |
|
||||
echo "ANDROID_HOME=$HOME/android-sdk" >> "$GITHUB_ENV"
|
||||
echo "ANDROID_NDK_HOME=$HOME/android-sdk/ndk/27.2.12479018" >> "$GITHUB_ENV"
|
||||
echo "$HOME/android-sdk/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
|
||||
echo "$HOME/android-sdk/platform-tools" >> "$GITHUB_PATH"
|
||||
|
||||
# ---------- 构建 ----------
|
||||
- name: Init Android project
|
||||
run: |
|
||||
# tauri android init 需要 cargo (运行 cargo metadata)。
|
||||
# Rust 工具链缓存命中时 "Install Rust" 步骤被跳过, 不会把 cargo 加进全局 PATH,
|
||||
# 因此这里显式加载 cargo 环境 (与 Build release APK 步骤保持一致)。
|
||||
. "$HOME/.cargo/env"
|
||||
cd src-tauri
|
||||
npx tauri android init --ci
|
||||
|
||||
- name: Inject release signing config
|
||||
run: bash scripts/ci/android-signing.sh
|
||||
env:
|
||||
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
|
||||
- name: Build release APK
|
||||
run: |
|
||||
export ANDROID_HOME="$HOME/android-sdk"
|
||||
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.2.12479018"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
|
||||
export CARGO_HOME="$HOME/.cargo"
|
||||
. "$HOME/.cargo/env"
|
||||
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: |
|
||||
# 优先 universal 已签名 APK, 找不到再回退到其他已签名 APK
|
||||
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"
|
||||
|
||||
# ---------- 上传到 Gitea Generic Package ----------
|
||||
- name: Publish APK to Gitea packages
|
||||
env:
|
||||
# 注意: 不能用 GITEA_TOKEN (内置 job token, 无组织 packages 写权限, 且命名规则禁止 GITEA_ 前缀)
|
||||
# 用一个自定义名 secret 存放有 packages 写权限的 user token
|
||||
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||
APK_PATH: ${{ steps.apk.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# 包版本: 用分支名去掉 android- 前缀 + 短 SHA, 保证每次 push 唯一
|
||||
BRANCH="${GITHUB_REF_NAME#android-}"
|
||||
PKG_VERSION="${BRANCH}-${GITHUB_SHA:0:8}"
|
||||
FILE_NAME="iboard-$(basename "$APK_PATH")"
|
||||
OWNER="${GITHUB_REPOSITORY_OWNER}"
|
||||
PKG_NAME="iboard"
|
||||
# Gitea Generic Package Registry 上传端点 (PUT)
|
||||
# 注意: GITHUB_SERVER_URL 是仓库 web 根地址, 例如 http://git.honor3.com
|
||||
URL="${GITHUB_SERVER_URL}/api/packages/${OWNER}/generic/${PKG_NAME}/${PKG_VERSION}/${FILE_NAME}"
|
||||
echo "上传到: ${URL}"
|
||||
curl -sS --fail-with-body \
|
||||
-H "Authorization: token ${PACKAGE_TOKEN}" \
|
||||
--upload-file "$APK_PATH" \
|
||||
"${URL}"
|
||||
echo ""
|
||||
echo "已发布: ${URL}"
|
||||
150
scripts/ci/android-signing.sh
Normal file
150
scripts/ci/android-signing.sh
Normal file
@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# CI 专用: 为 Tauri Android release 构建注入 release 签名配置
|
||||
# 在 `tauri android init` 之后、`tauri android build` 之前运行。
|
||||
#
|
||||
# 依赖 (由调用方注入环境变量, 不要写死密钥到脚本/仓库):
|
||||
# ANDROID_KEYSTORE_B64 - base64 编码的 keystore 文件内容 (CI secret)
|
||||
# ANDROID_KEY_ALIAS - 密钥别名 (CI secret/variable)
|
||||
# ANDROID_KEY_PASSWORD - store & key 密码 (CI secret; 本脚本两者用同一个)
|
||||
#
|
||||
# 作用:
|
||||
# 1. 把 base64 密钥解码写到 $RUNNER_TEMP/iboard-release.keystore
|
||||
# 2. 生成 gen/android/keystore.properties (Tauri 签名约定读取此文件)
|
||||
# 3. 修改 gen/android/app/build.gradle.kts, 添加 signingConfigs.release
|
||||
# 并让 buildTypes.release 使用它, 产出已签名 APK
|
||||
set -euo pipefail
|
||||
|
||||
APP_GRADLE="src-tauri/gen/android/app/build.gradle.kts"
|
||||
|
||||
echo "[signing] 注入 Android release 签名配置..."
|
||||
|
||||
# --- 1. 解码 keystore ---
|
||||
if [ -z "${ANDROID_KEYSTORE_B64:-}" ]; then
|
||||
echo "[signing] ERROR: ANDROID_KEYSTORE_B64 未设置, 无法签名 release APK" >&2
|
||||
exit 1
|
||||
fi
|
||||
: "${ANDROID_KEY_ALIAS:?ANDROID_KEY_ALIAS 未设置}"
|
||||
: "${ANDROID_KEY_PASSWORD:?ANDROID_KEY_PASSWORD 未设置}"
|
||||
|
||||
KEYSTORE_FILE="${RUNNER_TEMP:-/tmp}/iboard-release.keystore"
|
||||
echo "${ANDROID_KEYSTORE_B64}" | base64 -d > "$KEYSTORE_FILE"
|
||||
|
||||
# --- 2. 写 keystore.properties (Tauri 官方约定的签名配置文件位置) ---
|
||||
# 注意: 不能写进 git, 这是运行时生成物
|
||||
KEYSTORE_PROPERTIES="src-tauri/gen/android/keystore.properties"
|
||||
cat > "$KEYSTORE_PROPERTIES" <<EOF
|
||||
keyAlias=${ANDROID_KEY_ALIAS}
|
||||
password=${ANDROID_KEY_PASSWORD}
|
||||
storeFile=${KEYSTORE_FILE}
|
||||
EOF
|
||||
|
||||
# --- 3. 修改 build.gradle.kts 注入 signingConfigs ---
|
||||
if [ ! -f "$APP_GRADLE" ]; then
|
||||
echo "[signing] ERROR: 找不到 $APP_GRADLE (请先运行 tauri android init)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 幂等: 已注入则跳过
|
||||
if grep -q 'create("release")' "$APP_GRADLE" 2>/dev/null; then
|
||||
echo "[signing] build.gradle.kts 已有 release signingConfigs, 跳过注入"
|
||||
else
|
||||
# 在 android { 块内 buildTypes { 之前插入 signingConfigs 块
|
||||
# 用 Python 处理 Kotlin DSL, 对模板结构做最小侵入
|
||||
python3 - "$APP_GRADLE" <<'PYEOF'
|
||||
import sys, re
|
||||
|
||||
path = sys.argv[1]
|
||||
src = open(path, encoding='utf-8').read()
|
||||
|
||||
# Gradle Kotlin DSL 默认 import java.util.* (Properties 短名可用), 但没有 java.io.*
|
||||
# 因此顶部需补 import java.io.FileInputStream, 注入块内全部用短名
|
||||
if 'import java.io.FileInputStream' not in src:
|
||||
# 在第一个 import 之后插入; 若无 import 则插到文件顶部
|
||||
imp = re.search(r'^import .*$', src, flags=re.M)
|
||||
new_import = 'import java.io.FileInputStream\n'
|
||||
if imp:
|
||||
src = src[:imp.end()] + '\n' + new_import + src[imp.end():]
|
||||
else:
|
||||
src = new_import + src
|
||||
|
||||
# 定位 android { 块内的 buildTypes { (注意 Kotlin 嵌套花括号)
|
||||
m = re.search(r'(\n[ \t]*)(buildTypes\s*\{)', src)
|
||||
if not m:
|
||||
print('[signing] 未找到 buildTypes 块, 中止', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
indent = m.group(1) # 含换行, e.g. "\n "
|
||||
inner_indent = indent + ' '
|
||||
|
||||
signing_block = (
|
||||
f'{inner_indent}signingConfigs {{\n'
|
||||
f'{inner_indent} create("release") {{\n'
|
||||
f'{inner_indent} val keystorePropertiesFile = rootProject.file("keystore.properties")\n'
|
||||
f'{inner_indent} val keystoreProperties = Properties()\n'
|
||||
f'{inner_indent} if (keystorePropertiesFile.exists()) {{\n'
|
||||
f'{inner_indent} keystoreProperties.load(FileInputStream(keystorePropertiesFile))\n'
|
||||
f'{inner_indent} }}\n'
|
||||
f'{inner_indent} keyAlias = keystoreProperties["keyAlias"] as? String ?: ""\n'
|
||||
f'{inner_indent} keyPassword = keystoreProperties["password"] as? String ?: ""\n'
|
||||
f'{inner_indent} storeFile = file(keystoreProperties["storeFile"] as? String ?: "")\n'
|
||||
f'{inner_indent} storePassword = keystoreProperties["password"] as? String ?: ""\n'
|
||||
f'{inner_indent} }}\n'
|
||||
f'{inner_indent}}}\n'
|
||||
)
|
||||
|
||||
# 在 buildTypes { 前插入 signingConfigs 块
|
||||
src = src.replace(m.group(0), signing_block + m.group(0), 1)
|
||||
|
||||
# 让 buildTypes 里的 release 使用该 signingConfig
|
||||
rel = re.search(r'(\n[ \t]*)(getByName\("release"\)\s*\{)', src)
|
||||
if not rel:
|
||||
rel = re.search(r'(\n[ \t]*)(release\s*\{)', src)
|
||||
if rel:
|
||||
rel_indent = rel.group(1) + ' '
|
||||
inject = rel_indent + 'signingConfig = signingConfigs.getByName("release")\n'
|
||||
src = src.replace(rel.group(0), rel.group(0) + inject, 1)
|
||||
|
||||
open(path, 'w', encoding='utf-8').write(src)
|
||||
print('[signing] build.gradle.kts 已注入 import + signingConfigs + release signingConfig')
|
||||
PYEOF
|
||||
fi
|
||||
|
||||
# --- 4. 放行明文 HTTP (release APK 默认拦截 http://, 而服务器仅支持 HTTP) ---
|
||||
# Tauri 模板默认 debug=true, release=false。Android 9+ 会拦截 release 的所有 http:// 明文请求
|
||||
# 导致 Failed to fetch。这里把 release 的 usesCleartextTraffic 改为 "true"。
|
||||
# 幂等: 第二次运行时模板已被改为 "true", 替换无 false 可改, 也不会重复注入。
|
||||
if [ ! -f "$APP_GRADLE" ]; then
|
||||
echo "[signing] ERROR: 找不到 $APP_GRADLE (明文放行跳过)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$APP_GRADLE" <<'PYEOF'
|
||||
import sys, re
|
||||
|
||||
path = sys.argv[1]
|
||||
src = open(path, encoding='utf-8').read()
|
||||
|
||||
# 1) 把显式的 usesCleartextTraffic = "false" 全部改为 "true"
|
||||
new = re.sub(
|
||||
r'(manifestPlaceholders\["usesCleartextTraffic"\]\s*=\s*)"false"',
|
||||
r'\1"true"',
|
||||
src,
|
||||
)
|
||||
|
||||
# 2) 若模板根本没有 usesCleartextTraffic 配置, 则在 release 构建块内注入一行 true
|
||||
if 'usesCleartextTraffic' not in src:
|
||||
m = re.search(r'^([ \t]*)getByName\("release"\)\s*\{', new, flags=re.M)
|
||||
if not m:
|
||||
m = re.search(r'^([ \t]*)release\s*\{', new, flags=re.M)
|
||||
if m:
|
||||
inner = m.group(1) + ' '
|
||||
inject = f'\n{inner}manifestPlaceholders["usesCleartextTraffic"] = "true"'
|
||||
new = new.replace(m.group(0), m.group(0) + inject, 1)
|
||||
else:
|
||||
print('[signing] 警告: 未找到 release 构建块, 明文放行注入跳过', file=sys.stderr)
|
||||
|
||||
open(path, 'w', encoding='utf-8').write(new)
|
||||
print('[signing] usesCleartextTraffic 已设为 true (release 放行明文 HTTP)')
|
||||
PYEOF
|
||||
|
||||
echo "[signing] 完成: keystore.properties 已写, build.gradle.kts 已注入"
|
||||
Loading…
Reference in New Issue
Block a user