Files
Aletheia/scripts/prepare-argus-package.ps1
T

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