Record project continuation changes
This commit is contained in:
@@ -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`, чтобы новый
|
||||
артефакт был сразу доступен для скачивания пользователем после сборки и для
|
||||
автообновления узлов.
|
||||
@@ -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"
|
||||
@@ -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 "Готово: релиз собран, опубликован и проверен."
|
||||
Reference in New Issue
Block a user