Files
Genarrative/scripts/check-module-runtime-artifact.mjs
T
lhk229 8f19964e3f
Project CI / Repository checks (push) Successful in 1m0s
Project CI / Backend tests (push) Successful in 3m52s
Project CI / Native shell tests (push) Successful in 13m8s
Project CI / Frontend tests (push) Successful in 2m43s
加入手动完美像素入口;像素艺术勾选时自动补提示词约束 (#124)
Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/124
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-08-05 21:28:40 +08:00

376 lines
11 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);
// GNU archives use newline-terminated names; MSVC COFF archives use NUL.
const candidateNameEnds = [
longNameTable.indexOf(0x00, nameOffset),
longNameTable.indexOf(0x0a, nameOffset),
].filter((nameEnd) => nameEnd >= 0);
const resolvedEnd =
candidateNameEnds.length > 0
? Math.min(...candidateNameEnds)
: 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')
.replace(/\0+$/u, '');
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',
],
});