Версионирование нативного мобильного кода в git (Android overlay)
Дата: 2026-05-31 Статус: проектирование Затрагивает: kontinuum-veil, kontinuum-app, kontinuum-tether (submodules)
Проблема
Tauri 2 генерирует Android-проект в <app>/.../gen/android, который везде в .gitignore. Рукописный нативный слой (Kotlin VpnService, JNI-bridge, правки манифеста), живущий внутри gen/android, не версионируется и теряется при rm -rf gen/android + tauri android init (регенерация после переименования identifier com.continuum.* → com.kontinuum.*).
Конкретный инцидент: при починке Android-сборки Veil каталог gen/android был удалён и пересоздан как голая болванка. Рукописный Kotlin-слой split-tunneling (window.VeilVpn bridge) исчез безвозвратно — его не было в git. Симптом в приложении: страница Split tunneling открывается, но показывает «No apps match» вместо списка приложений, потому что window.VeilVpn не инжектируется в WebView.
Существующий обходной путь у kontinuum-app — scripts/patch-android.sh (heredoc целого MainActivity.kt + sed-вставки в манифест, запуск вручную) — подвержен той же потере и хрупок.
Цели
- Версионировать рукописный нативный код (Android сейчас, iOS позже) в git как реальные файлы (diff/review/IDE), а не sed-патчи.
- Механизм переживает регенерацию
gen/(tauri android init). - Единый подход для трёх приложений и двух платформ; нативный слой будет расти.
- Сгенерированный мусор (gradle-wrapper jar,
build/, кэши) остаётся вне git. - Восстановить утерянный split-tunnel слой Veil.
Не-цели (YAGNI)
- Не коммитим весь
gen/(бинарники, build-артефакты). - Не отказываемся от
cargo tauri android buildради отдельного Android Studio проекта. - iOS-слой сейчас не пишем — только закладываем структуру
native/apple/. - Не меняем существующее поведение
kontinuum-appAPK (только переносим источник правды). - Quick Settings tile («шторка») и auto-connect on boot — НЕ в первой итерации. Запуск headless-VPN из шторки (
VeilTileService) и по загрузке (VeilBootReceiver) выносятся в отдельную вторую итерацию (см. ниже). В первой итерации восстанавливаем только JNI-инфраструктуру под них (VeilHeadlessNative.kt), чтобы вторая итерация свелась к добавлению tile/boot-классов без переделок.
Подход A: overlay-каталог + sync-скрипт (выбран)
Структура
В каждом приложении, рядом с tauri-проектом (backend/ у app и veil, client/ у tether):
<tauri-root>/native/
android/
overlay/ # зеркалит gen/android/ файл-в-файл
app/src/main/AndroidManifest.xml
app/src/main/java/<pkg>/MainActivity.kt
app/src/main/java/<pkg>/... # рукописные .kt
app/build.gradle.kts # если нужны правки
README.md
apple/ # будущее, та же схема (overlay/ под gen/apple/)Инвариант: путь файла внутри overlay/ идентичен пути внутри gen/android/. sync рекурсивно копирует overlay/* → gen/android/*, перезаписывая. Различие «чисто рукописный» (нет в генерации) vs «замещающий» (tauri генерит болванку) на уровне механизма не важно — overlay одинаково копирует оба. Для гибридных файлов (AndroidManifest.xml) храним полную желаемую версию (стратегия «целиком замещать», утверждена).
sync-скрипт
scripts/sync-native.sh (один на приложение, рядом с android-envs.sh). Аргумент: android (default) | apple.
Шаги:
- Резолв путей:
TAURI_ROOT,GEN=<root>/gen/<platform>,OVERLAY=<root>/native/<platform>/overlay. - Если
GENнет →tauri <platform> init(черезandroid-envs.sh). - Проверка рассинхрона identifier: вычислить ожидаемый java-package path из
identifierвtauri.conf.json; если соответствующего каталога вGENнет (симптом «Project directory … does not exist») → бэкапcp -r "$GEN" /tmp/gen-<platform>-backup-$$затемrm -rf "$GEN"+tauri <platform> init. rsync -a "$OVERLAY"/ "$GEN"/— накатить overlay.- Вывести список наложенных файлов.
Скрипт идемпотентен: повторный запуск ничего не ломает.
Интеграция в nx
В project.json каждого приложения добавить таргет sync:android и подключить его к build:android:
"sync:android": {
"executor": "nx:run-commands",
"options": {
"command": "bash ../scripts/sync-native.sh android",
"cwd": "{projectRoot}"
}
},
"build:android": {
"options": { "command": "eval \"$(bash ../scripts/android-envs.sh --export)\" && cargo tauri android build --target aarch64", "cwd": "{projectRoot}" },
"dependsOn": ["^build", "gen:bindings", "sync:android"]
}(У tether путь cwd/скриптов — client/; уточнить при реализации.)
После этого nx run veil-backend:build:android сам синхронизирует overlay → собирает. patch-android.sh удаляется.
.gitignore
gen/ остаётся ignored. native/ — НЕ ignored (явно проверить, что правило /backend/gen не задевает native/; при необходимости добавить !native/). Бинарные артефакты в overlay не кладём.
Восстановление split-tunnel слоя Veil
Воссоздаём в kontinuum-veil/backend/native/android/overlay/. Опора: Rust JNI-сигнатуры (backend/src/headless/android.rs, целы) и фронт-контракт.
Контракт window.VeilVpn (13 методов, из frontend)
Split tunnel (entities/splitTunnel/useSplitTunnelStore.ts):
listInstalledApps(): string— JSON[{package,label,system}]черезPackageManager(требуетQUERY_ALL_PACKAGES).getSplitTunnelMode(): string/setSplitTunnelMode(mode)—'off'|'allowlist'|'denylist'.getSplitTunnelPackages(): string/setSplitTunnelPackages(json)— JSON-массив package-имён.
VPN lifecycle (entities/connection/useConnectionStore.ts):
startVpn(port, user, pass, serverIp)— поднятьVeilVpnService(TUN → SOCKS5 от Rust-бэкенда).stopVpn()/isVpnRunning(): boolean.
Settings (pages/settings/SettingsPage.vue):
isLocationEnabled()/enableLocation()/disableLocation().isStartOnBootEnabled()/setStartOnBootEnabled(bool).
Файлы overlay
MainActivity.kt— наследникTauriActivity; регистрирует bridge в WebView:webView.addJavascriptInterface(VeilBridge(this), "VeilVpn"). (Точку получения WebView уточнить — у tauri этоonWebViewCreate/аналог.)bridge/VeilBridge.kt—@JavascriptInterface-методы (13 шт. выше). Split-tunnel prefs вSharedPreferences.listInstalledAppsчерезPackageManager.VeilVpnService.kt—VpnService: строит TUN, применяет split-tunnel (addAllowedApplication/addDisallowedApplicationпо mode+packages), запускает проксирование через TUN-JNI (backend/src/tun/android.rs) на SOCKS-порт изstartVpn.VeilHeadlessNative.kt—external funдекларации под существующиеJava_com_kontinuum_veil_VeilHeadlessNative_*+System.loadLibrary. Восстанавливается в первой итерации как инфраструктура; реальные потребители (tile/boot) приходят во второй итерации. Сейчас его использует только bridge-методisStartOnBootEnabled/setStartOnBootEnabled(persist в prefs, без фактического boot-receiver).VeilBootReceiver.kt/VeilTileService.kt— start-on-boot и Quick Settings tile. Вторая итерация (см. раздел ниже), в первой итерации НЕ создаются.AndroidManifest.xml—QUERY_ALL_PACKAGES,<service android:name=".VeilVpnService" android:permission="android.permission.BIND_VPN_SERVICE">с<intent-filter>наandroid.net.VpnService, нужные permissions; FOREGROUND_SERVICE для VPN-нотификации.app/build.gradle.kts— если нужны доп. зависимости.
Граница UI ↔ натив (важно)
Основной коннект из UI идёт через Tauri-команды (vpn_connect/vpn_connect_auto/vpn_connect_force → get_proxy_credentials), затем JS зовёт VeilVpn.startVpn(...) чтобы поднять TUN. JNI-путь (VeilHeadlessNative) — только для headless (tile/boot). То есть Rust-бэкенд уже даёт SOCKS5; задача нативного слоя — TUN+split-tunnel поверх него и enumeration приложений. Это снижает объём: бизнес-логику VPN заново писать не нужно.
Вторая итерация: Quick Settings tile + start-on-boot
Отдельный последующий этап (не входит в первую итерацию восстановления). Цель — паритет с утерянной версией: поднимать VPN headless (~10ms, без запуска WebView/UI) из «шторки» и по загрузке устройства. Опирается на уже восстановленный в первой итерации VeilHeadlessNative.kt (JNI → HeadlessBackend в Rust).
Состав:
VeilTileService.kt—TileService: по тапу в шторке вызываетVeilHeadlessNative.initBackend()→connectActiveServer()→ поднимаетVeilVpnServiceс полученными SOCKS-кредами; отражает состояние (on/off) на плитке.VeilBootReceiver.kt—BroadcastReceiverнаBOOT_COMPLETED: еслиisStartOnBootEnabled→ тот же headless-путь. Требует, чтобы VPN-разрешение было выдано ранее через UI (система не покажет диалог из boot-broadcast).AndroidManifest.xml— регистрация<service>tile (BIND_QUICK_SETTINGS_TILE) и<receiver>boot (RECEIVE_BOOT_COMPLETED).
Всё это кладётся в тот же native/android/overlay/ и подхватывается sync-механизмом — отдельных изменений в инфраструктуре не требуется.
Миграция kontinuum-app
Перенести содержимое scripts/patch-android.sh в native/android/overlay/ как реальные файлы:
MainActivity.kt(storage permissions logic),AndroidManifest.xml(storage perms +requestLegacyExternalStorage+ FileProvider). Удалитьpatch-android.sh. Добавитьsync:android+dependsOn. Поведение APK не меняется.
kontinuum-tether
Завести каркас client/native/android/overlay/ (минимальный/пустой + README), sync:android в client/project.json. Рукописного Kotlin сверх tauri сейчас нет (только Rust client/src/android/*.rs) — инфраструктура на вырост.
Сломанные симлинки submodule (сопутствующее)
Внутри kontinuum-veil и kontinuum-tether симлинки scripts/* → ../../continuum-app/... битые (каталог переименован в kontinuum-app). Временный костыль — корневой continuum-app → kontinuum-app (untracked). Правильно: внутри каждого submodule перенацелить симлинки на ../../kontinuum-app/..., закоммитить в submodule, удалить костыль. Включается в план как отдельный шаг.
Тестирование / верификация
sync-native.shидемпотентен: двойной запуск → одинаковыйgen/.- Сборка:
nx run veil-backend:build:androidпроходит, APK содержитVeilVpnService,QUERY_ALL_PACKAGES, bridge-классы (проверка черезunzip/aapt dump). - Функционально: установить подписанный APK на устройство
BL8800Pro, открыть Split tunneling → список приложений непустой; allow/deny переключение влияет на маршрутизацию (smoke). - Регенерация:
rm -rf gen/android && nx run veil-backend:build:android→ overlay восстанавливает слой, список приложений снова на месте. - kontinuum-app: APK после миграции эквивалентен (storage perms + FileProvider присутствуют).
Риски
- Апгрейд Tauri может изменить структуру/болванку генерируемых файлов (MainActivity, manifest) → overlay-версия разойдётся. Митигировать: при апгрейде сверять overlay с свежей генерацией (git-diff), README фиксирует это требование.
- Точка инъекции bridge в WebView зависит от внутренностей tauri (как получить WebView из
TauriActivity). Уточнить в реализации (возможно черезonWebViewCreateили сгенерированныйRustWebView). - Split-tunnel поверх VpnService — самая нетривиальная часть; требует корректного построения TUN и связки с TUN-JNI. Достоверность функциональной части подтверждается тем, что слой уже существовал и работал (скриншот пользователя).