208 lines
5.8 KiB
JavaScript
208 lines
5.8 KiB
JavaScript
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 (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;
|
||
}
|