Publish IKAR Android 1.0.4 and verify release artifacts
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

This commit is contained in:
sevenhill
2026-10-07 22:46:55 +03:00
parent 12b692f4a9
commit 2fcd23a4cf
3 changed files with 120 additions and 15 deletions
+18 -10
View File
@@ -1,45 +1,53 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import '../lib/core/transport/ikar_network.dart';
import 'package:ikar/core/transport/ikar_network.dart';
Future<void> main() async {
Future<void> main(List<String> args) async {
final version = args.isEmpty ? '1.0.0' : args[0];
final code = args.length < 2 ? 1 : int.parse(args[1]);
final reportSuffix = code == 1 ? '' : '-$version';
final http = IkarNetwork.client();
try {
final base = Uri.parse('https://ikar.kusoft.xyz');
final response = await (await http.getUrl(
base.resolve('/api/apps/ikar/manifest'),
)).close();
if (response.statusCode != 200)
if (response.statusCode != 200) {
throw StateError('Release manifest unavailable');
}
final manifest = jsonDecode(await utf8.decodeStream(response));
if (manifest['app']['packageName'] != 'ru.ikar.app' ||
manifest['release']['version'] != '1.0.0+1')
manifest['release']['version'] != '$version+$code') {
throw StateError('Incorrect release identity');
}
final expected = (await File(
'artifacts/ikar-1.0.0+1.apk.sha256',
'artifacts/ikar-$version+$code.apk.sha256',
).readAsString()).split(' ').first;
if (manifest['release']['sha256'] != expected)
if (manifest['release']['sha256'] != expected) {
throw StateError('Manifest checksum mismatch');
}
final download = await (await http.getUrl(
base.resolve(manifest['release']['downloadPath']),
)).close();
if (download.statusCode != 200)
if (download.statusCode != 200) {
throw StateError('APK download unavailable');
}
final digest = await sha256.bind(download).first;
if (digest.toString() != expected)
if (digest.toString() != expected) {
throw StateError('Downloaded APK checksum mismatch');
}
final report = {
'passed': true,
'package': 'ru.ikar.app',
'version': '1.0.0+1',
'version': '$version+$code',
'sha256': expected,
'tlsValidation': true,
'bytes': manifest['release']['packageSizeBytes'],
'timestamp': DateTime.now().toUtc().toIso8601String(),
};
await File(
'artifacts/release-verification.json',
'artifacts/release-verification$reportSuffix.json',
).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
stdout.writeln(jsonEncode(report));
} finally {
+87
View File
@@ -0,0 +1,87 @@
"""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())