param( [string]$Slug = 'ikar-android', [string]$Name, [string]$Summary = 'Android client build', [string]$Description = 'Android release channel for the Ikar client.', [string]$Version, [string]$Channel = 'stable', [string]$Notes, [string]$RepositoryUrl, [string]$HomepageUrl = 'https://ikar.kusoft.xyz', [switch]$Unlisted, [switch]$SkipBuild ) $ErrorActionPreference = 'Stop' function Require-Tool([string]$Path, [string]$Label) { if (-not (Test-Path $Path)) { throw "$Label was not found: $Path" } } function Get-ApkBadgingValue([string]$BadgingText, [string]$Pattern) { $match = [regex]::Match($BadgingText, $Pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline) if (-not $match.Success) { return $null } return $match.Groups[1].Value } function Resolve-ArgusPlatform([string[]]$NativeCodes) { $normalized = $NativeCodes | Where-Object { $_ } | ForEach-Object { $_.Trim().ToLowerInvariant() } $allUniversalAbis = @('arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64') if (@($allUniversalAbis | Where-Object { $normalized -contains $_ }).Count -eq $allUniversalAbis.Count) { return 'android-universal' } if ($normalized -contains 'arm64-v8a') { return 'android-arm64' } if ($normalized -contains 'armeabi-v7a') { return 'android-armv7' } if ($normalized -contains 'x86_64') { return 'android-x86_64' } if ($normalized -contains 'x86') { return 'android-x86' } return 'android' } function Get-RepositoryUrl() { $remote = git remote get-url origin 2>$null if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($remote)) { return $remote.Trim() } return $null } $repoRoot = Split-Path -Parent $PSScriptRoot Set-Location $repoRoot $javaHome = if ($env:JAVA_HOME) { $env:JAVA_HOME } else { 'C:\Program Files\Android\openjdk\jdk-21.0.8' } $androidHome = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } else { 'C:\Users\seven\AppData\Local\Android\Sdk' } $env:JAVA_HOME = $javaHome $env:ANDROID_HOME = $androidHome $projectPath = Join-Path $repoRoot 'src\Ikar.Client\Ikar.Client.csproj' $publishDir = Join-Path $repoRoot 'src\Ikar.Client\bin\Release\net10.0-android' $releaseApkPath = Join-Path $publishDir 'com.seven.ikar-Signed.apk' $aaptPath = Join-Path $androidHome 'build-tools\36.1.0\aapt.exe' $apksignerPath = Join-Path $androidHome 'build-tools\36.1.0\apksigner.bat' Require-Tool $projectPath 'Ikar.Client project' Require-Tool $aaptPath 'aapt' Require-Tool $apksignerPath 'apksigner' if (-not $SkipBuild) { dotnet publish $projectPath -f net10.0-android -c Release -p:AndroidPackageFormat=apk -v minimal --no-restore if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE." } } if (-not (Test-Path $releaseApkPath)) { throw "Release APK was not found: $releaseApkPath" } $badging = & $aaptPath dump badging $releaseApkPath if ($LASTEXITCODE -ne 0) { throw "aapt badging failed with exit code $LASTEXITCODE." } $badgingText = ($badging | Out-String) $packageName = Get-ApkBadgingValue $badgingText "package: name='([^']+)'" $versionCode = Get-ApkBadgingValue $badgingText "versionCode='([^']+)'" $versionName = Get-ApkBadgingValue $badgingText "versionName='([^']+)'" $targetSdk = Get-ApkBadgingValue $badgingText "targetSdkVersion:'([^']+)'" $applicationLabel = Get-ApkBadgingValue $badgingText "application-label:'([^']+)'" $nativeCodeLine = ($badging | Where-Object { $_ -like 'native-code:*' } | Select-Object -First 1) $nativeCodes = if ($nativeCodeLine) { [regex]::Matches($nativeCodeLine, "'([^']+)'") | ForEach-Object { $_.Groups[1].Value } } else { @() } if (-not $packageName) { throw 'Package name could not be extracted from APK.' } if (-not $versionCode) { throw 'Version code could not be extracted from APK.' } if (-not $versionName) { throw 'Version name could not be extracted from APK.' } $signatureOutput = & $apksignerPath verify --print-certs $releaseApkPath if ($LASTEXITCODE -ne 0) { throw "apksigner verification failed with exit code $LASTEXITCODE." } $signatureText = ($signatureOutput | Out-String) $signerDn = Get-ApkBadgingValue $signatureText "Signer #1 certificate DN: (.+)" $signerSha256 = Get-ApkBadgingValue $signatureText "Signer #1 certificate SHA-256 digest: ([A-Fa-f0-9]+)" if ($signerDn) { $signerDn = $signerDn.Trim() } if ($signerSha256) { $signerSha256 = $signerSha256.Trim() } $isDebugSigned = $false if ($signerDn -and $signerDn -like '*CN=Android Debug*') { $isDebugSigned = $true } $resolvedName = if ($Name) { $Name } elseif ($applicationLabel) { $applicationLabel } else { 'Ikar' } $resolvedVersion = if ($Version) { $Version } else { '{0}+{1}' -f $versionName, $versionCode } $resolvedRepositoryUrl = if ($RepositoryUrl) { $RepositoryUrl } else { Get-RepositoryUrl } $resolvedPlatform = Resolve-ArgusPlatform $nativeCodes $apkItem = Get-Item $releaseApkPath $apkHash = (Get-FileHash -Path $releaseApkPath -Algorithm SHA256).Hash.ToLowerInvariant() $stageDirectory = Join-Path $repoRoot ("artifacts\argus\{0}\v{1}" -f $Slug, $versionCode) if (-not (Test-Path $stageDirectory)) { New-Item -ItemType Directory -Path $stageDirectory -Force | Out-Null } $stagedApkName = "{0}-{1}.apk" -f $Slug, $versionCode $stagedApkPath = Join-Path $stageDirectory $stagedApkName Copy-Item $releaseApkPath $stagedApkPath -Force $metadata = [ordered]@{ slug = $Slug name = $resolvedName summary = $Summary description = $Description version = $resolvedVersion channel = $Channel platform = $resolvedPlatform packageKind = 'apk' notes = if ($Notes) { $Notes } else { $null } repositoryUrl = if ($resolvedRepositoryUrl) { $resolvedRepositoryUrl } else { $null } homepageUrl = if ($HomepageUrl) { $HomepageUrl } else { $null } androidPackageName = $packageName androidVersionCode = [int]$versionCode targetSdkVersion = if ($targetSdk) { [int]$targetSdk } else { $null } isListed = (-not $Unlisted) packagePath = $stagedApkPath packageFileName = $stagedApkName packageSizeBytes = $apkItem.Length packageSha256 = $apkHash preparedAt = [DateTimeOffset]::UtcNow.ToString('O') verifiedFromApk = [ordered]@{ sourceApk = $releaseApkPath applicationLabel = if ($applicationLabel) { $applicationLabel } else { $null } packageName = $packageName versionCode = [int]$versionCode versionName = $versionName targetSdkVersion = if ($targetSdk) { [int]$targetSdk } else { $null } nativeCodes = $nativeCodes signerDn = if ($signerDn) { $signerDn } else { $null } signerSha256 = if ($signerSha256) { $signerSha256.ToLowerInvariant() } else { $null } isDebugSigned = $isDebugSigned } } $metadataPath = Join-Path $stageDirectory 'argus-package.json' $metadata | ConvertTo-Json -Depth 6 | Set-Content -Path $metadataPath -Encoding UTF8 $summaryLines = @( "slug=$Slug" "name=$resolvedName" "version=$resolvedVersion" "channel=$Channel" "platform=$resolvedPlatform" "packageKind=apk" "androidPackageName=$packageName" "androidVersionCode=$versionCode" "targetSdkVersion=$targetSdk" "packagePath=$stagedApkPath" "packageSizeBytes=$($apkItem.Length)" "packageSha256=$apkHash" "signerDn=$signerDn" "signerSha256=$($signerSha256.ToLowerInvariant())" "isDebugSigned=$isDebugSigned" ) $summaryPath = Join-Path $stageDirectory 'argus-package-summary.txt' $summaryLines | Set-Content -Path $summaryPath -Encoding UTF8 Write-Output "Prepared Argus package files." Write-Output "APK: $stagedApkPath" Write-Output "Metadata: $metadataPath" Write-Output "Summary: $summaryPath"