@@ -67,15 +67,21 @@ function stopExistingWindowsApiServer() {
|
||||
|
||||
// Windows 下 cargo 重新编译时无法覆盖仍在运行的 exe,只清理本仓库 target 内的旧进程。
|
||||
const command = [
|
||||
'$ErrorActionPreference = "Continue"',
|
||||
'$target = [System.IO.Path]::GetFullPath($env:GENARRATIVE_API_SERVER_EXE_TARGET)',
|
||||
'$processes = Get-Process -Name api-server -ErrorAction SilentlyContinue | Where-Object {',
|
||||
' $_.Path -and ([System.IO.Path]::GetFullPath($_.Path) -ieq $target)',
|
||||
'}',
|
||||
'foreach ($process in $processes) {',
|
||||
' Stop-Process -Id $process.Id -Force',
|
||||
' Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue',
|
||||
' Write-Output $process.Id',
|
||||
' try {',
|
||||
' Stop-Process -Id $process.Id -Force -ErrorAction Stop',
|
||||
' Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue',
|
||||
' Write-Output $process.Id',
|
||||
' } catch {',
|
||||
' Write-Error "[api-server:maincloud] 忽略旧进程清理瞬时失败 pid=$($process.Id): $($_.Exception.Message)"',
|
||||
' }',
|
||||
'}',
|
||||
'exit 0',
|
||||
].join('\n');
|
||||
|
||||
const output = execFileSync(
|
||||
|
||||
@@ -78,7 +78,7 @@ wait_for_spacetime() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if spacetime --root-dir="${root_dir}" server ping "${server}" >/dev/null 2>&1; then
|
||||
if is_spacetime_ready "${server}" "${root_dir}"; then
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -92,8 +92,14 @@ wait_for_spacetime() {
|
||||
is_spacetime_ready() {
|
||||
local server="$1"
|
||||
local root_dir="$2"
|
||||
local output
|
||||
|
||||
spacetime --root-dir="${root_dir}" server ping "${server}" >/dev/null 2>&1
|
||||
if ! output="$(spacetime --root-dir="${root_dir}" server ping "${server}" 2>&1)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# SpacetimeDB CLI 2.1.0 在 502 Bad Gateway 时仍可能返回 0,不能只依赖退出码。
|
||||
[[ "${output}" == *"Server is online:"* ]]
|
||||
}
|
||||
|
||||
describe_spacetime_root_owner() {
|
||||
|
||||
207
scripts/generate-spacetime-bindings.mjs
Normal file
207
scripts/generate-spacetime-bindings.mjs
Normal file
@@ -0,0 +1,207 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user