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(toDir, {recursive: true}); const entries = await readdir(fromDir, {withFileTypes: true}); for (const entry of entries) { await cp(path.join(fromDir, entry.name), path.join(toDir, entry.name), { recursive: true, force: 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 (code === 0) { if (output.includes('Could not format generated files')) { // 中文注释:Windows 下 Rust 绑定文件很多时,SpacetimeDB CLI 可能已生成成功但 rustfmt 启动失败。 // 这里保留后续文件数量校验,避免把格式化警告误判成绑定生成失败。 console.warn(`[spacetime:generate] ${command} 生成后格式化失败,继续校验并同步生成文件。`); } 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; }