From 2260719ff7b600654f009d92f522dbff6e7102be Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 2 Nov 2026 15:03:46 +0100 Subject: [PATCH] Add signed Android release build scripts --- tools/chatgui-android/.gitignore | 3 + tools/chatgui-android/AGENTS.md | 16 ++++++ tools/chatgui-android/app/build.gradle.kts | 35 +++++++++++ tools/chatgui-android/build.sh | 64 ++++++++++++++++----- tools/chatgui-android/build_release.sh | 4 ++ tools/chatgui-android/create_signing_key.sh | 47 +++++++++++++++ 6 files changed, 155 insertions(+), 14 deletions(-) create mode 100755 tools/chatgui-android/build_release.sh create mode 100755 tools/chatgui-android/create_signing_key.sh diff --git a/tools/chatgui-android/.gitignore b/tools/chatgui-android/.gitignore index 1febfa344..2ed6ff5e3 100644 --- a/tools/chatgui-android/.gitignore +++ b/tools/chatgui-android/.gitignore @@ -7,3 +7,6 @@ build_headless/ .idea/ local.properties .cxx/ + +# Локальный ключ релизной подписи и пароли. +/signing/ diff --git a/tools/chatgui-android/AGENTS.md b/tools/chatgui-android/AGENTS.md index 66a10b630..162777a17 100644 --- a/tools/chatgui-android/AGENTS.md +++ b/tools/chatgui-android/AGENTS.md @@ -155,8 +155,24 @@ sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" ./build.sh # clean + сборка + установка на все подключённые устройства ./build.sh noinstall # только сборка, без прошивки ./build.sh # clean + сборка + прошивка на конкретный девайс +./create_signing_key.sh # однократно создать ключ релизной подписи +./build.sh --release noinstall # clean + сборка подписанного релизного APK +./build_release.sh # clean + подписанный релиз + установка на все подключённые устройства +./build_release.sh # то же, установка на конкретный девайс ``` +Ключ RSA 3072 в PKCS12 (`signing/release.p12`) и случайный пароль +(`signing/keystore.properties`) создаются автоматически, без ввода пароля в командную строку. +Каталог `signing/` исключён из Git; права каталога — 700, файлов — 600. +Сохранить резервную копию **всего каталога `signing/`**: этот ключ нужен для дальнейших обновлений приложения. +Повторное создание поверх существующего каталога запрещено скриптом. +Для генерации нужны JDK 17+ (`keytool`, учитывается `JAVA_HOME`) и `openssl`. + +Релизный APK: `app/build/outputs/apk/release/app-release.apk`. +Прямой вызов `./gradlew :app:assembleRelease` также подписывает APK этим ключом; +без настроек подписи release завершается ошибкой. Debug не требует релизного ключа. +`./build.sh --release ` собирает и устанавливает релиз на выбранное устройство. + ### APK (Android) ```bash diff --git a/tools/chatgui-android/app/build.gradle.kts b/tools/chatgui-android/app/build.gradle.kts index bf246f1f7..e88df5c09 100644 --- a/tools/chatgui-android/app/build.gradle.kts +++ b/tools/chatgui-android/app/build.gradle.kts @@ -1,9 +1,30 @@ +import java.util.Properties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") } +// Ключ и пароли создаёт create_signing_key.sh; пути считаются от каталога настроек. +val releaseSigningFile = rootProject.file("signing/keystore.properties") +val releaseSigning = Properties().apply { + if (releaseSigningFile.isFile) releaseSigningFile.inputStream().use { load(it) } +} +val validateReleaseSigning by tasks.registering { + doLast { + check(releaseSigningFile.isFile) { + "Release signing settings missing: $releaseSigningFile. Run ./create_signing_key.sh first." + } + for (key in listOf("storeFile", "storePassword", "keyAlias", "keyPassword")) { + check(!releaseSigning.getProperty(key).isNullOrBlank()) { "Release signing: missing $key in $releaseSigningFile" } + } + val keystore = releaseSigningFile.parentFile.resolve(releaseSigning.getProperty("storeFile")) + check(keystore.isFile) { "Release signing keystore not found: $keystore" } + logger.lifecycle("Release signing: keystore={}, alias={}", keystore, releaseSigning.getProperty("keyAlias")) + } +} + // Фон берём из desktop-чатгуи; drawable-nodpi сохраняет исходный размер плитки. val prepareChatBackground by tasks.registering(Copy::class) { from("../../chatgui/resources/bg.png") @@ -68,12 +89,26 @@ android { } } + signingConfigs { + create("release") { + if (releaseSigningFile.isFile) { + storeFile = releaseSigning.getProperty("storeFile")?.let { releaseSigningFile.parentFile.resolve(it) } + storePassword = releaseSigning.getProperty("storePassword") + keyAlias = releaseSigning.getProperty("keyAlias") + keyPassword = releaseSigning.getProperty("keyPassword") + storeType = "PKCS12" + } + } + } + buildTypes { release { isMinifyEnabled = false + signingConfig = signingConfigs.getByName("release") } } } +tasks.matching { it.name == "preReleaseBuild" }.configureEach { dependsOn(validateReleaseSigning) } // Настоящий JNI callback с отказами JVM; Linux host нужен для ELF --gc-sections, NDK ABI отдельно проверяет сборка APK. val testJniLog by tasks.registering(Exec::class) { diff --git a/tools/chatgui-android/build.sh b/tools/chatgui-android/build.sh index c80b86d8c..ea563b1a4 100755 --- a/tools/chatgui-android/build.sh +++ b/tools/chatgui-android/build.sh @@ -1,24 +1,60 @@ #!/bin/bash -set -eo pipefail +set -euo pipefail cd "$(dirname "$0")" export ANDROID_HOME="${ANDROID_HOME:-/home/user/android}" -APK=app/build/outputs/apk/debug/app-debug.apk - -echo "Building..." -TMP=$(mktemp) -./gradlew clean > /dev/null 2>&1 -if ! ./gradlew assembleDebug > "$TMP" 2>&1; then - cat "$TMP" - rm -f "$TMP" +BUILD_TYPE=Debug +NOINSTALL=0 +DEVICE="" +for arg in "$@"; do + case "$arg" in + --release) BUILD_TYPE=Release ;; + noinstall|--no-install) NOINSTALL=1 ;; + --help|-h) + echo "Usage: $0 [--release] [noinstall | DEVICE_SERIAL]" + echo "Default: clean debug build and install on all connected devices." + echo "Release: create the key once with ./create_signing_key.sh; then use --release noinstall." + exit 0 + ;; + -*) echo "ERROR: Unknown option: $arg. Use --help." >&2; exit 1 ;; + *) + if [[ -n "$DEVICE" ]]; then + echo "ERROR: Only one device serial may be specified." >&2 + exit 1 + fi + DEVICE="$arg" + ;; + esac +done +if [[ "$NOINSTALL" == 1 && -n "$DEVICE" ]]; then + echo "ERROR: Specify either noinstall or a device serial." >&2 exit 1 fi -rm -f "$TMP" -echo "Build OK" +if [[ "$BUILD_TYPE" == Release && ! -f signing/keystore.properties ]]; then + echo "ERROR: Release signing settings missing. Run ./create_signing_key.sh first." >&2 + exit 1 +fi +VARIANT="${BUILD_TYPE,,}" +APK="app/build/outputs/apk/$VARIANT/app-$VARIANT.apk" +trap 'echo "ERROR: Android build/install failed at line $LINENO." >&2' ERR -[ "$1" = "noinstall" ] && exit 0 +echo "Building $BUILD_TYPE APK..." +TMP=$(mktemp) +trap 'rm -f "$TMP"' EXIT +if ! ./gradlew clean ":app:assemble$BUILD_TYPE" > "$TMP" 2>&1; then + cat "$TMP" + echo "ERROR: Gradle $BUILD_TYPE build failed." >&2 + exit 1 +fi +if [[ ! -f "$APK" ]]; then + echo "ERROR: Gradle succeeded but APK not found: $APK" >&2 + exit 1 +fi +echo "Build OK: $PWD/$APK" -if [ -n "$1" ]; then - DEVICES=("$1") +[[ "$NOINSTALL" == 1 ]] && exit 0 + +if [[ -n "$DEVICE" ]]; then + DEVICES=("$DEVICE") else mapfile -t DEVICES < <(adb devices | awk 'NR>1 && $2=="device"{print $1}') fi diff --git a/tools/chatgui-android/build_release.sh b/tools/chatgui-android/build_release.sh new file mode 100755 index 000000000..e810194c1 --- /dev/null +++ b/tools/chatgui-android/build_release.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Сборка и установка подписанного релиза; аргументы передаются общему скрипту сборки. +set -euo pipefail +exec "$(dirname "$0")/build.sh" --release "$@" diff --git a/tools/chatgui-android/create_signing_key.sh b/tools/chatgui-android/create_signing_key.sh new file mode 100755 index 000000000..492ecdef1 --- /dev/null +++ b/tools/chatgui-android/create_signing_key.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Однократно создаёт ключ релизной подписи и локальные настройки Gradle. +set -euo pipefail +cd "$(dirname "$0")" + +if [[ "${1:-}" == "--help" ]]; then + echo "Usage: $0" + echo "Creates signing/release.p12 and signing/keystore.properties (excluded from Git)." + exit 0 +fi +if [[ $# -ne 0 ]]; then + echo "ERROR: Unexpected arguments. Use $0 --help." >&2 + exit 1 +fi + +KEYTOOL="${JAVA_HOME:+$JAVA_HOME/bin/}keytool" +for tool in "$KEYTOOL" openssl; do + if ! command -v "$tool" > /dev/null; then + echo "ERROR: Required tool not found: $tool (install JDK 17+ and OpenSSL)." >&2 + exit 1 + fi +done +if [[ -e signing || -L signing ]]; then + echo "ERROR: signing already exists; refusing to replace the release key or its settings." >&2 + exit 1 +fi + +umask 077 +mkdir signing +trap 'echo "ERROR: Key creation failed at line $LINENO; inspect signing/ before retrying." >&2' ERR +export UTUN_KEYSTORE_PASSWORD +UTUN_KEYSTORE_PASSWORD=$(openssl rand -hex 32) +# Сохраняем пароль до генерации ключа, чтобы ошибка записи не оставила ключ без пароля. +cat > signing/keystore.properties <