27 KiB
🏗️ Сборка AAB/APK из России — Полное руководство
Дата создания: 2026-02-28 Проблема: Java TLS заблокирован российским DPI → Gradle не может скачать зависимости Решение: Maven proxy на прод-сервере + SSH-туннель + обход SDK auto-download Время сборки: ~1.5 мин (AAB), ~46 сек (APK после AAB)
📖 См. также: SETUP_AND_BUILD_GUIDE.md — общее руководство по установке и сборке (для работы не из России)
📋 Содержание
- Суть проблемы
- Предварительные требования
- Шаг 1: Maven proxy на прод-сервере
- Шаг 2: SSH-туннель
- Шаг 3: Сборка AAB
- Шаг 4: Сборка APK для эмулятора
- Шаг 5: Установка в эмулятор
- Быстрый скрипт «всё одной командой»
- DOs & DON'Ts
- Конфигурация Gradle (что изменено и почему)
- Troubleshooting
- Версионирование
🔍 Суть проблемы
Почему Gradle не работает из России напрямую
Gradle → Java TLS → dl.google.com / repo1.maven.org
↑
DPI (Deep Packet Inspection) блокирует TLS handshake
Java получает: "Remote host terminated the handshake"
Ключевой факт: curl работает нормально (macOS LibreSSL), а Java — нет (JSSE TLS implementation). DPI по-разному обрабатывает TLS Client Hello от разных реализаций.
Решение: Maven proxy
Gradle → HTTP (127.0.0.1:8765) → SSH-туннель → Прод-сервер (python proxy)
↓
HTTPS → dl.google.com
HTTPS → repo1.maven.org
HTTPS → plugins.gradle.org
Java работает по HTTP (без TLS) до localhost. Прод-сервер в Digital Ocean скачивает по HTTPS без DPI.
✅ Предварительные требования
📖 Перед началом: Убедитесь, что выполнены базовые требования из SETUP_AND_BUILD_GUIDE.md:
- Java OpenJDK 21+ установлен
- Android SDK установлен (через Android Studio GUI)
- Build Tools установлены
- Эмулятор настроен (см. EMULATOR_STARTUP_GUIDE.md)
Локальная машина (macOS)
| Компонент | Версия | Путь |
|---|---|---|
| Java (OpenJDK) | 21.0.9 | /opt/homebrew/opt/openjdk@21 |
| Android SDK | API 34-36 | $HOME/Library/Android/sdk |
| Build Tools | 35.0.1 | $HOME/Library/Android/sdk/build-tools/35.0.1 |
| Gradle Wrapper | 8.11.1 | frontend/android/gradle/wrapper/ |
| AGP | 8.7.2 | frontend/android/build.gradle |
| Capacitor | 7.4.x | frontend/package.json |
Прод-сервер
| Компонент | Значение |
|---|---|
| SSH хост | hunab-prod (209.38.32.21) |
| Python | 3.12+ (предустановлен) |
| Порт proxy | 8765 (localhost) |
Проверка готовности
# Java
/opt/homebrew/opt/openjdk@21/bin/java -version
# → openjdk version "21.0.9"
# Android SDK
ls $HOME/Library/Android/sdk/platforms/
# → android-34 android-35 android-36
# Build Tools
ls $HOME/Library/Android/sdk/build-tools/
# → 35.0.1 36.1.0 (34.0.0 и 35.0.0 УДАЛЕНЫ — были пустые!)
# SSH к проду
ssh hunab-prod "echo OK"
# → OK
🔧 Шаг 1: Maven proxy на прод-сервере
1.1 Скопировать скрипт прокси на прод
Создать файл /tmp/maven-proxy.py локально:
cat > /tmp/maven-proxy.py << 'PYEOF'
#!/usr/bin/env python3
"""Многопоточный Maven reverse proxy для Gradle.
Обходит Java TLS/DPI блокировку при сборке Android из РФ.
Поддерживает GET и HEAD запросы (Gradle использует оба).
"""
import http.server, socketserver, urllib.request, ssl, sys
REPOS = {
'/google/': 'https://dl.google.com/dl/android/maven2/',
'/central/': 'https://repo1.maven.org/maven2/',
'/gradle/': 'https://plugins.gradle.org/m2/',
}
class H(http.server.BaseHTTPRequestHandler):
def _p(self, m='GET'):
for pfx, base in REPOS.items():
if self.path.startswith(pfx):
url = base + self.path[len(pfx):]
try:
r = urllib.request.urlopen(
urllib.request.Request(url, headers={'User-Agent':'Gradle'}, method=m),
timeout=300,
context=ssl.create_default_context()
)
d = r.read() if m=='GET' else b''
self.send_response(200)
self.send_header('Content-Type', r.headers.get('Content-Type','application/octet-stream'))
self.send_header('Content-Length', r.headers.get('Content-Length',str(len(d))))
self.end_headers()
if m=='GET': self.wfile.write(d)
except urllib.error.HTTPError as e:
self.send_response(e.code); self.end_headers()
except Exception as e:
sys.stderr.write(f"ERR {m} {url}: {e}\n"); sys.stderr.flush()
self.send_response(502); self.end_headers()
return
self.send_response(404); self.end_headers()
def do_GET(self): self._p('GET')
def do_HEAD(self): self._p('HEAD')
def log_message(self, fmt, *a):
sys.stderr.write(f"{a[0]}\n"); sys.stderr.flush()
class S(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True; allow_reuse_address = True
if __name__ == '__main__':
print("Maven proxy v2 on :8765 (threaded+logging)")
sys.stdout.flush()
S(('127.0.0.1',8765),H).serve_forever()
PYEOF
Скопировать на прод:
scp /tmp/maven-proxy.py hunab-prod:/tmp/maven-proxy.py
1.2 Запустить прокси на проде
# Убить предыдущий экземпляр (НЕЛЬЗЯ использовать pkill -f maven-proxy — убьёт SSH сессию!)
ssh hunab-prod "ps aux | grep maven-proxy | grep python | grep -v grep | awk '{print \$2}' | xargs -r kill"
sleep 1
# Запустить
ssh hunab-prod "nohup python3 /tmp/maven-proxy.py > /tmp/maven-proxy.log 2>&1 & echo PID=\$!"
1.3 Проверить прокси на проде
ssh hunab-prod "curl -s -I http://127.0.0.1:8765/google/com/android/tools/build/gradle/8.7.2/gradle-8.7.2.pom | head -2"
# Ожидаемый ответ:
# HTTP/1.0 200 OK
# Server: BaseHTTP/0.6 Python/3.12.7
🔧 Шаг 2: SSH-туннель
2.1 Создать туннель
# Убить старые туннели
pkill -f "ssh.*-L.*8765" 2>/dev/null
# Новый туннель (фоновый режим)
ssh -f -N -L 8765:127.0.0.1:8765 hunab-prod
2.2 Проверить туннель локально
curl -s -I http://127.0.0.1:8765/google/com/android/tools/build/gradle/8.7.2/gradle-8.7.2.pom | head -2
# Ожидаемый ответ:
# HTTP/1.0 200 OK
# Server: BaseHTTP/0.6 Python/3.12.7
⚠️ Если ответ пустой или "Connection refused" — прокси на проде не запущен или туннель не работает. Повторите шаги 1.2 и 2.1.
🔧 Шаг 3: Сборка AAB
3.1 Установка переменных окружения
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
export PATH="$JAVA_HOME/bin:$PATH"
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools
3.2 Сборка фронтенда (если не собран)
cd /Users/eternal/code/hunabapp-dev/frontend
pnpm build
3.3 Синхронизация Capacitor (если не сделано)
cd /Users/eternal/code/hunabapp-dev/frontend
npx cap sync android
3.4 Сборка AAB
cd /Users/eternal/code/hunabapp-dev/frontend/android
./gradlew clean bundleRelease --no-daemon --console=plain
Ожидаемое время: ~1 мин 20 сек
3.5 Результат
ls -lh ./app/build/outputs/bundle/release/app-release.aab
# → 37M app-release.aab
3.6 Копирование в temp/
VERSION_NAME=$(grep 'versionName' ./app/build.gradle | grep -oE '"[^"]*"' | tr -d '"')
VERSION_CODE=$(grep 'versionCode' ./app/build.gradle | grep -oE '[0-9]+')
mkdir -p /Users/eternal/code/hunabapp-dev/temp/android
cp ./app/build/outputs/bundle/release/app-release.aab \
/Users/eternal/code/hunabapp-dev/temp/android/hunab-app-v${VERSION_NAME}-v${VERSION_CODE}-release.aab
echo "📦 AAB: temp/android/hunab-app-v${VERSION_NAME}-v${VERSION_CODE}-release.aab"
🔧 Шаг 4: Сборка APK для эмулятора
AAB нельзя установить в эмулятор напрямую — нужен APK.
cd /Users/eternal/code/hunabapp-dev/frontend/android
./gradlew assembleRelease --no-daemon --console=plain
Ожидаемое время: ~46 сек (после AAB сборки — кэш уже разогрет)
Результат:
cp ./app/build/outputs/apk/release/app-release.apk \
/Users/eternal/code/hunabapp-dev/temp/android/hunab-app-v${VERSION_NAME}-release.apk
🔧 Шаг 5: Установка в эмулятор
5.1 Запуск эмулятора
# Список доступных AVD
$ANDROID_HOME/emulator/emulator -list-avds
# → Pixel_5_API_35
# Запуск (в фоне)
$ANDROID_HOME/emulator/emulator -avd Pixel_5_API_35 -no-snapshot-load -no-snapshot-save &
# Ожидание загрузки (30-60 сек)
adb wait-for-device
adb shell getprop sys.boot_completed
# → 1 (когда загружено)
5.2 Установка APK
adb install -r /Users/eternal/code/hunabapp-dev/temp/android/hunab-app-v${VERSION_NAME}-release.apk
5.3 Запуск приложения
adb shell am start -n com.hunab.app/.MainActivity
🚀 Быстрый скрипт «всё одной командой»
Предполагает: прокси уже запущен и SSH-туннель активен (шаги 1-2).
#!/bin/bash
set -e
# === Настройка ===
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
export PATH="$JAVA_HOME/bin:$PATH"
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator
PROJECT_ROOT="/Users/eternal/code/hunabapp-dev"
# === Проверка прокси ===
echo "🔍 Проверяю Maven proxy..."
if ! curl -s -I http://127.0.0.1:8765/google/com/android/tools/build/gradle/8.7.2/gradle-8.7.2.pom | grep -q "200 OK"; then
echo "❌ Maven proxy не работает! Запустите шаги 1-2 из BUILD_AAB_FROM_RUSSIA_GUIDE.md"
exit 1
fi
echo "✅ Proxy OK"
# === Сборка фронтенда ===
echo "🔨 Сборка фронтенда..."
cd "$PROJECT_ROOT/frontend"
pnpm build
# === Capacitor sync ===
echo "🔄 Capacitor sync..."
npx cap sync android
# === Версия ===
cd android
VERSION_NAME=$(grep 'versionName' ./app/build.gradle | grep -oE '"[^"]*"' | tr -d '"')
VERSION_CODE=$(grep 'versionCode' ./app/build.gradle | grep -oE '[0-9]+')
echo "📊 Version: $VERSION_NAME (code: $VERSION_CODE)"
# === AAB ===
echo "📦 Сборка AAB..."
./gradlew clean bundleRelease --no-daemon --console=plain
mkdir -p "$PROJECT_ROOT/temp/android"
cp ./app/build/outputs/bundle/release/app-release.aab \
"$PROJECT_ROOT/temp/android/hunab-app-v${VERSION_NAME}-v${VERSION_CODE}-release.aab"
echo "✅ AAB: temp/android/hunab-app-v${VERSION_NAME}-v${VERSION_CODE}-release.aab"
# === APK (для эмулятора) ===
echo "📱 Сборка APK..."
./gradlew assembleRelease --no-daemon --console=plain
cp ./app/build/outputs/apk/release/app-release.apk \
"$PROJECT_ROOT/temp/android/hunab-app-v${VERSION_NAME}-release.apk"
# === Эмулятор ===
echo "📲 Установка в эмулятор..."
if adb devices 2>/dev/null | grep -q "device$"; then
adb install -r "$PROJECT_ROOT/temp/android/hunab-app-v${VERSION_NAME}-release.apk"
adb shell am start -n com.hunab.app/.MainActivity
echo "🎮 Приложение запущено!"
else
echo "⚠️ Эмулятор не запущен. Запустите вручную и повторите установку."
fi
ls -lh "$PROJECT_ROOT/temp/android/"
echo ""
echo "✅✅✅ ГОТОВО!"
🚫 DOs & DON'Ts
✅ DO (ДЕЛАЙ)
| # | Правило | Почему |
|---|---|---|
| 1 | Используй Maven proxy через SSH-туннель | Java TLS заблокирован DPI в РФ |
| 2 | ТОЛЬКО прокси repos в build.gradle (без google(), mavenCentral()) |
Прямые HTTPS repos зависают навсегда |
| 3 | allowInsecureProtocol = true для proxy repos |
Прокси работает по HTTP, Gradle 8+ блокирует HTTP по умолчанию |
| 4 | android.builder.sdkDownload=false в gradle.properties |
SDK auto-download зависает на TLS |
| 5 | compileSdkVersion = 36 в variables.gradle |
android-36 — base SDK, корректно установлен |
| 6 | buildToolsVersion "35.0.1" явно в app/build.gradle |
34.0.0 и 35.0.0 не установлены (пустые папки были удалены) |
| 7 | Global buildToolsVersion override через subprojects{} в root build.gradle |
Capacitor модули тоже требуют build tools |
| 8 | android.suppressUnsupportedCompileSdk=36 в gradle.properties |
Убирает warning от AGP 8.7.2 |
| 9 | -Djdk.tls.client.protocols=TLSv1.2 в org.gradle.jvmargs |
Частично помогает с DPI для прямых Java TLS |
| 10 | IsBaseSdk=true в source.properties platforms |
Gradle не находит extension SDK без манифеста |
| 11 | ThreadingMixIn в Python proxy |
Gradle делает десятки параллельных запросов |
| 12 | HEAD + GET в proxy | Gradle использует HEAD для проверки существования артефактов |
| 13 | Фоновая сборка при запуске из Cursor | ./gradlew через Cursor tool таймаутится через ~2 мин |
| 14 | **Убивай pkill через `ps aux | grep |
❌ DON'T (НЕ ДЕЛАЙ)
| # | Правило | Почему |
|---|---|---|
| 1 | НЕ оставляй google() / mavenCentral() в repositories |
Gradle попробует их → зависнет на TLS handshake на НЕОПРЕДЕЛЁННОЕ время |
| 2 | НЕ используй pkill -f maven-proxy через SSH |
Паттерн совпадает с самой SSH командой → убивает SSH сессию (exit 255) |
| 3 | НЕ используй однопоточный Python HTTP server | Gradle делает 10-50 параллельных запросов → очередь → таймаут |
| 4 | НЕ используй SOCKS proxy для Gradle (socksProxyHost) |
Java TLS через SOCKS тоже блокируется DPI |
| 5 | НЕ пытайся скачать Gradle cache на проде и скопировать | Кэш привязан к хэшам и абсолютным путям — не переносим |
| 6 | НЕ устанавливай Android SDK через sdkmanager из РФ | Тот же TLS блок. Устанавливай через Android Studio GUI (у студии свои настройки) |
| 7 | НЕ удаляй android.builder.sdkDownload=false |
Gradle будет пытаться скачать SDK манифесты → зависнет |
| 8 | НЕ используй compileSdkVersion = 35 |
Установленный android-35 — extension SDK, Gradle без манифеста не может его найти |
| 9 | НЕ забывай buildToolsVersion в subprojects |
Capacitor-android и другие модули тоже требуют build tools |
| 10 | НЕ создавай APK для Google Play | Google Play требует только AAB формат |
| 11 | НЕ оставляй пустые build-tools директории | Gradle пытается их использовать → "Build Tools not found" |
| 12 | НЕ запускай Gradle через tool call Cursor напрямую | Таймаут ~2 мин; запускай в фоне через nohup ... & |
⚙️ Конфигурация Gradle (что изменено и почему)
frontend/android/build.gradle (корневой)
buildscript {
repositories {
// ТОЛЬКО прокси — прямые repos заблокированы DPI
maven { url 'http://127.0.0.1:8765/google/'; allowInsecureProtocol = true }
maven { url 'http://127.0.0.1:8765/central/'; allowInsecureProtocol = true }
maven { url 'http://127.0.0.1:8765/gradle/'; allowInsecureProtocol = true }
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
}
}
allprojects {
repositories {
// ТОЛЬКО прокси
maven { url 'http://127.0.0.1:8765/google/'; allowInsecureProtocol = true }
maven { url 'http://127.0.0.1:8765/central/'; allowInsecureProtocol = true }
maven { url 'http://127.0.0.1:8765/gradle/'; allowInsecureProtocol = true }
}
}
// Глобальный override buildToolsVersion для ВСЕХ модулей
subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android.buildToolsVersion = "35.0.1"
}
}
}
Почему:
allowInsecureProtocol = true— Gradle 8+ блокирует HTTP repos по умолчанию- Три прокси: Google Maven, Maven Central, Gradle Plugin Portal
subprojects { afterEvaluate }— Capacitor модули не указывают buildToolsVersion явно, AGP 8.7.2 по умолчанию ищет 34.0.0 (не установлен)
frontend/android/gradle.properties
org.gradle.jvmargs=-Xmx1536m -Djdk.tls.client.protocols=TLSv1.2
android.useAndroidX=true
android.builder.sdkDownload=false
android.suppressUnsupportedCompileSdk=36
Почему:
-Djdk.tls.client.protocols=TLSv1.2— принудительный TLS 1.2 (частично обходит DPI)sdkDownload=false— блокирует авто-загрузку SDK (зависает на TLS)suppressUnsupportedCompileSdk=36— AGP 8.7.2 не знает о SDK 36, подавляем warning
frontend/android/variables.gradle
ext {
compileSdkVersion = 36 // Было 35! android-35 — extension SDK, не находится без манифеста
targetSdkVersion = 35 // Оставляем 35 для совместимости с Google Play
// ...
}
Почему:
android-35установлен как extension SDK (path:platforms;android-35-ext15) — Gradle без сетевого манифеста не может его распознать какandroid-35android-36установлен как base SDK (path:platforms;android-36,IsBaseSdk=true) — работаетtargetSdkVersionостаётся 35 — это то, что Google Play учитывает
frontend/android/app/build.gradle
android {
compileSdk rootProject.ext.compileSdkVersion
buildToolsVersion "35.0.1" // Добавлено явно
// ...
}
SDK Platform Patches
$HOME/Library/Android/sdk/platforms/android-35/source.properties:
AndroidVersion.IsBaseSdk=true # Было false — Gradle не находил SDK без манифеста
$HOME/Library/Android/sdk/platforms/android-35/package.xml:
<localPackage path="platforms;android-35" ...> <!-- Было: platforms;android-35-ext15 -->
⚠️ Аналогичные изменения для android-34
🔧 Troubleshooting
Проблема: «Still waiting for package manifests to be fetched remotely»
Причина: android.builder.sdkDownload=true (или не задан) → Gradle пытается скачать манифесты по HTTPS → DPI блокирует.
Решение:
# gradle.properties
android.builder.sdkDownload=false
Проблема: «Failed to find target with hash string 'android-35'»
Причина: Установленный SDK — extension SDK (IsBaseSdk=false, path android-35-ext15), а Gradle ищет base SDK android-35.
Решение варианты:
- Использовать
compileSdkVersion = 36(android-36 = base SDK) ← текущее решение - Или патчить
source.properties:IsBaseSdk=true+package.xml:path="platforms;android-35"
Проблема: «Failed to find Build Tools revision 34.0.0»
Причина: AGP 8.7.2 по умолчанию требует build-tools 34.0.0. При sdkDownload=false не может скачать.
Решение: Явно указать установленную версию в app/build.gradle и глобально через subprojects в root build.gradle:
buildToolsVersion "35.0.1"
Проблема: Gradle зависает, прокси лог пустой (0 строк)
Причина: Gradle обращается к google() / mavenCentral() напрямую, минуя прокси.
Решение: Убрать google() и mavenCentral() из repositories. Оставить ТОЛЬКО proxy URLs.
Проблема: SSH exit code 255 при pkill
Причина: pkill -f maven-proxy совпадает с самой SSH командой и убивает сессию.
Решение:
ssh hunab-prod "ps aux | grep maven-proxy | grep python | grep -v grep | awk '{print \$2}' | xargs -r kill"
Проблема: Cursor tool timeout при сборке
Причина: Gradle билд занимает ~1.5 мин, Cursor tool таймаутится через ~2 мин.
Решение: Запускать в фоне:
nohup bash -c '
cd /path/to/frontend/android
./gradlew bundleRelease --no-daemon --console=plain 2>&1 | tee /tmp/aab-build.log
' &>/dev/null &
# Мониторинг
tail -f /tmp/aab-build.log
Проблема: Однопоточный proxy таймаутится
Причина: Python http.server.HTTPServer обрабатывает 1 запрос за раз. Gradle делает 10-50 параллельных запросов.
Решение: ThreadingMixIn:
class S(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
allow_reuse_address = True
Проблема: Proxy возвращает 404 на HEAD запросы
Причина: BaseHTTPRequestHandler по умолчанию не имеет do_HEAD.
Решение: Добавить do_HEAD:
def do_HEAD(self): self._p('HEAD')
📊 Версионирование
Текущая версия (2026-02-28)
| Параметр | Значение |
|---|---|
| versionName | 1.3.8 |
| versionCode | 10308 |
| compileSdkVersion | 36 |
| targetSdkVersion | 35 |
| minSdkVersion | 23 |
| buildToolsVersion | 35.0.1 |
| AGP | 8.7.2 |
| Gradle | 8.11.1 |
| Java | OpenJDK 21.0.9 |
| Capacitor | 7.4.x |
Для Google Play
- AAB файл:
temp/android/hunab-app-v1.3.8-v10308-release.aab - Размер: ~37 MB
- Подписан:
hunab-app.keystore(release signing config) - Формат: AAB (NEVER APK for Google Play!)
- versionCode должен быть больше предыдущего релиза!
Перед новым релизом
- Обновить
versionCodeиversionNameвfrontend/android/app/build.gradle - Собрать фронтенд (
pnpm build) npx cap sync android- Следовать шагам 1-4 этого руководства
📁 Связанные файлы
| Файл | Назначение |
|---|---|
frontend/android/build.gradle |
Корневой Gradle — repos, plugins, subprojects override |
frontend/android/app/build.gradle |
App модуль — version, signing, buildTools |
frontend/android/gradle.properties |
JVM args, SDK download, TLS, suppressWarnings |
frontend/android/variables.gradle |
compileSdk, targetSdk, dependency versions |
docs/android/guides/SETUP_AND_BUILD_GUIDE.md |
Общее руководство по установке и сборке (для работы не из России) |
docs/android/guides/EMULATOR_STARTUP_GUIDE.md |
Полное руководство по запуску эмулятора |
docs/android/TROUBLESHOOTING.md |
Общий troubleshooting (включая другие проблемы) |
docs/android/MOBILE_BUILD_REGULATIONS.md |
Регламент сборки мобильного приложения |
scripts/android/quick-build-and-launch.sh |
Старый скрипт быстрой сборки (требует адаптации под proxy) |
Последнее обновление: 2026-02-28 Автор: Aura (Cursor AI Agent) Проверено: Успешная сборка AAB v1.3.8 (BUILD SUCCESSFUL in 1m 21s, 112 tasks)