387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
106 lines
3.1 KiB
JavaScript
106 lines
3.1 KiB
JavaScript
import {spawnSync} from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
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');
|