Record project continuation changes

This commit is contained in:
2026-05-12 21:02:29 +03:00
parent 3059d1d7a3
commit 8f69d53193
339 changed files with 101111 additions and 1769 deletions
+138
View File
@@ -0,0 +1,138 @@
# Android сборка и публикация APK (локально)
Эти скрипты позволяют делать локальную сборку Android-клиента, сразу публиковать
готовый APK в место, из которого веб-панель его скачивает, и при необходимости
быстро проверять состояние окружения.
## Подготовка окружения
Сначала проверьте зависимости и переменные окружения:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\prepare-android-build-environment.ps1 -SetEnvironment
```
Скрипт проверит:
- `java` (JDK 17+)
- `gradle`
- Android SDK (по умолчанию `C:\Android\Sdk`)
- `sdkmanager` из `cmdline-tools`
- `platform-tools`, `platforms;android-<compileSdk>`, `build-tools;35.0.1`
По необходимости можно автосоздать отсутствующие SDK-компоненты:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\prepare-android-build-environment.ps1 -InstallMissing -SetEnvironment
```
## Быстрый запуск после апдейта
После любого обновления версии соблюдаем правило: **сборка → публикация → проверка manifest**.
Любой новый номер версии должен сразу появляться в скачиваемых артефактах.
После обновления клиента запусти сборку и публикацию одной командой:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\rebuild-and-publish-android-apk.ps1
```
По умолчанию команда делает публикацию в локальные папки и в test-docker (vpn.cin.su):
это нужно для корректного обновления узлов и пользователей.
Для релиза `-PublishToTestDockerDownloads` включен по умолчанию; отключать публикацию
внешне можно только флагами `-PublishToTestDockerDownloads:$false` или `-NoRemotePublish`.
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\rebuild-and-publish-android-apk.ps1 `
-PublishToTestDockerDownloads `
-TestDockerSshAlias test-docker `
-TestDockerDownloadPath "/tmp/rap-web-admin/html/downloads"
```
Если нужно сделать полноценный release-пайплайн в один шаг (подготовка окружения +
сборка + публикация + верификация доступного манифеста), можно использовать:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\release-android-apk.ps1 `
-InstallMissing `
-BuildType release `
-PublishToTestDockerDownloads:$true `
-TestDockerSshAlias test-docker `
-TestDockerDownloadPath "/tmp/rap-web-admin/html/downloads" `
-PortalVerifyBaseUrl "http://192.168.200.61:18080"
```
Для «одной команды без параметров» добавлен быстрый запускатор:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\fast-release-android-apk.ps1
```
Также можно запускать двойным кликом из Windows:
```text
scripts\android\fast-release-android-apk.cmd
```
Через `.cmd` можно добавлять параметры FastScript, например:
```text
scripts\android\fast-release-android-apk.cmd -NoRemotePublish -NoPrepare
```
Для production-сборок не отключай `-NoRemotePublish` и `-NoPrepare`.
Эти флаги только для отладочной локальной проверки и не применяются к выпуску.
## Полный bootstrap локальной сборочной машины
Если на новом ПК/ноутбуке нужно собрать проект локально, сначала прогрейте окружение:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\prepare-local-build-workstation.ps1 -SetEnvironment
```
Если нужно сразу пытаться устанавливать недостающее:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\prepare-local-build-workstation.ps1 -InstallMissing -SetEnvironment
```
После этого можно делать сборку:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\fast-release-android-apk.ps1
```
По умолчанию результат будет (для release-сборки из CI/рабочего процесса это самые нужные имена):
- `dist/downloads/rap-android-rdp-vpn-latest-release.apk`
- `dist/releases/<version>/rap-android-rdp-vpn-<version>-release.apk`
- `dist/downloads/rap-android-rdp-vpn-build.json`
- `web-admin/deploy/html/downloads/rap-android-rdp-vpn-latest-release.apk`
- `web-admin/deploy/html/downloads/releases/<version>/rap-android-rdp-vpn-<version>-release.apk`
- `web-admin/deploy/html/downloads/rap-android-rdp-vpn-build.json`
Эти файлы и папки игнорируются git для `dist`, поэтому `web-admin` артефакты
должны публиковаться отдельным шагом инфраструктуры.
## Принудительная сборка Release
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\build-android-apk.ps1 -BuildType release
```
## Где указывать SDK
Если SDK лежит не в `C:\Android\Sdk`, укажи явно:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts\android\build-android-apk.ps1 -AndroidHome "D:\SDK\Android"
```
## Что важно для работы с админ-панелью
- Веб-панель уже ожидает файл:
`downloads/rap-android-rdp-vpn-latest-release.apk`
- Поэтому скрипт публикует APK в `web-admin/deploy/html/downloads`, чтобы новый
артефакт был сразу доступен для скачивания пользователем после сборки и для
автообновления узлов.
+403
View File
@@ -0,0 +1,403 @@
#requires -Version 5
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path,
[string]$BuildType = "release",
[string]$AndroidHome = $env:ANDROID_HOME,
[switch]$SkipWorkspaceCleanup,
[switch]$PrintOnly,
[bool]$PublishToTestDockerDownloads = $false,
[string]$TestDockerSshAlias = "test-docker",
[string]$TestDockerDownloadPath = "/tmp/rap-web-admin/html/downloads"
)
$ErrorActionPreference = "Stop"
function Fail([string]$Message) {
Write-Error $Message
exit 1
}
function Resolve-AndroidSdk([string]$CandidateHome) {
$candidates = @(
$CandidateHome,
"C:\Android\Sdk",
"$env:LOCALAPPDATA\Android\Sdk",
"$env:USERPROFILE\AppData\Local\Android\Sdk"
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
foreach ($candidate in $candidates) {
if ((Test-Path (Join-Path $candidate "platform-tools")) -and (Test-Path (Join-Path $candidate "platforms"))) {
return $candidate
}
}
if ($candidates.Count -gt 0) {
return $candidates[0]
}
return $null
}
function Get-GradleBinary {
$gradleCandidate = Get-Command gradle -ErrorAction SilentlyContinue
if ($null -eq $gradleCandidate) {
Fail "Gradle не найден в PATH. Установи Gradle (`winget install Gradle.Gradle` или `choco install gradle`)."
}
return $gradleCandidate.Source
}
function Get-JavaVersion {
$javaCandidate = Get-Command java -ErrorAction SilentlyContinue
if ($null -eq $javaCandidate) {
Fail "Java не найден в PATH. Нужен JDK 17+."
}
$versionLine = & $javaCandidate.Source -version 2>&1 | Select-Object -First 1
if ($versionLine -match "version\s+`"(\d+)\.") {
return $matches[1]
}
return "unknown"
}
function Resolve-SdkComponent([string]$Root, [string]$RelativePath) {
$candidate = Join-Path $Root $RelativePath
if (Test-Path $candidate) {
return $candidate
}
return $null
}
function Get-AndroidVersionInfo([string]$AndroidProjectDir) {
$moduleBuildFile = Join-Path $AndroidProjectDir "app\build.gradle"
if (-not (Test-Path $moduleBuildFile)) {
Fail "Не найден файл app/build.gradle: $moduleBuildFile"
}
$content = Get-Content $moduleBuildFile -Raw
$versionCodeMatch = [regex]::Match($content, '(?m)^\s*versionCode\s+([0-9]+)\s*$')
$versionNameMatch = [regex]::Match($content, '(?m)^\s*versionName\s+"([^"]+)"\s*$')
if (-not $versionCodeMatch.Success -or -not $versionNameMatch.Success) {
Fail "Не удалось прочитать versionCode/versionName из app/build.gradle"
}
return @{
VersionCode = [int]$versionCodeMatch.Groups[1].Value
VersionName = $versionNameMatch.Groups[1].Value.Trim()
}
}
function Publish-Artifact {
param(
[string]$SourcePath,
[string]$PublishRoot,
[string]$BuildTypeNormalized,
[string]$VersionName,
[string]$VersionFileName,
[string]$LatestFileName,
[string]$PublishedPathPrefix,
[string]$VersionTag
)
New-Item -ItemType Directory -Force -Path $PublishRoot | Out-Null
$versionDir = Join-Path $PublishRoot "releases\$VersionName"
New-Item -ItemType Directory -Force -Path $versionDir | Out-Null
$latestPath = Join-Path $PublishRoot $LatestFileName
$versionPath = Join-Path $versionDir $VersionFileName
$metaPath = Join-Path $PublishRoot "rap-android-rdp-vpn-build.json"
Copy-Item -Path $SourcePath -Destination $latestPath -Force
Copy-Item -Path $SourcePath -Destination $versionPath -Force
$hash = (Get-FileHash -Algorithm SHA256 -Path $SourcePath).Hash.ToLowerInvariant()
$size = (Get-Item $SourcePath).Length
$published = [ordered]@{
version = @{
name = $VersionName
code = $VersionTag
}
build = @{
type = $BuildTypeNormalized
artifact = "app-$BuildTypeNormalized.apk"
}
published = @{
timestamp_utc = (Get-Date).ToUniversalTime().ToString("o")
path = ("$PublishedPathPrefix/$LatestFileName").TrimStart("/")
size_bytes = $size
sha256 = $hash
}
release_paths = @{
latest = $LatestFileName
versioned = "releases/$VersionName/$VersionFileName"
}
}
$published | ConvertTo-Json -Depth 6 | Out-File -FilePath $metaPath -Encoding utf8 -Force
return @{
latest = $latestPath
versioned = $versionPath
summary = $metaPath
hash = $hash
size = $size
artifacts = @(
$latestPath,
$versionPath,
$metaPath
)
releaseDir = $versionDir
}
}
function Resolve-RemoteDirectory([string]$RemotePath) {
return $RemotePath.Trim()
}
function Ensure-RemoteDirectory {
param(
[string]$RemoteHost,
[string]$RemotePath
)
if (-not $RemoteHost -or -not $RemotePath) {
Fail "Remote host/path must be set for publish sync."
}
$escapedPath = $RemotePath.Replace("'", "''")
& ssh $RemoteHost "mkdir -p '$escapedPath'" | Out-Null
if ($LASTEXITCODE -ne 0) {
Fail "Не удалось создать каталог на remote хосте: $RemotePath"
}
}
function Publish-ToTestDockerDownloads {
param(
[string]$RemoteHost,
[string]$RemoteRoot,
[string]$VersionName,
[string]$LatestFile,
[string]$VersionFile,
[string]$ManifestFile
)
Ensure-RemoteDirectory -RemoteHost $RemoteHost -RemotePath $RemoteRoot
$remoteVersionDir = "$RemoteRoot/releases/$VersionName"
Ensure-RemoteDirectory -RemoteHost $RemoteHost -RemotePath $remoteVersionDir
& scp -q $LatestFile "${RemoteHost}:$($RemoteRoot)/" | Out-Null
if ($LASTEXITCODE -ne 0) {
Fail "Не удалось скопировать latest APK на remote host: ${RemoteHost}:$RemoteRoot"
}
& scp -q $ManifestFile "${RemoteHost}:$($RemoteRoot)/" | Out-Null
if ($LASTEXITCODE -ne 0) {
Fail "Не удалось скопировать манифест на remote host: ${RemoteHost}:$RemoteRoot"
}
& scp -q $VersionFile "${RemoteHost}:$($remoteVersionDir)/" | Out-Null
if ($LASTEXITCODE -ne 0) {
Fail "Не удалось скопировать versioned APK на remote host: ${RemoteHost}:$remoteVersionDir"
}
Write-Host "SCP на ${RemoteHost}:${RemoteRoot} выполнен."
}
function Resolve-AbsoluteProjectPath([string]$path) {
if ([string]::IsNullOrWhiteSpace($path)) {
return $null
}
if (Test-Path $path) {
return (Resolve-Path $path).ProviderPath
}
return $null
}
$projectDir = Resolve-AbsoluteProjectPath $RepoRoot
if (-not $projectDir) {
Fail "RepoRoot не найден: $RepoRoot"
}
$androidProject = [System.IO.Path]::GetFullPath((Join-Path $projectDir "clients\android"))
if (-not (Test-Path $androidProject)) {
Fail "Не найден Android проект: $androidProject"
}
$sdkRoot = Resolve-AndroidSdk $AndroidHome
if (-not $sdkRoot) {
Fail "Android SDK не найден. Укажи -AndroidHome или установи SDK в C:\Android\Sdk."
}
$cmdlineTools = Resolve-SdkComponent $sdkRoot "cmdline-tools\latest"
if (-not $cmdlineTools) {
Fail "Android cmdline-tools не найдены. Установи Android SDK command-line tools."
}
if (-not $env:ANDROID_HOME -or $env:ANDROID_HOME -ne $sdkRoot) {
Write-Host "Устанавливаю переменные ANDROID_HOME/ANDROID_SDK_ROOT в текущей сессии: $sdkRoot"
$env:ANDROID_HOME = $sdkRoot
}
$env:ANDROID_SDK_ROOT = $sdkRoot
$gradleBinary = Get-GradleBinary
$javaMajor = Get-JavaVersion
if ($javaMajor -ne "unknown" -and [int]$javaMajor -lt 17) {
Fail "Найден Java $javaMajor, нужен JDK 17+"
}
$versionInfo = Get-AndroidVersionInfo $androidProject
$versionName = $versionInfo.VersionName
if ([string]::IsNullOrWhiteSpace($versionName)) {
$versionName = "0"
}
$versionCode = $versionInfo.VersionCode
if ($versionCode -le 0) {
$versionCode = 0
}
$buildTypeNormalized = $BuildType.Trim().ToLower()
if ($buildTypeNormalized -notin @("debug","release")) {
Fail "BuildType должен быть debug или release"
}
if (-not $PrintOnly -and $buildTypeNormalized -eq "release" -and -not $PublishToTestDockerDownloads) {
Fail "Release сборка запрещена без публикации артефактов. Передай -PublishToTestDockerDownloads:`$true или запускай release-android-apk.ps1/fast-release-android-apk.ps1."
}
$buildTask = "assemble" + (Get-Culture).TextInfo.ToTitleCase($buildTypeNormalized)
Write-Host "=== RAP Android сборка APK ==="
Write-Host "Проект: $androidProject"
Write-Host "Версия: $versionName ($versionCode)"
Write-Host "SDK: $sdkRoot"
Write-Host "Gradle: $gradleBinary"
Write-Host "Build: $buildTask"
Write-Host "Build task: $buildTask"
if (-not (Test-Path (Join-Path $sdkRoot "platforms\android-35"))) {
Write-Host "Внимание: не найдено платформы android-35 в $sdkRoot\platforms. Проверь install: platforms;android-35."
}
if (-not (Test-Path (Join-Path $sdkRoot "build-tools\35.0.1"))) {
Write-Host "Внимание: не найдено build-tools 35.0.1 в $sdkRoot\build-tools."
}
$workspace = Join-Path $env:TEMP ("rap-android-build-" + [guid]::NewGuid().ToString("N"))
if (-not (Test-Path "C:\\")) {
Write-Host "Каталог C:\ недоступен. Использую TEMP: $env:TEMP"
} else {
$candidateWorkspace = Join-Path "C:\" ("rap-android-build-" + [guid]::NewGuid().ToString("N"))
$candidateParent = Split-Path $candidateWorkspace -Parent
if (Test-Path $candidateParent) {
$workspace = $candidateWorkspace
}
}
New-Item -ItemType Directory -Path $workspace -Force | Out-Null
Write-Host "Временная рабочая папка: $workspace"
$buildSucceeded = $false
$builtApk = $null
$summary = $null
try {
Write-Host "Копирую проект в локальную рабочую папку (избавление от проблем UNC)."
Copy-Item -Path (Join-Path $androidProject "*") -Destination $workspace -Recurse -Force
if ($PrintOnly) {
Write-Host "PrintOnly: сборка не выполнена."
return
}
$originalLocation = Get-Location
try {
Push-Location $workspace
$resolvedLocalProperties = Join-Path $workspace "local.properties"
if (Test-Path $resolvedLocalProperties) {
Remove-Item -Force $resolvedLocalProperties
}
Write-Host "Запускаю сборку..."
& $gradleBinary $buildTask
if ($LASTEXITCODE -ne 0) {
Fail "Gradle сборка завершилась с кодом $LASTEXITCODE"
}
}
finally {
Set-Location $originalLocation
}
$builtApkDir = Join-Path $workspace "app\build\outputs\apk\$buildTypeNormalized"
$candidateApks = @()
if (Test-Path $builtApkDir) {
$candidateApks = Get-ChildItem -Path $builtApkDir -Filter "app-${buildTypeNormalized}*.apk" -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -ExpandProperty FullName
}
if (-not $candidateApks -or $candidateApks.Count -eq 0) {
$fallbackApk = Join-Path $builtApkDir "app-$buildTypeNormalized.apk"
if (Test-Path $fallbackApk) {
$candidateApks = @($fallbackApk)
} else {
$candidateApks = @(Get-ChildItem -Path (Join-Path $workspace "app\build\outputs\apk") -Recurse -Filter "app-*.apk" -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -ExpandProperty FullName)
}
}
$builtApk = $candidateApks | Select-Object -First 1
if (-not $builtApk) {
$expectedPath = Join-Path $builtApkDir "app-${buildTypeNormalized}.apk"
Fail "Сборка не дала APK: $expectedPath"
}
$buildSucceeded = $true
$latestFileName = "rap-android-rdp-vpn-latest-$buildTypeNormalized.apk"
$versionFileName = "rap-android-rdp-vpn-$versionName-$buildTypeNormalized.apk"
$publishedPathPrefix = "downloads"
$publishDirs = @(
[System.IO.Path]::GetFullPath((Join-Path $projectDir "dist\downloads")),
[System.IO.Path]::GetFullPath((Join-Path $projectDir "web-admin\deploy\html\downloads"))
)
$publishResults = @()
foreach ($publishDir in $publishDirs) {
$result = Publish-Artifact -SourcePath $builtApk `
-PublishRoot $publishDir `
-BuildTypeNormalized $buildTypeNormalized `
-VersionName $versionName `
-VersionFileName $versionFileName `
-LatestFileName $latestFileName `
-PublishedPathPrefix $publishedPathPrefix `
-VersionTag $versionCode
$publishResults += $result
Write-Host "Опубликован в: $publishDir"
}
$summary = $publishResults[0]
if ($PublishToTestDockerDownloads) {
$remotePublishSource = if ($publishResults.Count -ge 2) { $publishResults[1] } else { $summary }
Write-Host "Публикую артефакты в test-docker ($TestDockerSshAlias): $TestDockerDownloadPath"
Publish-ToTestDockerDownloads -RemoteHost $TestDockerSshAlias -RemoteRoot (Resolve-RemoteDirectory $TestDockerDownloadPath) `
-VersionName $versionName `
-LatestFile $remotePublishSource.latest `
-VersionFile $remotePublishSource.versioned `
-ManifestFile $remotePublishSource.summary
}
Write-Host "Сборка завершена."
Write-Host "APK: $($summary.latest)"
Write-Host "Версионная копия: $($summary.versioned)"
Write-Host "Размер: $($summary.size) байт"
Write-Host "SHA256: $($summary.hash)"
}
finally {
if (-not $SkipWorkspaceCleanup -and (Test-Path $workspace)) {
Remove-Item -Recurse -Force $workspace
}
}
if (-not $buildSucceeded) {
Fail "Сборка не завершена."
}
@@ -0,0 +1,7 @@
@echo off
setlocal
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0fast-release-android-apk.ps1" %*
if %ERRORLEVEL% neq 0 (
exit /b %ERRORLEVEL%
)
@@ -0,0 +1,35 @@
#requires -Version 5
param(
[string]$BuildType = "release",
[bool]$PublishToTestDockerDownloads = $true,
[switch]$NoRemotePublish,
[switch]$NoPrepare,
[switch]$SkipPortalVerify,
[string]$PortalVerifyBaseUrl = "http://192.168.200.61:18080",
[string]$TestDockerSshAlias = "test-docker",
[string]$TestDockerDownloadPath = "/tmp/rap-web-admin/html/downloads",
[int]$PreparationRetryDelaySeconds = 0
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent (Resolve-Path $PSCommandPath).ProviderPath
$releaseScript = Join-Path $scriptDir "release-android-apk.ps1"
if (-not (Test-Path $releaseScript)) {
Write-Error "Не найден скрипт release-android-apk.ps1: $releaseScript"
exit 1
}
$publishToTestDockerDownloads = $PublishToTestDockerDownloads -and -not $NoRemotePublish
& $releaseScript -BuildType $BuildType `
-InstallMissing `
-PublishToTestDockerDownloads:$publishToTestDockerDownloads `
-TestDockerSshAlias $TestDockerSshAlias `
-TestDockerDownloadPath $TestDockerDownloadPath `
-PortalVerifyBaseUrl $PortalVerifyBaseUrl `
-SkipPortalVerify:$SkipPortalVerify `
-PreparationRetryDelaySeconds $PreparationRetryDelaySeconds `
-SkipPrepare:$NoPrepare
@@ -0,0 +1,166 @@
#requires -Version 5
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path,
[string]$AndroidHome = $env:ANDROID_HOME,
[switch]$SetEnvironment,
[switch]$InstallMissing
)
$ErrorActionPreference = "Stop"
function Fail([string]$Message) {
Write-Error $Message
exit 1
}
function Get-CommandPath([string]$Name) {
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
if ($null -eq $cmd) { return $null }
return $cmd.Source
}
function Parse-AndroidCompileVersion([string]$AndroidProjectDir) {
$buildFile = Join-Path $AndroidProjectDir "app\build.gradle"
if (-not (Test-Path $buildFile)) {
return $null
}
$content = Get-Content $buildFile -Raw
$compileMatch = [regex]::Match($content, '(?m)^\s*compileSdk\s+([0-9]+)\s*$')
if ($compileMatch.Success) { return $compileMatch.Groups[1].Value }
return $null
}
function Get-AndroidSdkHome([string]$CandidateHome) {
$candidates = @(
$CandidateHome,
"C:\Android\Sdk",
"$env:LOCALAPPDATA\Android\Sdk",
"$env:USERPROFILE\AppData\Local\Android\Sdk"
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
foreach ($candidate in $candidates) {
if ((Test-Path (Join-Path $candidate "platform-tools")) -and (Test-Path (Join-Path $candidate "platforms"))) {
return $candidate
}
}
if ($candidates.Count -gt 0) { return $candidates[0] }
return $null
}
Write-Host "=== RAP: подготовка Android среды сборки ==="
$repoRoot = (Resolve-Path $RepoRoot).ProviderPath
if (-not $repoRoot) {
Fail "RepoRoot не найден: $RepoRoot"
}
$androidProject = Join-Path $repoRoot "clients\android"
if (-not (Test-Path $androidProject)) {
Fail "Не найден проект clients\android: $androidProject"
}
$compileSdk = Parse-AndroidCompileVersion $androidProject
if ([string]::IsNullOrWhiteSpace($compileSdk)) {
$compileSdk = "35"
}
$java = Get-CommandPath "java"
if (-not $java) {
Write-Host "Не найден java в PATH. Установи JDK 17+: winget install EclipseAdoptium.Temurin.17.JDK или choco install temurin17"
Fail "Зависимость Java отсутствует."
}
$javaVersion = (& $java -version 2>&1 | Select-Object -First 1)
if ($javaVersion -match 'version\s+"(\d+)\.' ) {
$major = [int]$matches[1]
Write-Host "Java: $java ($major)"
if ($major -lt 17) {
Write-Host "Найден Java $major, нужен JDK 17+."
Fail "Нужен Java 17 или новее."
}
} else {
Write-Host "Java найден, версия не распознана: $javaVersion"
}
$gradle = Get-CommandPath "gradle"
if (-not $gradle) {
Write-Host "Не найден gradle в PATH. Установи: winget install Gradle.Gradle / choco install gradle"
Fail "Зависимость Gradle отсутствует."
}
Write-Host "Gradle: $gradle"
$sdkRoot = Get-AndroidSdkHome $AndroidHome
if (-not $sdkRoot) {
Write-Host "Не найден Android SDK. Укажи -AndroidHome или установи SDK в C:\Android\Sdk."
Fail "Android SDK не найден."
}
Write-Host "Android SDK: $sdkRoot"
$cmdlineTools = Join-Path $sdkRoot "cmdline-tools\latest\bin\sdkmanager.bat"
if (-not (Test-Path $cmdlineTools)) {
Write-Host "Не найден Android cmdline-tools ($cmdlineTools)."
Write-Host "Установи Android SDK Command-line Tools через Android Studio или вручную скачай SDK Manager."
Fail "cmdline-tools не найдены."
}
Write-Host "sdkmanager: $cmdlineTools"
Write-Host "Поддерживаем compileSdk: $compileSdk"
$requiredComponents = @(
"platform-tools",
"platforms;android-$compileSdk",
"build-tools;35.0.1"
)
$missingComponents = @()
foreach ($component in $requiredComponents) {
switch -Regex ($component) {
"^platform-tools$" {
if (-not (Test-Path (Join-Path $sdkRoot "platform-tools"))) { $missingComponents += $component }
}
"^platforms;android-(?<api>\d+)$" {
if (-not (Test-Path (Join-Path $sdkRoot "platforms\android-$($Matches["api"])"))) { $missingComponents += $component }
}
"^build-tools;(?<bt>.+)$" {
if (-not (Test-Path (Join-Path $sdkRoot "build-tools\$($Matches["bt"])"))) { $missingComponents += $component }
}
default { }
}
}
if ($missingComponents.Count -gt 0) {
Write-Host ""
Write-Host "Отсутствуют компоненты Android SDK:"
$missingComponents | ForEach-Object { Write-Host " - $_" }
Write-Host ""
$installCmd = "& `"$cmdlineTools`" --sdk_root=`"$sdkRoot`" --install $($missingComponents -join ' ')"
Write-Host "Команда установки:"
Write-Host " $installCmd"
if ($InstallMissing) {
Write-Host "Запускаю установку отсутствующих компонентов..."
& $cmdlineTools --sdk_root=$sdkRoot --install $missingComponents
if ($LASTEXITCODE -ne 0) {
Fail "Не удалось установить компоненты через sdkmanager."
}
Write-Host "Компоненты установлены."
} else {
Write-Host "Добавь -InstallMissing для автo-установки."
}
} else {
Write-Host "Все базовые компоненты SDK присутствуют."
}
if ($SetEnvironment) {
[Environment]::SetEnvironmentVariable("ANDROID_HOME", $sdkRoot, "User")
[Environment]::SetEnvironmentVariable("ANDROID_SDK_ROOT", $sdkRoot, "User")
Write-Host "Персистентные переменные ANDROID_HOME/ANDROID_SDK_ROOT сохранены для текущего пользователя."
}
Write-Host ""
Write-Host "Рекомендуется в текущей сессии:"
Write-Host " `$env:ANDROID_HOME='$sdkRoot'"
Write-Host " `$env:ANDROID_SDK_ROOT='$sdkRoot'"
Write-Host ""
Write-Host "Готово. После этого запуск:"
Write-Host " pwsh -ExecutionPolicy Bypass -File scripts\\android\\build-android-apk.ps1"
@@ -0,0 +1,85 @@
#requires -Version 5
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).ProviderPath,
[string]$BuildType = "release",
[string]$AndroidHome = $env:ANDROID_HOME,
[switch]$InstallMissing,
[switch]$SkipWorkspaceCleanup,
[switch]$PrintOnly,
[switch]$SkipPrepare,
[bool]$PublishToTestDockerDownloads = $true,
[string]$TestDockerSshAlias = "test-docker",
[string]$TestDockerDownloadPath = "/tmp/rap-web-admin/html/downloads",
[int]$PreparationRetryDelaySeconds = 0
)
$ErrorActionPreference = "Stop"
function Fail([string]$Message) {
Write-Error $Message
exit 1
}
function Run-Step([string]$Name, [scriptblock]$Action) {
Write-Host "=== $Name ==="
$global:LASTEXITCODE = 0
try {
& $Action
} catch {
Fail "$Name завершился с ошибкой: $($_.Exception.Message)"
}
if (-not $?) {
Fail "$Name завершился с кодом ошибки"
}
if ($LASTEXITCODE -ne 0) {
Fail "$Name завершился с кодом $LASTEXITCODE"
}
}
$scriptDir = Split-Path -Parent (Resolve-Path $PSCommandPath).ProviderPath
$prepareScript = Join-Path $scriptDir "prepare-android-build-environment.ps1"
$buildScript = Join-Path $scriptDir "build-android-apk.ps1"
if (-not (Test-Path $prepareScript)) {
Fail "Не найден скрипт подготовки окружения: $prepareScript"
}
if (-not (Test-Path $buildScript)) {
Fail "Не найден скрипт сборки: $buildScript"
}
if ($PreparationRetryDelaySeconds -lt 0 -or $PreparationRetryDelaySeconds -gt 3600) {
Fail "PreparationRetryDelaySeconds должен быть в диапазоне 0..3600."
}
if (-not $SkipPrepare) {
Run-Step "Подготовка Android окружения" {
& $prepareScript -RepoRoot $RepoRoot -AndroidHome $AndroidHome -SetEnvironment -InstallMissing:$InstallMissing
}
if ($PreparationRetryDelaySeconds -gt 0) {
Start-Sleep -Seconds $PreparationRetryDelaySeconds
}
} else {
Write-Host "Подготовка окружения пропущена (-SkipPrepare)."
}
if ($PrintOnly) {
Run-Step "Проверка параметров и печать (без сборки)" {
& $buildScript -RepoRoot $RepoRoot -BuildType $BuildType -AndroidHome $AndroidHome -PrintOnly `
-SkipWorkspaceCleanup:$SkipWorkspaceCleanup `
-PublishToTestDockerDownloads:$PublishToTestDockerDownloads `
-TestDockerSshAlias $TestDockerSshAlias `
-TestDockerDownloadPath $TestDockerDownloadPath
}
exit 0
}
Run-Step "Сборка и публикация Android APK" {
& $buildScript -RepoRoot $RepoRoot -BuildType $BuildType -AndroidHome $AndroidHome -SkipWorkspaceCleanup:$SkipWorkspaceCleanup `
-PublishToTestDockerDownloads:$PublishToTestDockerDownloads `
-TestDockerSshAlias $TestDockerSshAlias `
-TestDockerDownloadPath $TestDockerDownloadPath
}
Write-Host ""
Write-Host "Готово. APK опубликован для веб-панели по ссылке: downloads/rap-android-rdp-vpn-latest-$BuildType.apk"
+134
View File
@@ -0,0 +1,134 @@
#requires -Version 5
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).ProviderPath,
[string]$BuildType = "release",
[string]$AndroidHome = $env:ANDROID_HOME,
[switch]$InstallMissing,
[switch]$SkipPrepare,
[switch]$SkipWorkspaceCleanup,
[switch]$PrintOnly,
[bool]$PublishToTestDockerDownloads = $true,
[switch]$NoRemotePublish,
[string]$TestDockerSshAlias = "test-docker",
[string]$TestDockerDownloadPath = "/tmp/rap-web-admin/html/downloads",
[int]$PreparationRetryDelaySeconds = 0,
[string]$PortalVerifyBaseUrl = "http://192.168.200.61:18080",
[switch]$SkipPortalVerify
)
$ErrorActionPreference = "Stop"
function Fail([string]$message) {
Write-Error $message
exit 1
}
function Run-Step([string]$name, [scriptblock]$action) {
Write-Host "=== $name ==="
$global:LASTEXITCODE = 0
try {
& $action
} catch {
Fail "$name failed: $($_.Exception.Message)"
}
if (-not $?) {
Fail "$name failed with script error"
}
if ($LASTEXITCODE -ne 0) {
Fail "$name failed with exit code $LASTEXITCODE"
}
}
$scriptDir = Split-Path -Parent (Resolve-Path $PSCommandPath).ProviderPath
$prepareScript = Join-Path $scriptDir "prepare-android-build-environment.ps1"
$buildScript = Join-Path $scriptDir "build-android-apk.ps1"
$buildTypeNormalized = $BuildType.Trim().ToLower()
if ($buildTypeNormalized -notin @("debug","release")) {
Fail "BuildType должен быть debug или release"
}
if (($NoRemotePublish -or -not $PublishToTestDockerDownloads) -and $buildTypeNormalized -eq "release") {
Fail "Для Release сборки в этом сценарии требуется публикация в test-docker. Убери -NoRemotePublish и явно не отключай -PublishToTestDockerDownloads."
}
if (-not (Test-Path $prepareScript)) {
Fail "Не найден скрипт подготовки окружения: $prepareScript"
}
if (-not (Test-Path $buildScript)) {
Fail "Не найден скрипт сборки: $buildScript"
}
if (-not $SkipPrepare) {
Run-Step "Подготовка Android окружения" {
& $prepareScript -RepoRoot $RepoRoot -AndroidHome $AndroidHome -SetEnvironment -InstallMissing:$InstallMissing
}
if ($PreparationRetryDelaySeconds -gt 0) {
Start-Sleep -Seconds $PreparationRetryDelaySeconds
}
} else {
Write-Host "Подготовка окружения пропущена (-SkipPrepare)."
}
if ($PrintOnly) {
if ($PublishToTestDockerDownloads -and -not $NoRemotePublish) {
& $buildScript -RepoRoot $RepoRoot -BuildType $BuildType -AndroidHome $AndroidHome -PrintOnly `
-SkipWorkspaceCleanup:$SkipWorkspaceCleanup `
-PublishToTestDockerDownloads:$true `
-TestDockerSshAlias $TestDockerSshAlias `
-TestDockerDownloadPath $TestDockerDownloadPath
} else {
& $buildScript -RepoRoot $RepoRoot -BuildType $BuildType -AndroidHome $AndroidHome -PrintOnly `
-SkipWorkspaceCleanup:$SkipWorkspaceCleanup
}
exit 0
}
Run-Step "Сборка и публикация Android APK" {
if ($PublishToTestDockerDownloads -and -not $NoRemotePublish) {
& $buildScript -RepoRoot $RepoRoot -BuildType $BuildType -AndroidHome $AndroidHome -SkipWorkspaceCleanup:$SkipWorkspaceCleanup `
-PublishToTestDockerDownloads:$true `
-TestDockerSshAlias $TestDockerSshAlias `
-TestDockerDownloadPath $TestDockerDownloadPath
} else {
& $buildScript -RepoRoot $RepoRoot -BuildType $BuildType -AndroidHome $AndroidHome -SkipWorkspaceCleanup:$SkipWorkspaceCleanup
}
}
if ($SkipPortalVerify) {
Write-Host "Сборка выполнена. Проверка публикации пропущена (-SkipPortalVerify)."
exit 0
}
Run-Step "Проверка манифеста веб-панели" {
$manifestPath = Join-Path $RepoRoot "web-admin\deploy\html\downloads\rap-android-rdp-vpn-build.json"
if (-not (Test-Path $manifestPath)) {
Fail "Локальный манифест не найден: $manifestPath"
}
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
if (-not $manifest.version -or -not $manifest.version.name) {
Fail "Манифест не содержит version.name: $manifestPath"
}
Write-Host "Локальный манифест APK версии: $($manifest.version.name) ($($manifest.version.code))"
Write-Host "Path: $($manifest.published.path)"
Write-Host "Sha256: $($manifest.published.sha256)"
if (-not [string]::IsNullOrWhiteSpace($PortalVerifyBaseUrl)) {
$manifestUrl = "$PortalVerifyBaseUrl/downloads/rap-android-rdp-vpn-build.json?_cb=$(Get-Date -Format 'yyyyMMddHHmmss')"
try {
$remoteManifest = Invoke-RestMethod -Uri $manifestUrl -Method Get
if (-not $remoteManifest.version -or -not $remoteManifest.version.name -or $remoteManifest.version.name -ne $manifest.version.name) {
Fail "Версия после деплоя не совпадает: локально=$($manifest.version.name), remote=$($remoteManifest.version.name)"
}
Write-Host "Подтверждено: portal returns version $($remoteManifest.version.name)"
} catch {
Write-Host "WARN: не удалось прочитать удаленный манифест из $manifestUrl"
Write-Host " Проверьте доступность веб-панели и путь/порт вручную."
}
}
}
Write-Host "Готово: релиз собран, опубликован и проверен."