ZaStoGram/TMessagesProj_AppStandalone/build.gradle
loop-uh a4145e909a
Some checks failed
Build three ZaStoGram APKs / build (arm64-v8a, ZaStoGram-standalone-arm64-v8a, Arm64, arm64) (push) Has been cancelled
Build three ZaStoGram APKs / build (armeabi-v7a, ZaStoGram-standalone-armeabi-v7a, Armv7, armv7) (push) Has been cancelled
Build three ZaStoGram APKs / build (x86, ZaStoGram-standalone-x86, X86, x86) (push) Has been cancelled
ZaStoGram source guards / guards (push) Has been cancelled
Не подписывать стабильные сборки запасным ключом
В проекте два хранилища ключей: настоящий релизный лежит вне репозитория
и подключается переменной ZASTO_RELEASE_KEYSTORE, а в самом репозитории
есть запасной с другим сертификатом. Если переменную не передать, сборка
молча брала запасной — так вышли 1.1.13 и 1.1.14, подписанные ключом
a08d7dc3 вместо 84315d38. Android запрещает обновление приложения со
сменившейся подписью и сообщает об этом лишь фразой «Приложение не
установлено», а Play Protect вдобавок помечает неизвестного подписанта
как угрозу — обе жалобы пользователя объясняются этим.

Теперь стабильная сборка без релизного ключа падает с внятной ошибкой,
а не подменяет ключ. Параметры подписи дополнительно принимаются как
свойства Gradle: System.getenv в скрипте сборки читает окружение демона,
а не команды, поэтому экспортированная переменная может не примениться —
та же ловушка, что была с номером сборки.

Добавлена проверка отпечатка сертификата собранного APK и её вызов из
конвейера, чтобы чужой ключ нельзя было выпустить незамеченным.

Заодно исправлен спам proxy_warmup в логах: дедупликация не действовала
на решения allow, а её кэш состоял из одной ячейки, поэтому чередующиеся
записи от разных аккаунтов вытесняли друг друга. Теперь окно у каждого
ключа своё, а число свёрнутых повторов печатается как repeated=N, чтобы
записи не пропадали молча.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:09:19 +03:00

332 lines
13 KiB
Groovy

apply plugin: 'com.android.application'
def zastoReleaseIdentity = rootProject.ext.zastoReleaseIdentity
def zastoUpdateChannel = zastoReleaseIdentity.updateChannel
def zastoBuildNumber = zastoReleaseIdentity.buildNumber
def zastoApplicationId = zastoUpdateChannel == "dev" ? "${APP_PACKAGE}.dev" : APP_PACKAGE
def zastoApplicationName = zastoUpdateChannel == "dev" ? "ZaStoGram Dev" : "ZaStoGram"
// Signing inputs accept a Gradle property as well as the environment variable.
// System.getenv() inside a build script reports the DAEMON's environment, not
// the one of the command that started the build, so an exported variable can
// silently fail to apply while a -P property always reaches this code.
def zastoSigningInput = { String propertyName, String envName ->
return (findProperty(propertyName) ?: System.getenv(envName))?.toString()?.trim() ?: null
}
def zastoReadSigningSecret = { String propertyName, String envName ->
def secretPath = zastoSigningInput(propertyName, envName)
if (!secretPath) {
return null
}
def secretFile = file(secretPath)
if (!secretFile.isFile()) {
throw new GradleException("ZaStoGram signing secret file does not exist: ${secretPath}")
}
return secretFile.getText("UTF-8").trim()
}
def zastoReleaseKeystorePath = zastoSigningInput("zastoReleaseKeystore", "ZASTO_RELEASE_KEYSTORE")
def zastoReleaseStorePassword = zastoReadSigningSecret("zastoReleaseStorePasswordFile", "ZASTO_RELEASE_STORE_PASSWORD_FILE")
def zastoReleaseKeyPassword = zastoReadSigningSecret("zastoReleaseKeyPasswordFile", "ZASTO_RELEASE_KEY_PASSWORD_FILE")
def zastoReleaseKeyAlias = zastoReadSigningSecret("zastoReleaseKeyAliasFile", "ZASTO_RELEASE_KEY_ALIAS_FILE")
def zastoExternalSigningValues = [
zastoReleaseKeystorePath,
zastoReleaseStorePassword,
zastoReleaseKeyPassword,
zastoReleaseKeyAlias,
]
if (zastoExternalSigningValues.any { it } && !zastoExternalSigningValues.every { it }) {
throw new GradleException("ZaStoGram external signing requires the keystore and all three credential files")
}
// A stable build MUST carry the real release identity. The in-repo keystore is
// a different certificate, and Android refuses to update an installed app whose
// signature changed - it only says "App not installed", and Play Protect flags
// the unknown signer on top. Releases 1.1.13 and 1.1.14 shipped that way
// because the fallback applied silently; refusing to build is the only way this
// cannot happen again.
if (zastoUpdateChannel == "stable" && !zastoExternalSigningValues.every { it }) {
throw new GradleException(
"Refusing a stable ZaStoGram build without its release signing key. " +
"Pass -PzastoReleaseKeystore, -PzastoReleaseStorePasswordFile, " +
"-PzastoReleaseKeyPasswordFile and -PzastoReleaseKeyAliasFile " +
"(or the matching ZASTO_RELEASE_* environment variables).")
}
def zastoReleaseKeystoreFile = zastoReleaseKeystorePath
? file(zastoReleaseKeystorePath)
: file("../TMessagesProj/config/release.keystore")
if (zastoReleaseKeystorePath && !zastoReleaseKeystoreFile.isFile()) {
throw new GradleException("ZaStoGram release keystore does not exist: ${zastoReleaseKeystorePath}")
}
repositories {
mavenCentral()
google()
}
configurations {
compile.exclude module: 'support-v4'
}
configurations.all {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'androidx.recyclerview', module: 'recyclerview'
}
dependencies {
implementation project(':TMessagesProj')
implementation 'androidx.fragment:fragment:1.8.9'
implementation 'androidx.core:core:1.16.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation files('../TMessagesProj/libs/libgsaverification-client.aar')
}
android {
compileSdkVersion 35
buildToolsVersion '35.0.0'
defaultConfig.applicationId = zastoApplicationId
sourceSets.main.jniLibs.srcDirs = ['../TMessagesProj/jni/']
lintOptions {
disable 'MissingTranslation'
disable 'ExtraTranslation'
disable 'BlockedPrivateApi'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
signingConfigs {
debug {
storeFile zastoReleaseKeystoreFile
storePassword zastoReleaseStorePassword ?: RELEASE_STORE_PASSWORD
keyAlias zastoReleaseKeyAlias ?: RELEASE_KEY_ALIAS
keyPassword zastoReleaseKeyPassword ?: RELEASE_KEY_PASSWORD
}
release {
storeFile zastoReleaseKeystoreFile
storePassword zastoReleaseStorePassword ?: RELEASE_STORE_PASSWORD
keyAlias zastoReleaseKeyAlias ?: RELEASE_KEY_ALIAS
keyPassword zastoReleaseKeyPassword ?: RELEASE_KEY_PASSWORD
}
}
buildTypes {
debug {
debuggable true
jniDebuggable true
signingConfig signingConfigs.debug
applicationIdSuffix ".web"
minifyEnabled false
multiDexEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), '../TMessagesProj/proguard-rules.pro', '../TMessagesProj/proguard-rules-beta.pro'
ndk.debugSymbolLevel = 'FULL'
}
standalone {
debuggable false
jniDebuggable false
signingConfig signingConfigs.release
minifyEnabled true
multiDexEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), '../TMessagesProj/proguard-rules.pro'
ndk.debugSymbolLevel = 'FULL'
}
}
sourceSets.debug {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
sourceSets.standalone {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
flavorDimensions "minApi"
productFlavors {
afat {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
ext {
abiVersionCode = 9
}
sourceSets.standalone {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
}
arm64 {
ndk {
abiFilters "arm64-v8a"
}
ext {
abiVersionCode = 11
}
sourceSets.standalone {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
}
armv7 {
ndk {
abiFilters "armeabi-v7a"
}
ext {
abiVersionCode = 12
}
sourceSets.standalone {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
}
x86 {
ndk {
abiFilters "x86"
}
ext {
abiVersionCode = 13
}
sourceSets.standalone {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
}
x64 {
ndk {
abiFilters "x86_64"
}
ext {
abiVersionCode = 14
}
sourceSets.standalone {
manifest.srcFile '../TMessagesProj/config/release/AndroidManifest_standalone.xml'
}
}
}
defaultConfig.versionCode = Integer.parseInt(APP_VERSION_CODE)
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "app.apk"
// Reserve five decimal digits per upstream Telegram version: four for the
// Forgejo Actions run and one for the ABI. A newer dev prerelease can then
// be installed over the previous prerelease even when APP_VERSION_CODE did
// not change between workflow runs.
def abiVersionDigit = variant.productFlavors.get(0).abiVersionCode % 10
output.versionCodeOverride = defaultConfig.versionCode * 100000 + zastoBuildNumber * 10 + abiVersionDigit
}
}
def standaloneBuildFlavors = ["afat", "arm64", "armv7", "x86", "x64"]
variantFilter { variant ->
def names = variant.flavors*.name
if (variant.buildType.name != "release" && !names.any { standaloneBuildFlavors.contains(it) }) {
setIgnore(true)
}
}
defaultConfig {
minSdkVersion 24 // ZaStoGram: raised for Chaquopy (plugin engine); must match library
targetSdkVersion 35
versionName APP_VERSION_NAME
ndkVersion "27.2.12479018"
// Stable and dev are independent installed applications. Keep their
// package, visible label and updater channel derived from one identity.
resValue "string", "ZastoApplicationName", zastoApplicationName
// Keep identity in resources rather than BuildConfig. BuildConfig always embeds the
// changing VERSION_CODE, which would invalidate the expensive R8 output each release.
resValue "string", "ZastoUpdateChannel", zastoUpdateChannel
resValue "string", "ZastoReleaseTag", zastoReleaseIdentity.releaseTag
resValue "string", "ZastoForgejoRepository", zastoReleaseIdentity.forgejoRepository
resValue "integer", "ZastoBuildNumber", zastoBuildNumber.toString()
multiDexEnabled true
vectorDrawables.generatedDensities = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi']
externalNativeBuild {
cmake {
version '3.10.2'
arguments '-DANDROID_STL=c++_static', '-DANDROID_PLATFORM=android-21' //, '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON'
}
}
}
buildFeatures {
buildConfig = false
}
namespace 'org.telegram.messenger.web'
lintOptions {
checkReleaseBuilds false
}
}
// A second line of defense after packaging: even a custom or abbreviated Gradle
// invocation must never leave behind an ABI-specific APK containing Chaquopy
// runtime assets for another architecture.
def zastoStandaloneAbiApks = [
Arm64: [outputDir: "arm64", abi: "arm64-v8a"],
Armv7: [outputDir: "armv7", abi: "armeabi-v7a"],
X86: [outputDir: "x86", abi: "x86"],
X64: [outputDir: "x64", abi: "x86_64"],
]
def zastoValidNativeAbis = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]
zastoStandaloneAbiApks.each { flavor, spec ->
def assembleTaskName = "assemble${flavor}Standalone"
def verifyTask = tasks.register("verify${flavor}StandaloneAbiIsolation") {
group = "verification"
description = "Rejects foreign native and Chaquopy ABI payload in ${flavor} standalone APK"
dependsOn assembleTaskName
doLast {
File apkFile = file("$buildDir/outputs/apk/${spec.outputDir}/standalone/app.apk")
if (!apkFile.isFile()) {
throw new GradleException("Standalone APK is missing: ${apkFile}")
}
def foreignEntries = []
def hasExpectedNative = false
def hasExpectedChaquopy = false
def archive = new java.util.zip.ZipFile(apkFile)
try {
def entries = archive.entries()
while (entries.hasMoreElements()) {
def name = entries.nextElement().name
zastoValidNativeAbis.each { abi ->
def nativeEntry = name.startsWith("lib/${abi}/")
def chaquopyBootstrap = name.startsWith("assets/chaquopy/bootstrap-native/${abi}/")
def chaquopyArchive = name == "assets/chaquopy/requirements-${abi}.imy" ||
name == "assets/chaquopy/stdlib-${abi}.imy"
if (nativeEntry || chaquopyBootstrap || chaquopyArchive) {
if (abi != spec.abi) {
foreignEntries.add(name)
} else {
hasExpectedNative |= nativeEntry
hasExpectedChaquopy |= chaquopyBootstrap
}
}
}
}
} finally {
archive.close()
}
if (!foreignEntries.isEmpty() || !hasExpectedNative || !hasExpectedChaquopy) {
throw new GradleException(
"APK ABI isolation failed for ${apkFile}: foreign ABI payload=${foreignEntries.take(12)}, " +
"expected native=${hasExpectedNative}, expected Chaquopy=${hasExpectedChaquopy}"
)
}
}
}
tasks.matching { it.name == assembleTaskName }.configureEach {
finalizedBy verifyTask
}
}
apply plugin: 'com.google.gms.google-services'