Добавьте файлы проекта.
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
param(
|
||||
[string]$JavaHome,
|
||||
[string]$GradleExe,
|
||||
[string]$SdkDir,
|
||||
[string]$KeystoreInfoPath,
|
||||
[string]$RepositoryUrl,
|
||||
[string]$HomepageUrl,
|
||||
[string]$ReleaseNotes,
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Read-KeyValueFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "File not found: $Path"
|
||||
}
|
||||
|
||||
$result = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith("#")) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $line -split "=", 2
|
||||
if ($parts.Count -eq 2) {
|
||||
$result[$parts[0].Trim()] = $parts[1].Trim()
|
||||
}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Resolve-JavaHome {
|
||||
param([string]$PreferredJavaHome)
|
||||
|
||||
$candidates = @()
|
||||
if ($PreferredJavaHome) {
|
||||
$candidates += $PreferredJavaHome
|
||||
}
|
||||
if ($env:JAVA_HOME) {
|
||||
$candidates += $env:JAVA_HOME
|
||||
}
|
||||
$candidates += @(
|
||||
"C:\Program Files\Android\Android Studio\jbr",
|
||||
"C:\Program Files\Android\openjdk"
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if ($candidate -and (Test-Path -LiteralPath (Join-Path $candidate "bin\java.exe"))) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw "Java runtime not found. Pass -JavaHome explicitly."
|
||||
}
|
||||
|
||||
function Resolve-GradleExe {
|
||||
param([string]$PreferredGradleExe)
|
||||
|
||||
$candidates = @()
|
||||
if ($PreferredGradleExe) {
|
||||
$candidates += $PreferredGradleExe
|
||||
}
|
||||
$wrapperRoot = Join-Path $env:USERPROFILE ".gradle\wrapper\dists"
|
||||
$preferredGradle = Join-Path $wrapperRoot "gradle-8.13-bin"
|
||||
if (Test-Path -LiteralPath $preferredGradle) {
|
||||
$candidates += Get-ChildItem -LiteralPath $preferredGradle -Recurse -Filter "gradle.bat" -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty FullName
|
||||
}
|
||||
if (Test-Path -LiteralPath $wrapperRoot) {
|
||||
$candidates += Get-ChildItem -LiteralPath $wrapperRoot -Recurse -Filter "gradle.bat" -ErrorAction SilentlyContinue |
|
||||
Sort-Object FullName -Descending |
|
||||
Select-Object -ExpandProperty FullName
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw "Gradle executable not found. Pass -GradleExe explicitly."
|
||||
}
|
||||
|
||||
function Resolve-SdkDir {
|
||||
param(
|
||||
[string]$PreferredSdkDir,
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$candidates = @()
|
||||
if ($PreferredSdkDir) {
|
||||
$candidates += $PreferredSdkDir
|
||||
}
|
||||
|
||||
$localPropertiesPath = Join-Path $ProjectRoot "local.properties"
|
||||
if (Test-Path -LiteralPath $localPropertiesPath) {
|
||||
$localProperties = Read-KeyValueFile -Path $localPropertiesPath
|
||||
if ($localProperties.ContainsKey("sdk.dir")) {
|
||||
$candidates += ($localProperties["sdk.dir"] -replace "\\\\", "\")
|
||||
}
|
||||
}
|
||||
|
||||
if ($env:ANDROID_SDK_ROOT) {
|
||||
$candidates += $env:ANDROID_SDK_ROOT
|
||||
}
|
||||
if ($env:ANDROID_HOME) {
|
||||
$candidates += $env:ANDROID_HOME
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw "Android SDK not found. Pass -SdkDir explicitly."
|
||||
}
|
||||
|
||||
function Get-LatestBuildToolsDir {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SdkDir
|
||||
)
|
||||
|
||||
$buildToolsRoot = Join-Path $SdkDir "build-tools"
|
||||
if (-not (Test-Path -LiteralPath $buildToolsRoot)) {
|
||||
throw "Android build-tools directory not found: $buildToolsRoot"
|
||||
}
|
||||
|
||||
$latest = Get-ChildItem -LiteralPath $buildToolsRoot -Directory |
|
||||
Sort-Object { [version]$_.Name } -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $latest) {
|
||||
throw "No Android build-tools versions found in $buildToolsRoot"
|
||||
}
|
||||
|
||||
return $latest.FullName
|
||||
}
|
||||
|
||||
function Get-ArgusTemplate {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$searchRoot = Join-Path $ProjectRoot "artifacts\argus"
|
||||
if (-not (Test-Path -LiteralPath $searchRoot)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$templatePath = Get-ChildItem -LiteralPath $searchRoot -Recurse -Filter "argus-release.json" -File |
|
||||
Sort-Object FullName -Descending |
|
||||
Select-Object -First 1 -ExpandProperty FullName
|
||||
|
||||
if (-not $templatePath) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return (Get-Content -LiteralPath $templatePath -Raw | ConvertFrom-Json)
|
||||
}
|
||||
|
||||
function Resolve-PackageFileName {
|
||||
param(
|
||||
[string]$Version,
|
||||
[object]$Template
|
||||
)
|
||||
|
||||
if ($Template -and $Template.packageFile -and ($Template.packageFile -match "^(?<base>.+)-\d[\w\.\-]*(?<ext>\.apk)$")) {
|
||||
return "$($matches.base)-$Version$($matches.ext)"
|
||||
}
|
||||
|
||||
return "aletheia-$Version.apk"
|
||||
}
|
||||
|
||||
function Resolve-KeystoreInfoPath {
|
||||
param(
|
||||
[string]$PreferredPath,
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$candidates = @()
|
||||
if ($PreferredPath) {
|
||||
$candidates += $PreferredPath
|
||||
}
|
||||
$candidates += @(
|
||||
(Join-Path $ProjectRoot "signing\release-keystore-info.txt"),
|
||||
(Join-Path $env:USERPROFILE ".aletheia\signing\release-keystore-info.txt")
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw "release-keystore-info.txt not found. Pass -KeystoreInfoPath explicitly."
|
||||
}
|
||||
|
||||
function Resolve-KeystorePath {
|
||||
param(
|
||||
[string]$ConfiguredPath,
|
||||
[string]$KeystoreInfoPath,
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$candidates = @()
|
||||
if ($ConfiguredPath) {
|
||||
$candidates += $ConfiguredPath
|
||||
|
||||
$configuredLeaf = Split-Path -Leaf $ConfiguredPath
|
||||
if ($configuredLeaf) {
|
||||
$candidates += (Join-Path (Split-Path -Parent $KeystoreInfoPath) $configuredLeaf)
|
||||
$candidates += (Join-Path (Join-Path $ProjectRoot "signing") $configuredLeaf)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw "Keystore file not found. Checked: $($candidates -join ', ')"
|
||||
}
|
||||
|
||||
function Resolve-GitOriginUrl {
|
||||
param([string]$ProjectRoot)
|
||||
|
||||
$gitConfigPath = Join-Path $ProjectRoot ".git\config"
|
||||
if (-not (Test-Path -LiteralPath $gitConfigPath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$inOriginSection = $false
|
||||
foreach ($line in Get-Content -LiteralPath $gitConfigPath) {
|
||||
if ($line -match '^\s*\[remote\s+"origin"\]\s*$') {
|
||||
$inOriginSection = $true
|
||||
continue
|
||||
}
|
||||
if ($line -match '^\s*\[') {
|
||||
$inOriginSection = $false
|
||||
}
|
||||
if ($inOriginSection -and $line -match '^\s*url\s*=\s*(.+?)\s*$') {
|
||||
return $matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
$javaHomePath = Resolve-JavaHome -PreferredJavaHome $JavaHome
|
||||
$gradleExePath = Resolve-GradleExe -PreferredGradleExe $GradleExe
|
||||
$sdkDirPath = Resolve-SdkDir -PreferredSdkDir $SdkDir -ProjectRoot $projectRoot
|
||||
$keystoreInfoPath = Resolve-KeystoreInfoPath -PreferredPath $KeystoreInfoPath -ProjectRoot $projectRoot
|
||||
$buildToolsDir = Get-LatestBuildToolsDir -SdkDir $sdkDirPath
|
||||
$gitOriginUrl = Resolve-GitOriginUrl -ProjectRoot $projectRoot
|
||||
$zipalignExe = Join-Path $buildToolsDir "zipalign.exe"
|
||||
$apksignerBat = Join-Path $buildToolsDir "apksigner.bat"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $zipalignExe)) {
|
||||
throw "zipalign.exe not found: $zipalignExe"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $apksignerBat)) {
|
||||
throw "apksigner.bat not found: $apksignerBat"
|
||||
}
|
||||
|
||||
$env:JAVA_HOME = $javaHomePath
|
||||
$env:ANDROID_SDK_ROOT = $sdkDirPath
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
& $gradleExePath assembleRelease --stacktrace
|
||||
}
|
||||
|
||||
$outputMetadataPath = Join-Path $projectRoot "app\build\outputs\apk\release\output-metadata.json"
|
||||
if (-not (Test-Path -LiteralPath $outputMetadataPath)) {
|
||||
throw "Release output metadata not found: $outputMetadataPath"
|
||||
}
|
||||
|
||||
$outputMetadata = Get-Content -LiteralPath $outputMetadataPath -Raw | ConvertFrom-Json
|
||||
$releaseElement = $outputMetadata.elements | Select-Object -First 1
|
||||
if (-not $releaseElement) {
|
||||
throw "Release output metadata does not contain any elements."
|
||||
}
|
||||
|
||||
$versionName = [string]$releaseElement.versionName
|
||||
if ([string]::IsNullOrWhiteSpace($versionName)) {
|
||||
throw "versionName is empty in release output metadata."
|
||||
}
|
||||
|
||||
$unsignedApk = Join-Path $projectRoot ("app\build\outputs\apk\release\" + $releaseElement.outputFile)
|
||||
if (-not (Test-Path -LiteralPath $unsignedApk)) {
|
||||
throw "Unsigned release APK not found: $unsignedApk"
|
||||
}
|
||||
|
||||
$keystoreInfo = Read-KeyValueFile -Path $keystoreInfoPath
|
||||
foreach ($requiredKey in @("KeystorePath", "Alias", "StorePassword", "KeyPassword", "StoreType")) {
|
||||
if (-not $keystoreInfo.ContainsKey($requiredKey)) {
|
||||
throw "Missing '$requiredKey' in $keystoreInfoPath"
|
||||
}
|
||||
}
|
||||
$keystorePath = Resolve-KeystorePath -ConfiguredPath $keystoreInfo["KeystorePath"] -KeystoreInfoPath $keystoreInfoPath -ProjectRoot $projectRoot
|
||||
|
||||
$template = Get-ArgusTemplate -ProjectRoot $projectRoot
|
||||
$slug = if ($template -and $template.slug) { [string]$template.slug } else { "aletheia-kotlin" }
|
||||
$packageFile = Resolve-PackageFileName -Version $versionName -Template $template
|
||||
$outputDir = Join-Path $projectRoot ("artifacts\argus\$slug\$versionName")
|
||||
$alignedApk = Join-Path $outputDir ([IO.Path]::GetFileNameWithoutExtension($packageFile) + "-aligned.apk")
|
||||
$signedApk = Join-Path $outputDir $packageFile
|
||||
$idsigPath = $signedApk + ".idsig"
|
||||
$summaryPath = Join-Path $outputDir "argus-publish-summary.txt"
|
||||
$manifestPath = Join-Path $outputDir "argus-release.json"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
|
||||
foreach ($path in @($alignedApk, $signedApk, $idsigPath)) {
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
Remove-Item -LiteralPath $path -Force
|
||||
}
|
||||
}
|
||||
|
||||
& $zipalignExe -p -f 4 $unsignedApk $alignedApk
|
||||
|
||||
$storePassFile = Join-Path $env:TEMP "aletheia-store-pass.txt"
|
||||
$keyPassFile = Join-Path $env:TEMP "aletheia-key-pass.txt"
|
||||
|
||||
try {
|
||||
Set-Content -LiteralPath $storePassFile -Value $keystoreInfo["StorePassword"] -NoNewline
|
||||
Set-Content -LiteralPath $keyPassFile -Value $keystoreInfo["KeyPassword"] -NoNewline
|
||||
|
||||
& $apksignerBat sign `
|
||||
--ks $keystorePath `
|
||||
--ks-type $keystoreInfo["StoreType"] `
|
||||
--ks-key-alias $keystoreInfo["Alias"] `
|
||||
--ks-pass ("file:" + $storePassFile) `
|
||||
--key-pass ("file:" + $keyPassFile) `
|
||||
--v4-signing-enabled true `
|
||||
--out $signedApk `
|
||||
$alignedApk
|
||||
}
|
||||
finally {
|
||||
foreach ($secretPath in @($storePassFile, $keyPassFile)) {
|
||||
if (Test-Path -LiteralPath $secretPath) {
|
||||
Remove-Item -LiteralPath $secretPath -Force
|
||||
}
|
||||
}
|
||||
if (Test-Path -LiteralPath $alignedApk) {
|
||||
Remove-Item -LiteralPath $alignedApk -Force
|
||||
}
|
||||
}
|
||||
|
||||
$verifyOutputLines = & $apksignerBat verify --verbose --print-certs --v4-signature-file $idsigPath $signedApk
|
||||
$verifyOutput = $verifyOutputLines | Out-String
|
||||
foreach ($requiredVerificationLine in @(
|
||||
"Verified using v2 scheme (APK Signature Scheme v2): true",
|
||||
"Verified using v3 scheme (APK Signature Scheme v3): true",
|
||||
"Verified using v4 scheme (APK Signature Scheme v4): true"
|
||||
)) {
|
||||
if ($verifyOutput -notmatch [regex]::Escape($requiredVerificationLine)) {
|
||||
throw "APK verification did not confirm expected signature state: $requiredVerificationLine"
|
||||
}
|
||||
}
|
||||
|
||||
$signerInfoLines = $verifyOutputLines | Where-Object { $_ -like "Signer #1 certificate *" }
|
||||
if (-not $signerInfoLines) {
|
||||
throw "Could not extract signer certificate details from apksigner output."
|
||||
}
|
||||
|
||||
$signerInfo = $signerInfoLines | Out-String
|
||||
$packageSha256 = ((Get-FileHash -LiteralPath $signedApk -Algorithm SHA256).Hash).ToLowerInvariant()
|
||||
|
||||
$releaseNotesText = if ($PSBoundParameters.ContainsKey("ReleaseNotes")) {
|
||||
$ReleaseNotes
|
||||
}
|
||||
else {
|
||||
"Technical release $versionName for Aletheia Kotlin: project version bumped and Argus package prepared."
|
||||
}
|
||||
|
||||
$repositoryUrlValue = if ($PSBoundParameters.ContainsKey("RepositoryUrl")) {
|
||||
$RepositoryUrl
|
||||
}
|
||||
elseif ($gitOriginUrl) {
|
||||
$gitOriginUrl
|
||||
}
|
||||
else {
|
||||
""
|
||||
}
|
||||
|
||||
$homepageUrlValue = if ($PSBoundParameters.ContainsKey("HomepageUrl")) {
|
||||
$HomepageUrl
|
||||
}
|
||||
else {
|
||||
""
|
||||
}
|
||||
|
||||
$manifest = [ordered]@{
|
||||
slug = $slug
|
||||
name = if ($template -and $template.name) { $template.name } else { "Aletheia" }
|
||||
summary = if ($template -and $template.summary) { $template.summary } else { "Android reader app." }
|
||||
description = if ($template -and $template.description) { $template.description } else { "Android application package prepared for Argus." }
|
||||
repositoryUrl = $repositoryUrlValue
|
||||
homepageUrl = $homepageUrlValue
|
||||
version = $versionName
|
||||
channel = if ($template -and $template.channel) { $template.channel } else { "stable" }
|
||||
platform = if ($template -and $template.platform) { $template.platform } else { "android" }
|
||||
packageKind = if ($template -and $template.packageKind) { $template.packageKind } else { "apk" }
|
||||
releaseNotes = $releaseNotesText
|
||||
publicCatalog = if ($template -and $null -ne $template.publicCatalog) { [bool]$template.publicCatalog } else { $false }
|
||||
packageFile = $packageFile
|
||||
packageSha256 = $packageSha256
|
||||
signerInfo = ($signerInfo.TrimEnd() -replace "\r?\n", [Environment]::NewLine)
|
||||
}
|
||||
|
||||
$manifestJson = $manifest | ConvertTo-Json -Depth 4
|
||||
Set-Content -LiteralPath $manifestPath -Value $manifestJson -Encoding utf8
|
||||
|
||||
$summaryLines = @(
|
||||
"Slug: $($manifest.slug)",
|
||||
"Name: $($manifest.name)",
|
||||
"Summary: $($manifest.summary)",
|
||||
"Description: $($manifest.description)",
|
||||
"Repository URL: $($manifest.repositoryUrl)",
|
||||
"Homepage URL: $($manifest.homepageUrl)",
|
||||
"Version: $($manifest.version)",
|
||||
"Channel: $($manifest.channel)",
|
||||
"Platform: $($manifest.platform)",
|
||||
"Package kind: $($manifest.packageKind)",
|
||||
"Release notes: $($manifest.releaseNotes)",
|
||||
"Package file: $signedApk",
|
||||
"Show in public catalog: $($manifest.publicCatalog)",
|
||||
"Package SHA-256: $($manifest.packageSha256)",
|
||||
"",
|
||||
"Signer:",
|
||||
$manifest.signerInfo
|
||||
)
|
||||
|
||||
Set-Content -LiteralPath $summaryPath -Value $summaryLines -Encoding utf8
|
||||
|
||||
Write-Host "Argus package prepared:" $signedApk
|
||||
Write-Host "SHA-256:" $packageSha256
|
||||
Write-Host "Manifest:" $manifestPath
|
||||
Write-Host "Summary:" $summaryPath
|
||||
Reference in New Issue
Block a user