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 !== '!\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 || ''}`); } 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', ], });