Add signed Android release build scripts

This commit is contained in:
evgeny committed 2026-11-02 15:03:46 +01:00
1 parent 3166ee7a41
commit 2260719ff7
6 files changed
+155 -14

No files matched your search

+3
View File
@@ -7,3 +7,6 @@ build_headless/
.idea/
local.properties
.cxx/
# Локальный ключ релизной подписи и пароли.
/signing/
+16
View File
@@ -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 <SN> # clean + сборка + прошивка на конкретный девайс
./create_signing_key.sh # однократно создать ключ релизной подписи
./build.sh --release noinstall # clean + сборка подписанного релизного APK
./build_release.sh # clean + подписанный релиз + установка на все подключённые устройства
./build_release.sh <SN> # то же, установка на конкретный девайс
```
Ключ 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 <SN>` собирает и устанавливает релиз на выбранное устройство.
### APK (Android)
```bash
@@ -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) {
+50 -14
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# Сборка и установка подписанного релиза; аргументы передаются общему скрипту сборки.
set -euo pipefail
exec "$(dirname "$0")/build.sh" --release "$@"
+47
View File
@@ -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 <<EOF
storeFile=release.p12
storePassword=$UTUN_KEYSTORE_PASSWORD
keyAlias=utun-release
keyPassword=$UTUN_KEYSTORE_PASSWORD
EOF
echo "Creating release signing key: signing/release.p12 (RSA 3072, validity 10000 days)..."
"$KEYTOOL" -genkeypair -noprompt -keystore signing/release.p12 -storetype PKCS12 \
-alias utun-release -keyalg RSA -keysize 3072 -sigalg SHA256withRSA -validity 10000 \
-dname "CN=uTun Android Release" -storepass:env UTUN_KEYSTORE_PASSWORD -keypass:env UTUN_KEYSTORE_PASSWORD
unset UTUN_KEYSTORE_PASSWORD
echo "Release signing key and settings created; permissions: directory 700, files 600."
echo "Back up the entire signing/ directory securely; keep it for all future application updates."
echo "Build a signed APK: ./build.sh --release noinstall"