fix: repair Android startup and improve mobile web layouts
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m17s
ci / ts / ts (packages + web) (push) Successful in 2m22s
release / android (APK в релиз) (push) Successful in 4m15s
release / release (версия и заметки) (push) Successful in 6s

This commit is contained in:
mrleo1nid committed 2026-10-21 17:14:05 +03:00
1 parent 4c2286e70d
commit b0cbd0f149
30 files changed
+609 -140

No files matched your search

+6 -2
View File
@@ -3,6 +3,7 @@
// - нативные библиотеки только под arm64-v8a: неполный каталог другой ABI роняет приложение при старте
// («couldn't find DSO to load: libexpo-modules-core.so» на x86-эмуляторе);
// - есть библиотеки, без которых приложение не запустится;
// - .so сжаты (useLegacyPackaging): Android распакует их для SoLoader / ARM-трансляции;
// - PT_LOAD-сегменты каждой .so выровнены по 16 КБ: иначе на устройствах с 16 КБ страницами памяти
// (Android 15+) библиотека не загружается и приложение закрывается сразу после запуска.
// Запуск: node scripts/check-apk.mjs app-release.apk
@@ -29,11 +30,11 @@ export function zipEntries(buf) {
const commentLen = buf.readUInt16LE(p + 32);
const local = buf.readUInt32LE(p + 42);
const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
out.set(name, () => {
out.set(name, Object.assign(() => {
const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
const raw = buf.subarray(start, start + size);
return method === 0 ? raw : inflateRawSync(raw);
});
}, { compressionMethod: method }));
p += 46 + nameLen + extraLen + commentLen;
}
return out;
@@ -62,6 +63,9 @@ export function checkApk(buf) {
const mine = new Set(libs.filter((n) => n.startsWith(`lib/${ABI}/`)).map((n) => n.split('/')[2]));
for (const lib of REQUIRED) if (!mine.has(lib)) problems.push(`нет lib/${ABI}/${lib}`);
for (const name of libs.filter((n) => n.startsWith(`lib/${ABI}/`))) {
if (entries.get(name).compressionMethod !== 8) {
problems.push(`${name}: библиотека не сжата, включите useLegacyPackaging для ARM-трансляции`);
}
const align = loadAlignment(entries.get(name)());
if (align < PAGE) problems.push(`${name}: сегменты выровнены по ${align} байт, нужно ${PAGE} (16 КБ страницы)`);
}
+17 -6
View File
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { deflateRawSync } from 'node:zlib';
import { checkApk, loadAlignment, REQUIRED } from './check-apk.mjs';
@@ -16,27 +17,30 @@ function elf(align) {
return b;
}
/** ZIP без сжатия из пар [имя, содержимое]. */
function zip(files) {
/** ZIP из пар [имя, содержимое]. */
function zip(files, compress = true) {
const locals = [];
const central = [];
let offset = 0;
for (const [name, data] of files) {
const packed = compress ? deflateRawSync(data) : data;
const n = Buffer.from(name);
const lh = Buffer.alloc(30);
lh.writeUInt32LE(0x04034b50, 0);
lh.writeUInt32LE(data.length, 18);
lh.writeUInt16LE(compress ? 8 : 0, 8);
lh.writeUInt32LE(packed.length, 18);
lh.writeUInt32LE(data.length, 22);
lh.writeUInt16LE(n.length, 26);
locals.push(lh, n, data);
locals.push(lh, n, packed);
const ch = Buffer.alloc(46);
ch.writeUInt32LE(0x02014b50, 0);
ch.writeUInt32LE(data.length, 20);
ch.writeUInt16LE(compress ? 8 : 0, 10);
ch.writeUInt32LE(packed.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;
offset += 30 + n.length + packed.length;
}
const cd = Buffer.concat(central);
const end = Buffer.alloc(22);
@@ -58,6 +62,13 @@ test('accepts an arm64-only APK aligned to 16 KB', () => {
assert.deepEqual(checkApk(apk).problems, []);
});
test('rejects direct APK library loading that breaks ARM translation', () => {
const apk = zip(REQUIRED.map((l) => [`lib/arm64-v8a/${l}`, elf(16384)]), false);
const { problems } = checkApk(apk);
assert.equal(problems.length, REQUIRED.length);
assert.ok(problems.every((p) => p.includes('useLegacyPackaging')));
});
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}`,