768546fdc7
新增移动壳 APK 与 iOS simulator 包产物检查脚本 新增根级 mobile-shell:build-artifacts 验收入口 移动壳配置门禁反查产物验收脚本与真实包内容检查 同步宿主壳方案文档和项目决策记录
106 lines
3.1 KiB
JavaScript
106 lines
3.1 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {spawnSync} from 'node:child_process';
|
|
|
|
const shellRoot = new URL('../', import.meta.url);
|
|
const repoRoot = path.resolve(shellRoot.pathname, '../..');
|
|
const androidArtifactPath = path.join(
|
|
repoRoot,
|
|
'build',
|
|
'native',
|
|
'mobile',
|
|
'genarrative-mobile-android.apk',
|
|
);
|
|
const iosSimulatorArtifactPath = path.join(
|
|
repoRoot,
|
|
'build',
|
|
'native',
|
|
'mobile',
|
|
'genarrative-mobile-ios-simulator.tar.gz',
|
|
);
|
|
|
|
function assertFile(filePath, label, minimumBytes) {
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`${label} is missing: ${filePath}`);
|
|
}
|
|
|
|
const stat = fs.statSync(filePath);
|
|
if (!stat.isFile() || stat.size < minimumBytes) {
|
|
throw new Error(`${label} must be a real build artifact`);
|
|
}
|
|
}
|
|
|
|
function readHeader(filePath, length) {
|
|
const handle = fs.openSync(filePath, 'r');
|
|
try {
|
|
const header = Buffer.alloc(length);
|
|
fs.readSync(handle, header, 0, header.length, 0);
|
|
return header;
|
|
} finally {
|
|
fs.closeSync(handle);
|
|
}
|
|
}
|
|
|
|
function assertZipHeader(filePath, label) {
|
|
const header = readHeader(filePath, 4);
|
|
if (
|
|
header[0] !== 0x50 ||
|
|
header[1] !== 0x4b ||
|
|
header[2] !== 0x03 ||
|
|
header[3] !== 0x04
|
|
) {
|
|
throw new Error(`${label} must be a ZIP based artifact`);
|
|
}
|
|
}
|
|
|
|
function assertGzipHeader(filePath, label) {
|
|
const header = readHeader(filePath, 2);
|
|
if (header[0] !== 0x1f || header[1] !== 0x8b) {
|
|
throw new Error(`${label} must be a gzip compressed tar artifact`);
|
|
}
|
|
}
|
|
|
|
function run(command, args, label) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
});
|
|
if (result.error) {
|
|
throw new Error(`${label} failed to start: ${result.error.message}`);
|
|
}
|
|
if ((result.status ?? 0) !== 0) {
|
|
throw new Error(`${label} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
}
|
|
return result.stdout;
|
|
}
|
|
|
|
function assertIncludes(source, snippet, label) {
|
|
if (!source.includes(snippet)) {
|
|
throw new Error(`${label} missing ${snippet}`);
|
|
}
|
|
}
|
|
|
|
assertFile(androidArtifactPath, 'Android mobile shell APK', 1024 * 1024);
|
|
assertZipHeader(androidArtifactPath, 'Android mobile shell APK');
|
|
const androidListing = run('unzip', ['-l', androidArtifactPath], 'Android APK listing');
|
|
assertIncludes(androidListing, 'AndroidManifest.xml', 'Android mobile shell APK');
|
|
assertIncludes(androidListing, 'classes.dex', 'Android mobile shell APK');
|
|
assertIncludes(androidListing, 'assets/index.android.bundle', 'Android mobile shell APK');
|
|
|
|
assertFile(iosSimulatorArtifactPath, 'iOS simulator mobile shell archive', 1024 * 1024);
|
|
assertGzipHeader(iosSimulatorArtifactPath, 'iOS simulator mobile shell archive');
|
|
const iosListing = run(
|
|
'tar',
|
|
['-tzf', iosSimulatorArtifactPath],
|
|
'iOS simulator archive listing',
|
|
);
|
|
assertIncludes(iosListing, '.app/Info.plist', 'iOS simulator mobile shell archive');
|
|
assertIncludes(iosListing, '.app/Genarrative', 'iOS simulator mobile shell archive');
|
|
assertIncludes(
|
|
iosListing,
|
|
'.app/main.jsbundle',
|
|
'iOS simulator mobile shell archive',
|
|
);
|
|
|
|
console.log('[mobile-shell:build-artifacts] OK');
|