<# .SYNOPSIS Builds a signed release APK and assembles the folder to publish on the NAS. .DESCRIPTION Produces dist/out/ containing: index.html landing page people are sent to latest.json update manifest the app polls memby-.apk the build itself *.apk.sha256 SHA-256 checksum for the build LICENSE / NOTICE licence terms and acknowledgements Copy that folder to whatever the NAS serves, keeping the file names. Old APKs can stay alongside — only latest.json decides what the app offers. .PARAMETER Version Version to build, e.g. 0.1.54. Updates versionName/versionCode in app/build.gradle.kts before building. Omit to build the version already set. .PARAMETER Notes What changed, shown both on the landing page and in the app's update prompt. .PARAMETER BaseUrl Public URL of the folder on the NAS, e.g. https://nas.example.com/memby. Used to build absolute links on the landing page. .PARAMETER SourceUrl Recipient-accessible URL for the exact corresponding source. This link is embedded in the APK and release page. It must remain available to everyone receiving a build. .EXAMPLE .\release.ps1 -Version 0.1.54 -Notes "Faster home screen" -BaseUrl https://nas.example.com/memby #> param( [string] $Version, [string] $Notes = '', [Parameter(Mandatory = $true)][string] $BaseUrl, [string] $SourceUrl = 'https://g.sublogue.com/admin/memby' ) $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $gradleFile = Join-Path $root 'app\build.gradle.kts' $outDir = Join-Path $root 'dist\out' $templateDir = Join-Path $root 'dist\template' $jdkHome = 'C:\Program Files\Android\Android Studio\jbr' if (Test-Path $jdkHome) { $env:JAVA_HOME = $jdkHome } # --- version --------------------------------------------------------------- $gradle = Get-Content -Raw $gradleFile if ($Version) { if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "Version must look like 0.1.54, got '$Version'" } $parts = $Version.Split('.') # Same scheme as app/build.gradle.kts: major*10000 + minor*100 + patch. $code = [int]$parts[0] * 10000 + [int]$parts[1] * 100 + [int]$parts[2] $gradle = $gradle -replace 'val defaultVersionName = "[^"]*"', "val defaultVersionName = `"$Version`"" Set-Content -LiteralPath $gradleFile -Value $gradle -NoNewline Write-Host "Set version $Version (versionCode $code)" -ForegroundColor Cyan } else { if ($gradle -notmatch 'val defaultVersionName = "([^"]*)"') { throw 'Could not read defaultVersionName from app/build.gradle.kts' } $Version = $Matches[1] Write-Host "Building existing version $Version" -ForegroundColor Cyan } # --- build ----------------------------------------------------------------- & (Join-Path $root 'gradlew.bat') --console=plain clean test assembleRelease "-Pmemby.sourceUrl=$SourceUrl" if ($LASTEXITCODE -ne 0) { throw 'Build failed' } $apk = Join-Path $root 'app\build\outputs\apk\release\app-release.apk' if (-not (Test-Path $apk)) { # An unsigned build lands under a different name and cannot be installed. $unsigned = Join-Path $root 'app\build\outputs\apk\release\app-release-unsigned.apk' if (Test-Path $unsigned) { throw "Release APK is UNSIGNED. Configure memby.keystore in local.properties — " + "an unsigned APK will not install, and a key change breaks updates for existing users." } throw "No release APK found at $apk" } # A successful Gradle task is not sufficient evidence for a release: verify the # finished archive with the Android SDK tool before it can enter dist/out. $sdk = $env:ANDROID_HOME if (-not $sdk) { $sdkLine = Get-Content -LiteralPath (Join-Path $root 'local.properties') | Where-Object { $_ -match '^sdk\.dir=' } | Select-Object -First 1 if ($sdkLine) { $sdk = $sdkLine.Substring($sdkLine.IndexOf('=') + 1). Replace('\:', ':'). Replace('\\', '\') } } if (-not $sdk -or -not (Test-Path -LiteralPath $sdk -PathType Container)) { throw 'Android SDK not found; the release signature cannot be verified.' } $buildTools = Get-ChildItem -LiteralPath (Join-Path $sdk 'build-tools') -Directory | Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1 if (-not $buildTools) { throw 'Android SDK Build Tools are not installed.' } $apkSigner = Join-Path $buildTools.FullName 'apksigner.bat' if (-not (Test-Path -LiteralPath $apkSigner -PathType Leaf)) { throw "APK signer not found: $apkSigner" } & $apkSigner verify --verbose --print-certs $apk if ($LASTEXITCODE -ne 0) { throw 'APK signature verification failed' } # --- assemble the publish folder ------------------------------------------- $apkName = "memby-$Version.apk" $base = $BaseUrl.TrimEnd('/') if (Test-Path $outDir) { Remove-Item $outDir -Recurse -Force } New-Item -ItemType Directory -Force $outDir | Out-Null Copy-Item $apk (Join-Path $outDir $apkName) $sha256 = (Get-FileHash -LiteralPath $apk -Algorithm SHA256).Hash.ToLowerInvariant() Set-Content -LiteralPath (Join-Path $outDir "$apkName.sha256") ` -Value "$sha256 $apkName" -Encoding ascii Copy-Item (Join-Path $root 'LICENSE') (Join-Path $outDir 'LICENSE') $notice = (Get-Content -Raw (Join-Path $root 'NOTICE')). Replace('https://g.sublogue.com/admin/memby', $SourceUrl) Set-Content -LiteralPath (Join-Path $outDir 'NOTICE') -Value $notice -Encoding utf8 # apkUrl stays relative so the folder keeps working if the NAS is reached by another # name; the app resolves it against the manifest's own URL. $manifest = [ordered]@{ version = $Version apkUrl = $apkName sha256 = $sha256 notes = $Notes } | ConvertTo-Json Set-Content -LiteralPath (Join-Path $outDir 'latest.json') -Value $manifest -Encoding utf8 $size = '{0:N1} MB' -f ((Get-Item $apk).Length / 1MB) $page = Get-Content -Raw (Join-Path $templateDir 'index.html') $page = $page.Replace('{{VERSION}}', $Version). Replace('{{APK_NAME}}', $apkName). Replace('{{APK_URL}}', "$base/$apkName"). Replace('{{BASE_URL}}', $base). Replace('{{SOURCE_URL}}', $SourceUrl). Replace('{{SIZE}}', $size). Replace('{{NOTES}}', $(if ($Notes) { $Notes } else { 'Various improvements.' })). Replace('{{DATE}}', (Get-Date -Format 'd MMMM yyyy')) Set-Content -LiteralPath (Join-Path $outDir 'index.html') -Value $page -Encoding utf8 Write-Host '' Write-Host "Ready to publish:" -ForegroundColor Green Get-ChildItem $outDir | ForEach-Object { " $($_.Name)" } Write-Host '' Write-Host "Copy the contents of dist\out\ to the folder served at $base" Write-Host "Landing page: $base/" Write-Host "Update manifest: $base/latest.json"