404 lines
14 KiB
PowerShell
404 lines
14 KiB
PowerShell
#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 "Сборка не завершена."
|
|
}
|