fix(mobile): APK starts on Android 15+ 16 KB page devices and emulators
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m56s
release / android (APK в релиз) (push) Successful in 4m53s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 3m27s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m2s
ci / ts / ts (packages + web) (push) Successful in 1m48s
release / release (версия и заметки) (push) Successful in 8s

On an Android 16 emulator the release APK crashed at launch with
"Cannot read property 'NativeModule' of undefined": libexpo-modules-core.so
was missing for the ABI the device picked. Two build defects:

- librnskia.so was linked with 4 KB segment alignment (Skia 2.0.0-next.4
  lacks ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES), so 16 KB page devices cannot
  load it; a pnpm patch adds the flag
- the APK carried incomplete x86, x86_64 and armeabi-v7a folders from the
  React Native libraries; an Expo config plugin keeps only arm64-v8a, the ABI
  the native modules are built for (the APK also halves in size)

scripts/check-apk.mjs now fails the release on another ABI, a missing core
library or a library not aligned to 16 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-21 03:22:13 +03:00
1 parent 66f5749fc1
commit 73e642c1ec
11 files changed
+217 -3

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { checkApk, loadAlignment, REQUIRED } from './check-apk.mjs';
/** ELF64 с одним PT_LOAD и заданным выравниванием. */
function elf(align) {
const b = Buffer.alloc(64 + 56);
b.writeUInt32BE(0x7f454c46, 0);
b[4] = 2;
b.writeBigUInt64LE(64n, 0x20);
b.writeUInt16LE(56, 0x36);
b.writeUInt16LE(1, 0x38);
b.writeUInt32LE(1, 64);
b.writeBigUInt64LE(BigInt(align), 64 + 0x30);
return b;
}
/** ZIP без сжатия из пар [имя, содержимое]. */
function zip(files) {
const locals = [];
const central = [];
let offset = 0;
for (const [name, data] of files) {
const n = Buffer.from(name);
const lh = Buffer.alloc(30);
lh.writeUInt32LE(0x04034b50, 0);
lh.writeUInt32LE(data.length, 18);
lh.writeUInt32LE(data.length, 22);
lh.writeUInt16LE(n.length, 26);
locals.push(lh, n, data);
const ch = Buffer.alloc(46);
ch.writeUInt32LE(0x02014b50, 0);
ch.writeUInt32LE(data.length, 20);
ch.writeUInt32LE(data.length, 24);
ch.writeUInt16LE(n.length, 28);
ch.writeUInt32LE(offset, 42);
central.push(ch, n);
offset += 30 + n.length + data.length;
}
const cd = Buffer.concat(central);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(files.length, 8);
end.writeUInt16LE(files.length, 10);
end.writeUInt32LE(cd.length, 12);
end.writeUInt32LE(offset, 16);
return Buffer.concat([...locals, cd, end]);
}
test('reads the smallest PT_LOAD alignment', () => {
assert.equal(loadAlignment(elf(16384)), 16384);
assert.equal(loadAlignment(elf(4096)), 4096);
});
test('accepts an arm64-only APK aligned to 16 KB', () => {
const apk = zip(REQUIRED.map((l) => [`lib/arm64-v8a/${l}`, elf(16384)]));
assert.deepEqual(checkApk(apk).problems, []);
});
test('rejects other ABIs, missing libraries and 4 KB alignment', () => {
const libs = REQUIRED.filter((l) => l !== 'libexpo-modules-core.so').map((l) => [
`lib/arm64-v8a/${l}`,
elf(l === 'librnskia.so' ? 4096 : 16384),
]);
const { problems } = checkApk(zip([...libs, ['lib/x86_64/libjsi.so', elf(16384)]]));
assert.equal(problems.length, 3);
assert.match(problems.join('\n'), /lib\/x86_64/);
assert.match(problems.join('\n'), /libexpo-modules-core\.so/);
assert.match(problems.join('\n'), /librnskia\.so.*4096/);
});