453 lines
14 KiB
PowerShell
453 lines
14 KiB
PowerShell
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,
|
|
[long]$VersionCode
|
|
)
|
|
|
|
return "aletheia-$Version-$VersionCode.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."
|
|
}
|
|
$versionCode = [long]$releaseElement.versionCode
|
|
if ($versionCode -le 0) {
|
|
throw "versionCode must be positive 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 -VersionCode $versionCode
|
|
$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
|
|
# The updater reads the public manifest endpoint. An unlisted app returns 404 there,
|
|
# so Aletheia releases must stay visible to the public Argus API.
|
|
publicCatalog = $true
|
|
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
|