133 lines
9.3 KiB
PowerShell
133 lines
9.3 KiB
PowerShell
# APK 검사 (P0, 10/11): 플러그인 등록 (GeneratedPluginRegistrant) 이 dex 에 들어 있는지 본다.
|
||
# - 이 파일이 빠지면 플러그인이 하나도 등록되지 않아 켜자마자 꺼진다 ('JNI is not initialized … too early', 묶음5 a192ef6 시험판).
|
||
# - 원인: android\app\src\main\java\io\flutter\plugins\GeneratedPluginRegistrant.java (git 밖, 생성 파일) 를 지운 뒤
|
||
# flutter build apk --no-pub 로 빌드하면 다시 만들어지지 않는다. 지웠으면 --no-pub 없이 빌드하거나 먼저 flutter pub get.
|
||
# - (195) third_party 의 네이티브 실행 파일: android\app\third_party_libs.txt 의 것이 모두 들어 있고, 목록에 없는 third_party 파일은 없는지.
|
||
# - 시험판 번호 (관리자 지시 10/12): 시험판의 versionCode 가 정식 판 (git 태그 v년.월.일_번호) 과 같으면 실패.
|
||
# 918 이 --build-number 없이 빌드되어 정식 판 v2026.10.10_001 (261010001) 과 똑같이 보였다 - 사용자 기기에 깔리면 정식 판으로 보이고
|
||
# 새 버전 확인도 "최신과 같음" 이 된다. 시험판은 flutter build apk --build-number 9xx 로 빌드한다. 정식 판 APK 는 -Release.
|
||
# - 순번 900 이상의 v 태그 (시험판 Pre-release, 예: v2026.10.18_977) 는 정식 판으로 보지 않는다 (관리자 지시 10/19, 28 지적:
|
||
# 시험판에 태그를 단 뒤 같은 APK 를 다시 검사하면 "정식 판 번호와 같음" 으로 거짓 실패). 정식 판 (-Release) 인데 순번이 900 이상이면 실패.
|
||
# - versionCode 의 모양 (관리자 지시 10/20, 15 제안): versionCode = 판 이름 날짜 (2026.10.18 → 261018) × 1000 + 순번 (000~999).
|
||
# 987 을 --build-number 261018987 로 빌드해 versionCode 522036987 (날짜가 두 번 더해짐) 이 나왔는데 통과했다 - 사용자 기기에 깔리면
|
||
# 그 뒤의 정식 판 (261018xxx) 이 "낮은 버전" 이라 업데이트가 막힌다. build.gradle.kts 가 날짜를 붙이므로 APK 의 --build-number 는 순번만 (예 987).
|
||
# 정식 판 (-Release) 은 순번 < 900, 시험판은 순번 >= 900.
|
||
# - HWP 보기 (관리자 지시 10/20, 28 지적): APK 안 libjj_rhwp.so 가 앱의 Dart 와 같은 C ABI 인지 - 내보낸 기호 (NDK llvm-nm) 에
|
||
# jj_rhwp_cancel · jj_rhwp_abi_version 등이 모두 있어야 한다 (옛 .so 는 이름 · 목록만으로는 못 가린다).
|
||
# 사용: powershell -ExecutionPolicy Bypass -File tool\check_apk.ps1 -Apk build\app\outputs\flutter-apk\app-release.apk [-Release]
|
||
param(
|
||
[Parameter(Mandatory = $true)][string]$Apk,
|
||
# 목록 파일 (기본: 이 스크립트가 있는 앱 폴더의 android\app\third_party_libs.txt)
|
||
[string]$Libs = '',
|
||
# 정식 판 APK (태그와 같은 번호가 맞음) - 시험판 번호 검사를 하지 않는다
|
||
[switch]$Release
|
||
)
|
||
$ErrorActionPreference = 'Stop'
|
||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||
$Apk = (Resolve-Path $Apk).Path
|
||
$want = [System.Text.Encoding]::ASCII.GetBytes('Lio/flutter/plugins/GeneratedPluginRegistrant;')
|
||
|
||
# 바이트 배열 안에 [needle] 이 있는지
|
||
function Test-Bytes([byte[]]$hay, [byte[]]$needle) {
|
||
$first = $needle[0]
|
||
$i = [Array]::IndexOf($hay, $first)
|
||
while ($i -ge 0 -and $i -le $hay.Length - $needle.Length) {
|
||
$ok = $true
|
||
for ($k = 1; $k -lt $needle.Length; $k++) {
|
||
if ($hay[$i + $k] -ne $needle[$k]) { $ok = $false; break }
|
||
}
|
||
if ($ok) { return $true }
|
||
$i = [Array]::IndexOf($hay, $first, $i + 1)
|
||
}
|
||
return $false
|
||
}
|
||
|
||
$app = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||
if (-not $Libs) { $Libs = Join-Path $app 'android\app\third_party_libs.txt' }
|
||
$listed = @(Get-Content $Libs | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
|
||
# third_party 에 있는 파일 (목록 밖이면 APK 에 없어야 함)
|
||
$tp = @()
|
||
foreach ($d in 'third_party\rsync\android', 'third_party\sdcpp\android', 'third_party\tor\android', 'third_party\7zip\android', 'third_party\rhwp\android') {
|
||
$dir = Join-Path $app $d
|
||
if (Test-Path $dir) { $tp += @(Get-ChildItem $dir -Recurse -Filter *.so | ForEach-Object { $_.Name }) }
|
||
}
|
||
$zip = [System.IO.Compression.ZipFile]::OpenRead($Apk)
|
||
try {
|
||
$libsIn = @($zip.Entries | Where-Object { $_.FullName -like 'lib/arm64-v8a/*.so' } | ForEach-Object { $_.Name })
|
||
$dexes = @($zip.Entries | Where-Object { $_.FullName -match '^classes\d*\.dex$' })
|
||
if ($dexes.Count -eq 0) { throw "dex 가 없습니다: $Apk" }
|
||
# HWP 보기 라이브러리는 꺼내 두고 아래에서 기호를 본다
|
||
$rhwpSo = $null
|
||
$rhwpEntry = $zip.Entries | Where-Object { $_.FullName -eq 'lib/arm64-v8a/libjj_rhwp.so' } | Select-Object -First 1
|
||
if ($rhwpEntry) {
|
||
$rhwpSo = Join-Path ([System.IO.Path]::GetTempPath()) ("jj_check_rhwp_{0}.so" -f [guid]::NewGuid().ToString('N'))
|
||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($rhwpEntry, $rhwpSo)
|
||
}
|
||
$found = $null
|
||
foreach ($e in $dexes) {
|
||
$s = $e.Open()
|
||
$ms = New-Object System.IO.MemoryStream
|
||
try { $s.CopyTo($ms) } finally { $s.Dispose() }
|
||
if (Test-Bytes $ms.ToArray() $want) { $found = $e.FullName; break }
|
||
}
|
||
} finally {
|
||
$zip.Dispose()
|
||
}
|
||
$bad = @()
|
||
# HWP 보기: libjj_rhwp.so 의 내보낸 기호 (lib/platform/android/hwp_pdf_ffi.dart 의 requiredSymbols 와 같게)
|
||
$rhwpNote = ''
|
||
if ($rhwpSo) {
|
||
try {
|
||
$nm = Get-ChildItem (Join-Path $app '..\tools\android-sdk\ndk') -Recurse -Filter llvm-nm.exe -ErrorAction SilentlyContinue | Select-Object -Last 1
|
||
if (-not $nm) { throw 'llvm-nm.exe 를 찾지 못했습니다 (tools\android-sdk\ndk)' }
|
||
$syms = @(& $nm.FullName -D --defined-only $rhwpSo 2>$null | ForEach-Object { ($_ -split '\s+')[-1] })
|
||
$need = 'jj_rhwp_abi_version', 'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_cancel', 'jj_rhwp_cancel_all', 'jj_rhwp_close', 'jj_rhwp_free'
|
||
$miss = @($need | Where-Object { $syms -notcontains $_ })
|
||
if ($miss) { $bad += "libjj_rhwp.so 가 옛 C ABI 입니다 - 없는 함수: $($miss -join ', ') (sh tool/build_rhwp_android.sh 로 다시 빌드)" }
|
||
else { $rhwpNote = " · libjj_rhwp.so 함수 $($need.Count)개 있음" }
|
||
} finally {
|
||
Remove-Item $rhwpSo -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
# 시험판 번호: aapt2 로 versionCode · versionName 을 읽어 정식 판 태그의 번호와 견준다 (build.gradle.kts 와 같은 셈: 년%100 · 월 · 일 · 번호)
|
||
$aapt = Get-ChildItem (Join-Path $app '..\tools\android-sdk\build-tools') -Recurse -Filter aapt2.exe -ErrorAction SilentlyContinue | Select-Object -Last 1
|
||
if (-not $aapt -and $env:ANDROID_HOME) { $aapt = Get-ChildItem (Join-Path $env:ANDROID_HOME 'build-tools') -Recurse -Filter aapt2.exe -ErrorAction SilentlyContinue | Select-Object -Last 1 }
|
||
if (-not $aapt) { throw 'aapt2.exe 를 찾지 못했습니다 (tools\android-sdk\build-tools 또는 ANDROID_HOME)' }
|
||
$badging = (& $aapt.FullName dump badging $Apk 2>$null | Select-Object -First 1)
|
||
if ($badging -notmatch "versionCode='(\d+)' versionName='([^']*)'") { throw "versionCode 를 읽지 못했습니다: $badging" }
|
||
$code = [long]$Matches[1]; $vname = $Matches[2]
|
||
$tags = @(git -C $app tag --list 'v*' 2>$null)
|
||
$same = @(foreach ($t in $tags) {
|
||
# 순번 900 이상 = 시험판 태그 (정식 판이 아님)
|
||
if ($t -match '^v(\d+)\.(\d+)\.(\d+)_(\d+)$' -and [long]$Matches[4] -lt 900 -and
|
||
([long]$Matches[1] % 100) * 10000000 + [long]$Matches[2] * 100000 + [long]$Matches[3] * 1000 + [long]$Matches[4] -eq $code) { $t }
|
||
})
|
||
# versionCode 의 모양: 판 이름의 날짜 × 1000 + 순번 (build.gradle.kts 와 같은 셈)
|
||
if ($vname -match '^(\d+)\.(\d+)\.(\d+)') {
|
||
$day = ([long]$Matches[1] % 100) * 10000 + [long]$Matches[2] * 100 + [long]$Matches[3]
|
||
if ([math]::Floor($code / 1000) -ne $day) {
|
||
$bad += "versionCode $code 가 판 이름 $vname 의 날짜 ($day × 1000 + 순번) 와 맞지 않음 - APK 는 --build-number 에 순번만 (예 987, 날짜를 붙이지 않음)"
|
||
} elseif (-not $Release -and ($code % 1000) -lt 900) {
|
||
$bad += "시험판인데 versionCode $code ($vname) 의 순번이 $($code % 1000) - 시험판은 --build-number 9xx (정식 판이면 -Release)"
|
||
}
|
||
} else {
|
||
$bad += "판 이름을 읽지 못함: '$vname' (년.월.일 이어야 함)"
|
||
}
|
||
if ($Release -and ($code % 1000) -ge 900) {
|
||
$bad += "정식 판 (-Release) 인데 versionCode $code ($vname) 의 순번이 $($code % 1000) - 시험판 번호입니다 (정식 판은 --build-number 없이, pubspec 의 +순번)"
|
||
}
|
||
if (-not $Release -and $same) {
|
||
$bad += "시험판인데 versionCode $code ($vname) 가 정식 판 $($same -join ', ') 과 같음 - flutter build apk --build-number 9xx 로 다시 빌드 (정식 판이면 -Release)"
|
||
}
|
||
foreach ($n in $listed) { if ($libsIn -notcontains $n) { $bad += "목록에 있는데 APK 에 없음: $n" } }
|
||
foreach ($n in ($tp | Select-Object -Unique)) { if ($listed -notcontains $n -and $libsIn -contains $n) { $bad += "목록에 없는데 APK 에 들어감: $n" } }
|
||
if ($bad) {
|
||
$bad | ForEach-Object { Write-Host "실패: $_ ($Libs)" -ForegroundColor Red }
|
||
exit 1
|
||
}
|
||
if (-not $found) {
|
||
Write-Host "실패: GeneratedPluginRegistrant 가 dex 에 없습니다 (켜자마자 꺼지는 APK). flutter pub get 뒤 --no-pub 없이 다시 빌드하세요." -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
Write-Host "통과: GeneratedPluginRegistrant 있음 ($found) · third_party 실행 파일 $($listed.Count)개 목록과 같음$rhwpNote · versionCode $code ($vname)$(if ($Release) { ' (정식 판)' } else { ' (시험판 - 정식 판 번호와 다름)' })"
|