Files
Genarrative/scripts/check-module-runtime-artifact.mjs
T
kdletters 7ea463ed08 退役旧创作模板业务并保留数据壳
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义
移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖
恢复现役创作、项目、我的入口及桌面移动导航
收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界
补齐开发栈、网关、原生壳和文档退役约束
2026-07-18 22:02:22 +08:00

368 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { dirname, isAbsolute, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(scriptDir, '..');
const manifestPath = 'server-rs/Cargo.toml';
const targetName = 'module_runtime';
const retiredSymbolSignatures = [
'CreationEntryConfigSnapshot',
'RuntimeBrowseHistorySnapshot',
'RuntimeProfilePlayedWorldSnapshot',
'RuntimeProfileSaveArchiveSnapshot',
'build_runtime_snapshot_record',
'prepare_runtime_browse_history_entries',
'resolve_runtime_profile_save_archive_meta',
];
const retiredStringSignatures = [
'/creation-type-references/puzzle.webp',
'customWorldProfile',
'storyEngineMemory',
];
const requiredAbiSignatures = [
'RuntimeBrowseHistoryThemeMode',
'RuntimeProfileWalletLedgerSourceType',
'RuntimeSettingSnapshot',
];
function cargoDiagnostics(stdout) {
const diagnostics = [];
for (const line of stdout.split(/\r?\n/u)) {
if (!line.trim()) {
continue;
}
try {
const message = JSON.parse(line);
if (message.reason === 'compiler-message' && message.message?.rendered) {
diagnostics.push(message.message.rendered.trimEnd());
}
} catch {
// Cargo may emit a non-JSON line before failing to start rustc.
}
}
return diagnostics;
}
function failBuild(result) {
console.error(
'module-runtime 编译产物门禁失败:无法完成 module-runtime 构建。',
);
for (const diagnostic of cargoDiagnostics(result.stdout ?? '')) {
console.error(diagnostic);
}
if (result.stderr) {
console.error(result.stderr.trimEnd());
}
if (result.error) {
console.error(`- 无法执行 Cargo${result.error.message}`);
}
process.exit(result.status || 1);
}
function collectArtifactPaths(stdout, artifactTargetName = targetName) {
const paths = new Set();
for (const line of stdout.split(/\r?\n/u)) {
if (!line.trim()) {
continue;
}
let message;
try {
message = JSON.parse(line);
} catch {
continue;
}
if (
message.reason !== 'compiler-artifact' ||
message.target?.name !== artifactTargetName ||
!message.target?.kind?.includes('lib')
) {
continue;
}
for (const fileName of message.filenames ?? []) {
if (!fileName.endsWith('.rlib') && !fileName.endsWith('.rmeta')) {
continue;
}
const absolutePath = isAbsolute(fileName)
? fileName
: join(repoRoot, fileName);
if (existsSync(absolutePath)) {
paths.add(absolutePath);
}
}
}
return [...paths];
}
function latestRlib(paths) {
const rlibs = paths.filter((path) => path.endsWith('.rlib'));
return rlibs.sort(
(left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs,
)[0];
}
function parseArchiveObjectMembers(artifact) {
const archiveMagic = artifact.subarray(0, 8).toString('ascii');
if (archiveMagic !== '!<arch>\n') {
throw new Error('产物不是可识别的 Unix rlib 归档');
}
const objectMembers = [];
let longNameTable = null;
let offset = 8;
while (offset + 60 <= artifact.length) {
const header = artifact.subarray(offset, offset + 60);
if (header.subarray(58, 60).toString('ascii') !== '`\n') {
throw new Error(`rlib 成员头损坏,偏移量 ${offset}`);
}
const rawName = header.subarray(0, 16).toString('ascii').trim();
const sizeText = header.subarray(48, 58).toString('ascii').trim();
const size = Number.parseInt(sizeText, 10);
if (!Number.isSafeInteger(size) || size < 0) {
throw new Error(`rlib 成员大小无效:${sizeText || '<empty>'}`);
}
let contentStart = offset + 60;
const contentEnd = contentStart + size;
if (contentEnd > artifact.length) {
throw new Error(`rlib 成员越界,偏移量 ${offset}`);
}
let memberName = rawName.replace(/\/$/u, '');
if (rawName === '//') {
longNameTable = artifact.subarray(contentStart, contentEnd);
} else if (/^\/\d+$/u.test(rawName) && longNameTable) {
const nameOffset = Number.parseInt(rawName.slice(1), 10);
const nameEnd = longNameTable.indexOf(0x0a, nameOffset);
const resolvedEnd = nameEnd >= 0 ? nameEnd : longNameTable.length;
memberName = longNameTable
.subarray(nameOffset, resolvedEnd)
.toString('utf8')
.replace(/\/$/u, '');
} else if (rawName.startsWith('#1/')) {
const nameLength = Number.parseInt(rawName.slice(3), 10);
if (!Number.isSafeInteger(nameLength) || nameLength > size) {
throw new Error(`rlib BSD 扩展成员名长度无效:${rawName}`);
}
memberName = artifact
.subarray(contentStart, contentStart + nameLength)
.toString('utf8');
contentStart += nameLength;
}
if (memberName.endsWith('.o')) {
objectMembers.push(artifact.subarray(contentStart, contentEnd));
}
offset = contentEnd + (size % 2);
}
if (objectMembers.length === 0) {
throw new Error('rlib 中没有可扫描的 Rust object 成员');
}
return objectMembers;
}
console.log('构建 module-runtime 并检查退役业务签名...');
const cargo = process.env.CARGO || 'cargo';
const buildResult = spawnSync(
cargo,
[
'build',
'--manifest-path',
manifestPath,
'--package',
'module-runtime',
'--all-features',
'--message-format=json-render-diagnostics',
'--color=never',
],
{
cwd: repoRoot,
encoding: 'utf8',
env: process.env,
maxBuffer: 256 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
},
);
if (buildResult.error || buildResult.status !== 0) {
failBuild(buildResult);
}
const artifactPaths = collectArtifactPaths(buildResult.stdout);
const artifactPath = latestRlib(artifactPaths);
if (!artifactPath) {
console.error(
'module-runtime 编译产物门禁失败:Cargo JSON 中没有 module_runtime 的 rlib。',
);
console.error(
'- rmeta 会保留被 cfg 禁用的源码 token,无法作为退役业务负向扫描依据;请确认执行的是 cargo build 而不是 cargo check。',
);
process.exit(1);
}
const artifact = readFileSync(artifactPath);
let objectMembers;
try {
objectMembers = parseArchiveObjectMembers(artifact);
} catch (error) {
console.error(`module-runtime 编译产物门禁失败:${artifactPath}`);
console.error(`- 无法读取 rlib object 成员:${error.message}`);
process.exit(1);
}
const retiredSymbolMatches = retiredSymbolSignatures.filter((signature) =>
objectMembers.some((member) => member.includes(Buffer.from(signature))),
);
const retiredStringMatches = retiredStringSignatures.filter((signature) =>
artifact.includes(Buffer.from(signature)),
);
const missingAbiSignatures = requiredAbiSignatures.filter(
(signature) =>
!objectMembers.some((member) => member.includes(Buffer.from(signature))),
);
if (
retiredSymbolMatches.length > 0 ||
retiredStringMatches.length > 0 ||
missingAbiSignatures.length > 0
) {
console.error(`module-runtime 编译产物门禁失败:${artifactPath}`);
for (const signature of retiredSymbolMatches) {
console.error(`- 退役业务符号仍存在:${signature}`);
}
for (const signature of retiredStringMatches) {
console.error(`- 退役业务字符串仍存在:${signature}`);
}
for (const signature of missingAbiSignatures) {
console.error(`- 必须保留的 ABI 签名缺失:${signature}`);
}
process.exit(1);
}
console.log(
`module-runtime 编译产物门禁通过:${retiredSymbolSignatures.length} 个退役符号与 ${retiredStringSignatures.length} 个退役字符串均不存在,${requiredAbiSignatures.length} 个兼容 ABI 签名均存在。`,
);
console.log(`已检查产物:${artifactPath}`);
function checkPlatformRetirementArtifact({
packageName,
artifactTargetName,
symbolSignatures,
stringSignatures,
}) {
const result = spawnSync(
cargo,
[
'build',
'--manifest-path',
manifestPath,
'--package',
packageName,
'--all-features',
'--message-format=json-render-diagnostics',
'--color=never',
],
{
cwd: repoRoot,
encoding: 'utf8',
env: process.env,
maxBuffer: 256 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
},
);
if (result.error || result.status !== 0) {
console.error(`${packageName} 编译产物门禁失败:无法完成构建。`);
for (const diagnostic of cargoDiagnostics(result.stdout ?? '')) {
console.error(diagnostic);
}
if (result.stderr) {
console.error(result.stderr.trimEnd());
}
process.exit(result.status || 1);
}
const paths = collectArtifactPaths(result.stdout, artifactTargetName);
const path = latestRlib(paths);
if (!path) {
console.error(`${packageName} 编译产物门禁失败:Cargo JSON 中没有 rlib。`);
process.exit(1);
}
const bytes = readFileSync(path);
let members;
try {
members = parseArchiveObjectMembers(bytes);
} catch (error) {
console.error(`${packageName} 编译产物门禁失败:${error.message}`);
process.exit(1);
}
const symbolMatches = symbolSignatures.filter((signature) =>
members.some((member) => member.includes(Buffer.from(signature))),
);
const stringMatches = stringSignatures.filter((signature) =>
bytes.includes(Buffer.from(signature)),
);
if (symbolMatches.length > 0 || stringMatches.length > 0) {
console.error(`${packageName} 编译产物门禁失败:${path}`);
for (const signature of symbolMatches) {
console.error(`- 退役业务符号仍存在:${signature}`);
}
for (const signature of stringMatches) {
console.error(`- 退役业务字符串仍存在:${signature}`);
}
process.exit(1);
}
console.log(
`${packageName} 编译产物门禁通过:${symbolSignatures.length} 个退役符号与 ${stringSignatures.length} 个退役字符串均不存在。`,
);
console.log(`已检查产物:${path}`);
}
checkPlatformRetirementArtifact({
packageName: 'platform-auth',
artifactTargetName: 'platform_auth',
symbolSignatures: [
'RuntimeGuestTokenClaims',
'sign_runtime_guest_token',
'verify_runtime_guest_token',
],
stringSignatures: ['runtime:public-play', 'runtime_guest'],
});
checkPlatformRetirementArtifact({
packageName: 'platform-wechat',
artifactTargetName: 'platform_wechat',
symbolSignatures: ['WechatSubscribeMessageRequest', 'send_subscribe_message'],
stringSignatures: [
'/cgi-bin/message/subscribe/send',
'subscribeMessage.send',
],
});