88 lines
4.4 KiB
JavaScript
88 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Проверка собранного APK перед публикацией (release.yml):
|
|
// - нативные библиотеки только под 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
|
|
import { readFileSync } from 'node:fs';
|
|
import { inflateRawSync } from 'node:zlib';
|
|
|
|
export const ABI = 'arm64-v8a';
|
|
export const REQUIRED = ['libreactnative.so', 'libhermes.so', 'libexpo-modules-core.so', 'librnskia.so', 'libreanimated.so'];
|
|
const PAGE = 16384;
|
|
|
|
/** Файлы ZIP по центральному каталогу: имя → содержимое. */
|
|
export function zipEntries(buf) {
|
|
let eocd = buf.length - 22;
|
|
while (eocd >= 0 && buf.readUInt32LE(eocd) !== 0x06054b50) eocd--;
|
|
if (eocd < 0) throw new Error('not a zip file');
|
|
const count = buf.readUInt16LE(eocd + 10);
|
|
let p = buf.readUInt32LE(eocd + 16);
|
|
const out = new Map();
|
|
for (let i = 0; i < count; i++) {
|
|
const method = buf.readUInt16LE(p + 10);
|
|
const size = buf.readUInt32LE(p + 20);
|
|
const nameLen = buf.readUInt16LE(p + 28);
|
|
const extraLen = buf.readUInt16LE(p + 30);
|
|
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, 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;
|
|
}
|
|
|
|
/** Наименьшее выравнивание PT_LOAD-сегментов 64-битной ELF-библиотеки. */
|
|
export function loadAlignment(elf) {
|
|
if (elf.readUInt32BE(0) !== 0x7f454c46 || elf[4] !== 2) throw new Error('not a 64-bit ELF');
|
|
const phoff = Number(elf.readBigUInt64LE(0x20));
|
|
const phentsize = elf.readUInt16LE(0x36);
|
|
const phnum = elf.readUInt16LE(0x38);
|
|
let min = Infinity;
|
|
for (let i = 0; i < phnum; i++) {
|
|
const off = phoff + i * phentsize;
|
|
if (elf.readUInt32LE(off) === 1) min = Math.min(min, Number(elf.readBigUInt64LE(off + 0x30)));
|
|
}
|
|
return min;
|
|
}
|
|
|
|
export function checkApk(buf) {
|
|
const problems = [];
|
|
const entries = zipEntries(buf);
|
|
const libs = [...entries.keys()].filter((n) => n.startsWith('lib/') && n.endsWith('.so'));
|
|
const abis = new Set(libs.map((n) => n.split('/')[1]));
|
|
for (const abi of abis) if (abi !== ABI) problems.push(`лишняя ABI в APK: lib/${abi}/`);
|
|
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 КБ страницы)`);
|
|
}
|
|
return { libs: mine.size, problems };
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('check-apk.mjs')) {
|
|
const file = process.argv[2];
|
|
if (!file) {
|
|
console.error('usage: node scripts/check-apk.mjs <apk>');
|
|
process.exit(2);
|
|
}
|
|
const { libs, problems } = checkApk(readFileSync(file));
|
|
if (problems.length) {
|
|
for (const p of problems) console.error(`✗ ${p}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`APK: ${libs} библиотек ${ABI}, все выровнены по 16 КБ`);
|
|
}
|