Files
IKAR/tool/verify_android_apk.py
T
sevenhill 2fcd23a4cf
Tests / test (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Publish IKAR Android 1.0.4 and verify release artifacts
2026-10-07 22:46:55 +03:00

88 lines
5.0 KiB
Python

"""Verify APK identity, upgrade signature, fresh Flutter libraries and icon assets."""
import argparse
import hashlib
import io
import json
from pathlib import Path
import re
import subprocess
import zipfile
from PIL import Image
def sha(data):
return hashlib.sha256(data).hexdigest()
def verify(args):
apk = Path(args.apk)
previous = Path(args.previous)
sdk = Path(args.build_tools)
metadata = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', 'badging', str(apk)], text=True, encoding='utf-8')
identity = re.search(r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'", metadata)
if identity is None or identity.groups() != ('ru.ikar.app', str(args.code), args.version):
raise RuntimeError('Incorrect Android package or version')
def certificate(path):
output = subprocess.check_output([str(sdk / 'apksigner.bat'), 'verify', '--verbose', '--print-certs', str(path)], text=True, encoding='utf-8')
values = re.findall(r'Signer #\d+ certificate SHA-256 digest: ([0-9a-f]+)', output)
if len(values) != 1 or 'Verified using v2 scheme (APK Signature Scheme v2): true' not in output:
raise RuntimeError('APK signature verification failed')
return values[0]
signer = certificate(apk)
if signer != certificate(previous):
raise RuntimeError('Signing certificate differs from the previously published APK')
latest_source = max(path.stat().st_mtime for path in Path('lib').rglob('*.dart'))
libraries = []
assets = []
with zipfile.ZipFile(apk) as package:
icon_path = re.search(r"application-icon-160:'([^']+)'", metadata).group(1)
icon_tree = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', 'xmltree', str(apk), icon_path], text=True, encoding='utf-8')
inset = re.search(r'android:inset\([^)]*\)=\(type 0x6\)0x([0-9a-f]+)', icon_tree)
if inset is None or int(inset[1], 16) >> 8 != 0:
raise RuntimeError('Adaptive launcher icon still has the old inset')
resources = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', '--values', 'resources', str(apk)], text=True, encoding='utf-8')
paths = re.findall(r'[^\n]*ru\.ikar\.app:drawable/ic_launcher_foreground: t=0x03[^\n]*\n\s*\(string8\) "([^"]+)"', resources)
expected_icons = {}
for path in Path('android/app/src/main/res').glob('drawable-*/ic_launcher_foreground.png'):
with Image.open(path) as image:
expected_icons[image.size] = image.convert('RGBA').tobytes()
if len(paths) != len(expected_icons):
raise RuntimeError('Missing launcher foreground densities')
for path in paths:
with Image.open(io.BytesIO(package.read(path))) as image:
if image.convert('RGBA').tobytes() != expected_icons.get(image.size):
raise RuntimeError('Packaged launcher icon differs from the current drawing')
for abi in ('armeabi-v7a', 'arm64-v8a', 'x86_64'):
generated = Path(f'build/app/intermediates/flutter/ikarRelease/jniLibs/{abi}/libapp.so')
stripped = Path(f'build/app/intermediates/stripped_native_libs/ikarRelease/stripIkarReleaseDebugSymbols/out/lib/{abi}/libapp.so')
actual = sha(package.read(f'lib/{abi}/libapp.so'))
if actual != sha(stripped.read_bytes()) or generated.stat().st_mtime < latest_source:
raise RuntimeError(f'Stale Flutter library for {abi}')
for name in ('libflutter.so', 'libfjs.so', 'libkomet_crypto.so', 'libkomet_tlottie.so'):
if not package.read(f'lib/{abi}/{name}'):
raise RuntimeError(f'Missing native dependency: {abi}/{name}')
libraries.append({'abi': abi, 'apkSha256': actual, 'matchesStripped': True, 'generatedAfterLastSourceChange': True})
for name in ('ikar.png', 'ikar_icon.png'):
expected = Path('assets') / name
if sha(package.read(f'assets/flutter_assets/assets/{name}')) != sha(expected.read_bytes()):
raise RuntimeError(f'Stale icon asset: {name}')
assets.append(name)
report = {'passed': True, 'package': 'ru.ikar.app', 'version': args.version,
'androidVersionCode': args.code, 'sha256': sha(apk.read_bytes()), 'bytes': apk.stat().st_size,
'certificateSha256': signer, 'upgradeSignerMatches': True, 'libraries': libraries, 'currentIconAssets': assets,
'adaptiveLauncherMatchesCurrentDrawing': True, 'adaptiveLauncherInsetZero': True}
Path(f'artifacts/apk-verification-{args.version}.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--apk', required=True)
parser.add_argument('--previous', required=True)
parser.add_argument('--version', required=True)
parser.add_argument('--code', required=True, type=int)
parser.add_argument('--build-tools', default='C:/Tools/AndroidSdk/build-tools/36.0.0')
verify(parser.parse_args())