7ea463ed08
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义 移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖 恢复现役创作、项目、我的入口及桌面移动导航 收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界 补齐开发栈、网关、原生壳和文档退役约束
368 lines
10 KiB
JavaScript
368 lines
10 KiB
JavaScript
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',
|
||
],
|
||
});
|