Add Android APK inspection and device verification gates
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath, [Parameter(Mandatory)][string]$EvidenceDir)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sdk = if ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android/Sdk' }
|
||||
$aapt = Get-ChildItem -LiteralPath (Join-Path $sdk 'build-tools') -Filter aapt.exe -Recurse | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if (!$aapt) { throw 'APK manifest 검사에 필요한 aapt를 찾지 못했습니다.' }
|
||||
$badging = & $aapt.FullName dump badging $ApkPath
|
||||
$nativeExit = $LASTEXITCODE
|
||||
$badging | Set-Content -LiteralPath (Join-Path $EvidenceDir 'apk-badging.txt') -Encoding utf8
|
||||
if ($nativeExit -ne 0) { throw "aapt 종료 코드: $nativeExit" }
|
||||
$zip = [IO.Compression.ZipFile]::OpenRead($ApkPath)
|
||||
try {
|
||||
$entries = @($zip.Entries | ForEach-Object FullName)
|
||||
$abis = @($entries | Where-Object { $_ -match '^lib/([^/]+)/libil2cpp\.so$' } | ForEach-Object { ($_ -split '/')[1] })
|
||||
if ($abis.Count -eq 0) { throw 'APK에 IL2CPP 라이브러리가 없습니다.' }
|
||||
foreach ($abi in $abis) { if ($entries -notcontains "lib/$abi/libunity.so") { throw "$abi Unity 런타임 누락" } }
|
||||
if ($entries | Where-Object { $_ -match '(?i)(SourceAssets|conda|Qwen|safetensors)' }) { throw '제작 환경 파일이 APK에 포함되었습니다.' }
|
||||
$entries | Set-Content -LiteralPath (Join-Path $EvidenceDir 'apk-entries.txt') -Encoding utf8
|
||||
} finally { $zip.Dispose() }
|
||||
$package = [regex]::Match(($badging -join "`n"), "package: name='([^']+)'").Groups[1].Value
|
||||
$activity = [regex]::Match(($badging -join "`n"), "launchable-activity: name='([^']+)'").Groups[1].Value
|
||||
if ($package -ne 'com.magnetismheroes.game') { throw "예상하지 않은 패키지: $package" }
|
||||
if (!$activity) { throw 'APK 시작 Activity를 찾지 못했습니다.' }
|
||||
if (($badging -join "`n") -match "uses-permission: name='android.permission.INTERNET'") { throw '오프라인 앱에 INTERNET 권한이 있습니다.' }
|
||||
[ordered]@{ apk=$ApkPath; apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash; package=$package;
|
||||
abis=$abis; activity=$activity; aaptCommand=('"' + $aapt.FullName + '" dump badging "' + $ApkPath + '"'); aaptExit=$nativeExit;
|
||||
sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain) } |
|
||||
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'apk-metadata.json') -Encoding utf8
|
||||
@@ -0,0 +1,29 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][ValidatePattern('^[a-z0-9-]+$')][string]$Step,
|
||||
[string[]]$AdbArguments = @(), [string]$Serial = 'emulator-5554',
|
||||
[string]$EvidenceDir = 'artifacts/verification/FirstSlice', [int]$DelayMilliseconds = 800,
|
||||
[switch]$Audio
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sdk = if ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android/Sdk' }
|
||||
$adb = Join-Path $sdk 'platform-tools/adb.exe'
|
||||
New-Item -ItemType Directory -Path $EvidenceDir -Force | Out-Null
|
||||
$commands = [Collections.Generic.List[object]]::new()
|
||||
function Invoke-Adb([string[]]$Arguments) {
|
||||
$output = & $adb -s $Serial @Arguments 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
$commands.Add([ordered]@{ command=('"' + $adb + '" -s ' + $Serial + ' ' + ($Arguments -join ' ')); exitCode=$code; output=($output -join "`n") })
|
||||
if ($code -ne 0) { throw "ADB 실패: $($Arguments -join ' '), 종료 코드 $code" }
|
||||
return $output
|
||||
}
|
||||
try {
|
||||
if ($AdbArguments.Count) { Invoke-Adb $AdbArguments | Out-Null }
|
||||
if ($Audio) { Invoke-Adb @('shell','dumpsys','media.audio_flinger') | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Step-audio-flinger.txt") -Encoding utf8 }
|
||||
Start-Sleep -Milliseconds $DelayMilliseconds
|
||||
Invoke-Adb @('shell','screencap','-p',"/sdcard/magnetismheroes-$Step.png") | Out-Null
|
||||
Invoke-Adb @('pull',"/sdcard/magnetismheroes-$Step.png",(Join-Path $EvidenceDir "$Step.png")) | Out-Null
|
||||
Invoke-Adb @('shell','rm',"/sdcard/magnetismheroes-$Step.png") | Out-Null
|
||||
} finally {
|
||||
[ordered]@{ step=$Step; observedUtc=[DateTime]::UtcNow.ToString('o'); serial=$Serial; sourceCommit=(& git rev-parse HEAD);
|
||||
commands=$commands.ToArray() } | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Step-commands.json") -Encoding utf8
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath, [string]$Milestone='FirstSlice', [Parameter(Mandatory)][string]$EvidenceDir, [string]$Serial='emulator-5554')
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sdk = if ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android/Sdk' }
|
||||
$adb = Join-Path $sdk 'platform-tools/adb.exe'
|
||||
$commands = [Collections.Generic.List[object]]::new()
|
||||
$checks = [Collections.Generic.List[object]]::new()
|
||||
function Invoke-Adb([string[]]$Arguments) {
|
||||
$output = & $adb -s $Serial @Arguments 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
$commands.Add([ordered]@{ command=('"' + $adb + '" -s ' + $Serial + ' ' + ($Arguments -join ' ')); exitCode=$code; output=($output -join "`n") })
|
||||
if ($code -ne 0) { throw "ADB 실패: $($Arguments -join ' '), 종료 코드 $code" }
|
||||
return ($output -join "`n").Trim()
|
||||
}
|
||||
function Assert-Check([string]$Name,[bool]$Condition,[string]$Actual) {
|
||||
$checks.Add([ordered]@{ criterion=$Name; passed=$Condition; actual=$Actual })
|
||||
if (!$Condition) { throw "검사 실패: $Name ($Actual)" }
|
||||
}
|
||||
$result = [ordered]@{ milestone=$Milestone; serial=$Serial; sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain); status='failed'; commands=@(); checks=@() }
|
||||
try {
|
||||
Assert-Check 'clean-source' (!$result.sourceDirty) $result.sourceCommit
|
||||
& (Join-Path $PSScriptRoot 'inspect-apk.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir
|
||||
$apk = Get-Content -LiteralPath (Join-Path $EvidenceDir 'apk-metadata.json') -Raw | ConvertFrom-Json
|
||||
$result.apkSha256 = $apk.apkSha256
|
||||
$supported = (Invoke-Adb @('shell','getprop','ro.product.cpu.abilist')) -split ','
|
||||
$native = $supported[0]
|
||||
$bridge = Invoke-Adb @('shell','getprop','ro.dalvik.vm.native.bridge')
|
||||
$intersection = @($apk.abis | Where-Object { $supported -contains $_ })
|
||||
$result.device = [ordered]@{ supportedAbis=$supported; nativeArchitecture=$native; bridge=$bridge; apkSupportedIntersection=$intersection;
|
||||
model=(Invoke-Adb @('shell','getprop','ro.product.model')); api=(Invoke-Adb @('shell','getprop','ro.build.version.sdk'));
|
||||
features=(Invoke-Adb @('shell','pm','list','features')); graphics=(Invoke-Adb @('shell','dumpsys','SurfaceFlinger')) }
|
||||
Assert-Check 'supported-abi-intersection' ($intersection.Count -gt 0) ($intersection -join ',')
|
||||
if ($supported[0] -ne $intersection[0]) { Assert-Check 'arm-native-bridge' ($bridge -match '^lib.+\.so$') $bridge }
|
||||
Invoke-Adb @('install','-r',$ApkPath) | Out-Null
|
||||
Invoke-Adb @('shell','am','start','-W','-n',($apk.package + '/' + $apk.activity)) | Out-Null
|
||||
Start-Sleep -Seconds 3
|
||||
$pidValue = Invoke-Adb @('shell','pidof',$apk.package)
|
||||
Assert-Check 'running-process' ($pidValue -match '^\d+') $pidValue
|
||||
Invoke-Adb @('logcat','-d','-s','Unity','AndroidRuntime') | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-runtime.log') -Encoding utf8
|
||||
& (Join-Path $PSScriptRoot 'observe.ps1') -Step 'smoke-home' -Serial $Serial -EvidenceDir $EvidenceDir
|
||||
$manualPath = Join-Path $EvidenceDir 'android-observations.json'
|
||||
Assert-Check 'observations-present' (Test-Path -LiteralPath $manualPath) $manualPath
|
||||
$manual = Get-Content -LiteralPath $manualPath -Raw | ConvertFrom-Json
|
||||
Assert-Check 'observations-match-apk' ($manual.apkSha256 -eq $apk.apkSha256 -and $manual.sourceCommit -eq $result.sourceCommit -and $manual.serial -eq $Serial) $manual.apkSha256
|
||||
$required = @('landscape-korean-assets','precollect-tap','direct-collection','camera-orbit','incomplete-restore','count-success-same-board','free-move','free-restore','offline-collection-count','offline-restore','guide-collect-playback','guide-count-playback','count01-playback')
|
||||
foreach ($id in $required) {
|
||||
$observations = @($manual.cases | Where-Object id -eq $id)
|
||||
Assert-Check "observation-$id" ($observations.Count -eq 1 -and $observations[0].passed -eq $true -and $observations[0].actual -and $observations[0].procedure) (($observations | ForEach-Object actual) -join ' ')
|
||||
foreach ($file in $observations[0].evidence) {
|
||||
$path = Join-Path $EvidenceDir $file
|
||||
Assert-Check "evidence-$id-$file" ((Test-Path -LiteralPath $path) -and (Get-Item -LiteralPath $path).Length -gt 0) $file
|
||||
}
|
||||
Assert-Check "has-evidence-$id" (@($observations[0].evidence).Count -gt 0) (@($observations[0].evidence) -join ',')
|
||||
}
|
||||
$result.manualObservations = $manual
|
||||
$result.status='passed'
|
||||
} catch { $result.error=$_.Exception.Message; Write-Host $result.error -ForegroundColor Red }
|
||||
finally {
|
||||
$result.commands=$commands.ToArray(); $result.checks=$checks.ToArray(); $result.finishedUtc=[DateTime]::UtcNow.ToString('o')
|
||||
$result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-device-evidence.json') -Encoding utf8
|
||||
}
|
||||
if ($result.status -ne 'passed') { exit 1 }
|
||||
Write-Host "Android 실제 기기 검사와 $($required.Count)개 관찰 검사 통과. 근거: $EvidenceDir"
|
||||
exit 0
|
||||
@@ -45,6 +45,8 @@ try {
|
||||
if ($LASTEXITCODE -ne 0 -or !(Test-Path -LiteralPath $apkPath)) { throw 'Android build failed or produced no APK.' }
|
||||
$record.apk = $apkPath
|
||||
$record.apkSha256 = (Get-FileHash -LiteralPath $apkPath -Algorithm SHA256).Hash
|
||||
& (Join-Path $PSScriptRoot 'android/inspect-apk.ps1') -ApkPath $apkPath -EvidenceDir $evidenceDir
|
||||
$record.apkMetadata = Join-Path $evidenceDir 'apk-metadata.json'
|
||||
if ($Milestone -eq 'Final') {
|
||||
Copy-Item -LiteralPath $apkPath -Destination (Join-Path $projectRoot 'artifacts/android/MagnetismHeroes.apk')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user