#!/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" <&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 已注入"