Files
local_machine/docs/connectivity/BUILD_AAB_FROM_RUSSIA_GUIDE.md
2026-03-14 20:03:57 +03:00

27 KiB
Raw Permalink Blame History

🏗️ Сборка 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. Суть проблемы
  2. Предварительные требования
  3. Шаг 1: Maven proxy на прод-сервере
  4. Шаг 2: SSH-туннель
  5. Шаг 3: Сборка AAB
  6. Шаг 4: Сборка APK для эмулятора
  7. Шаг 5: Установка в эмулятор
  8. Быстрый скрипт «всё одной командой»
  9. DOs & DON'Ts
  10. Конфигурация Gradle (что изменено и почему)
  11. Troubleshooting
  12. Версионирование

🔍 Суть проблемы

Почему 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-35
  • android-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.

Решение варианты:

  1. Использовать compileSdkVersion = 36 (android-36 = base SDK) ← текущее решение
  2. Или патчить 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 должен быть больше предыдущего релиза!

Перед новым релизом

  1. Обновить versionCode и versionName в frontend/android/app/build.gradle
  2. Собрать фронтенд (pnpm build)
  3. npx cap sync android
  4. Следовать шагам 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)