Files
Genarrative/scripts/generate-spacetime-bindings.mjs
kdletters f8ea3b704f
Some checks failed
CI / verify (push) Has been cancelled
清理spacetimedb绑定生成
2026-04-26 20:41:11 +08:00

208 lines
5.5 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 {spawn} from 'node:child_process';
import {existsSync} from 'node:fs';
import {cp, mkdir, readdir, rm, stat} from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(SCRIPT_DIR, '..');
const MODULE_PATH = path.join(REPO_ROOT, 'server-rs', 'crates', 'spacetime-module');
const TARGETS = [
{
name: 'Rust',
lang: 'rust',
tempName: 'rs',
outDir: path.join(
REPO_ROOT,
'server-rs',
'crates',
'spacetime-client',
'src',
'module_bindings',
),
},
];
const args = new Set(process.argv.slice(2));
const KNOWN_ARGS = new Set(['--rust-only']);
for (const arg of args) {
if (!KNOWN_ARGS.has(arg)) {
console.error(`[spacetime:generate] 未知参数: ${arg}`);
process.exit(1);
}
}
if (!existsSync(path.join(MODULE_PATH, 'Cargo.toml'))) {
console.error(`[spacetime:generate] 未找到模块: ${MODULE_PATH}`);
process.exit(1);
}
const tempRoot = resolveTempRoot();
assertSafeTempRoot(tempRoot);
const selectedTargets = TARGETS.filter((target) => shouldRunTarget(target.lang));
if (selectedTargets.length === 0) {
console.error('[spacetime:generate] 没有需要生成的目标。');
process.exit(1);
}
await mkdir(tempRoot, {recursive: true});
for (const target of selectedTargets) {
const tempOutDir = path.join(tempRoot, target.tempName);
await recreateTempDir(tempOutDir);
console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`);
await run('spacetime', buildGenerateArgs(target, tempOutDir));
const fileCount = await countFiles(tempOutDir);
if (fileCount === 0) {
throw new Error(`${target.name} bindings 未生成任何文件。`);
}
console.log(`[spacetime:generate] 同步 ${fileCount} 个文件到 ${target.outDir}`);
await replaceGeneratedDir(tempOutDir, target.outDir);
}
await rm(tempRoot, {recursive: true, force: true});
console.log('[spacetime:generate] bindings 生成完成。');
function shouldRunTarget(lang) {
if (args.has('--rust-only')) {
return lang === 'rust';
}
return true;
}
function resolveTempRoot() {
if (process.env.GENARRATIVE_BINDGEN_TEMP_ROOT) {
return path.resolve(process.env.GENARRATIVE_BINDGEN_TEMP_ROOT);
}
// Windows 下 SpacetimeDB CLI 2.1.0 会把所有生成文件路径一次性传给 formatter
// Rust bindings 文件数较多,输出到仓库深目录时容易触发 CreateProcess 路径总长限制。
if (process.platform === 'win32') {
return path.join(path.parse(REPO_ROOT).root, '.genarrative-bindgen');
}
return path.join(REPO_ROOT, 'tmp', 'spacetime-bindgen');
}
async function recreateTempDir(dir) {
assertInside(dir, tempRoot, '临时生成目录');
await rm(dir, {recursive: true, force: true});
await mkdir(dir, {recursive: true});
}
async function replaceGeneratedDir(fromDir, toDir) {
assertInside(toDir, REPO_ROOT, '仓库生成目录');
await rm(toDir, {recursive: true, force: true});
await mkdir(path.dirname(toDir), {recursive: true});
await cp(fromDir, toDir, {recursive: true});
}
function assertInside(candidate, parent, label) {
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error(`${label} 不在预期目录内: ${candidate}`);
}
}
function assertSafeTempRoot(dir) {
const resolved = path.resolve(dir);
const parsed = path.parse(resolved);
const basename = path.basename(resolved).toLowerCase();
if (resolved === path.resolve(REPO_ROOT) || resolved === parsed.root) {
throw new Error(`临时根目录不允许指向仓库或磁盘根目录: ${resolved}`);
}
if (!basename.includes('bindgen')) {
throw new Error(`临时根目录必须是明确的 bindings 生成目录: ${resolved}`);
}
}
function buildGenerateArgs(target, outDir) {
const generateArgs = [
'generate',
'--no-config',
'--lang',
target.lang,
'--out-dir',
outDir,
'--module-path',
MODULE_PATH,
'--yes',
];
return generateArgs;
}
function run(command, commandArgs) {
return new Promise((resolve, reject) => {
const child = spawn(command, commandArgs, {
cwd: REPO_ROOT,
env: process.env,
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
});
let output = '';
child.stdout.on('data', (chunk) => {
const text = chunk.toString();
output += text;
process.stdout.write(text);
});
child.stderr.on('data', (chunk) => {
const text = chunk.toString();
output += text;
process.stderr.write(text);
});
child.on('error', reject);
child.on('exit', (code, signal) => {
if (signal) {
reject(new Error(`${command} 被信号中断: ${signal}`));
return;
}
if (output.includes('Could not format generated files')) {
reject(new Error(`${command} 生成后格式化失败。`));
return;
}
if (code === 0) {
resolve();
return;
}
reject(new Error(`${command} 退出码: ${code ?? 'unknown'}`));
});
});
}
async function countFiles(dir) {
let count = 0;
const entries = await readdir(dir, {withFileTypes: true});
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
count += await countFiles(entryPath);
continue;
}
if (entry.isFile() || (await stat(entryPath)).isFile()) {
count += 1;
}
}
return count;
}