Skip to content

Commit 780bab8

Browse files
committed
fix: better-sqlite3再構築とWASMフォールバック
1 parent ca7c2d0 commit 780bab8

File tree

9 files changed

+278
-11
lines changed

9 files changed

+278
-11
lines changed

mcp-server/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
2828
"prepare": "cd .. && husky || true",
2929
"prepublishOnly": "npm run test:ci",
30-
"postinstall": "chmod +x bin/unity-mcp-server || true",
30+
"postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server || true",
3131
"test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
3232
"test:unity": "node tests/run-unity-integration.mjs",
3333
"test:nounity": "npm run test:integration",
@@ -50,7 +50,8 @@
5050
"dependencies": {
5151
"@modelcontextprotocol/sdk": "^0.6.1",
5252
"better-sqlite3": "^9.4.3",
53-
"find-up": "^6.3.0"
53+
"find-up": "^6.3.0",
54+
"sql.js": "^1.13.0"
5455
},
5556
"engines": {
5657
"node": ">=18 <23"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env node
2+
// Ensure better-sqlite3 native binding exists; rebuild if missing (covers npx install where prebuilt not shipped)
3+
import { existsSync } from 'fs';
4+
import { resolve } from 'path';
5+
import { spawnSync } from 'child_process';
6+
7+
const bindingPath = resolve(
8+
'node_modules',
9+
'better-sqlite3',
10+
'build',
11+
'Release',
12+
'better_sqlite3.node'
13+
);
14+
15+
function main() {
16+
if (process.env.SKIP_SQLITE_REBUILD) {
17+
console.log('[postinstall] SKIP_SQLITE_REBUILD set, skipping better-sqlite3 check');
18+
return;
19+
}
20+
21+
if (existsSync(bindingPath)) {
22+
return;
23+
}
24+
25+
console.log(
26+
'[postinstall] better-sqlite3 binding missing; rebuilding (npm rebuild better-sqlite3 --build-from-source)'
27+
);
28+
const result = spawnSync('npm', ['rebuild', 'better-sqlite3', '--build-from-source'], {
29+
stdio: 'inherit',
30+
shell: false
31+
});
32+
if (result.status !== 0) {
33+
throw new Error(`better-sqlite3 rebuild failed with code ${result.status ?? 'null'}`);
34+
}
35+
36+
if (!existsSync(bindingPath)) {
37+
throw new Error('better-sqlite3 rebuild completed but binding was still not found');
38+
}
39+
}
40+
41+
try {
42+
main();
43+
} catch (err) {
44+
console.warn(`[postinstall] Warning: ${err.message}`);
45+
// Do not hard fail install; runtime may still use sql.js fallback in codeIndex
46+
}

mcp-server/src/core/codeIndex.js

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { ProjectInfoProvider } from './projectInfo.js';
4+
import { logger } from './config.js';
5+
import { createSqliteFallback } from './sqliteFallback.js';
6+
7+
// Shared driver availability state across CodeIndex instances
8+
const driverStatus = {
9+
available: null,
10+
error: null,
11+
logged: false
12+
};
413

514
export class CodeIndex {
615
constructor(unityConnection) {
@@ -9,21 +18,42 @@ export class CodeIndex {
918
this.db = null;
1019
this.dbPath = null;
1120
this.disabled = false; // set true if better-sqlite3 is unavailable
21+
this.disableReason = null;
1222
this._Database = null;
23+
this._openFallback = null;
24+
this._persistFallback = null;
1325
}
1426

1527
async _ensureDriver() {
16-
if (this.disabled) return false;
28+
if (driverStatus.available === false || this.disabled) {
29+
this.disabled = true;
30+
this.disableReason = this.disableReason || driverStatus.error;
31+
return false;
32+
}
1733
if (this._Database) return true;
1834
try {
1935
// Dynamic import to avoid hard failure when native binding is missing
2036
const mod = await import('better-sqlite3');
2137
this._Database = mod.default || mod;
38+
driverStatus.available = true;
39+
driverStatus.error = null;
2240
return true;
2341
} catch (e) {
24-
// Mark as disabled and operate in fallback (index unavailable)
25-
this.disabled = true;
26-
return false;
42+
// Try wasm fallback (sql.js) before giving up
43+
try {
44+
this._openFallback = createSqliteFallback;
45+
driverStatus.available = true;
46+
driverStatus.error = null;
47+
logger?.info?.('[index] falling back to sql.js (WASM) for code index');
48+
return true;
49+
} catch (fallbackError) {
50+
this.disabled = true;
51+
this.disableReason = `better-sqlite3 unavailable: ${e?.message || e}. Fallback failed: ${fallbackError?.message || fallbackError}`;
52+
driverStatus.available = false;
53+
driverStatus.error = this.disableReason;
54+
this._logDisable(this.disableReason);
55+
return false;
56+
}
2757
}
2858
}
2959

@@ -36,11 +66,35 @@ export class CodeIndex {
3666
fs.mkdirSync(dir, { recursive: true });
3767
const dbPath = path.join(dir, 'code-index.db');
3868
this.dbPath = dbPath;
39-
this.db = new this._Database(dbPath);
69+
try {
70+
if (this._Database) {
71+
this.db = new this._Database(dbPath);
72+
} else if (this._openFallback) {
73+
this.db = await this._openFallback(dbPath);
74+
this._persistFallback = this.db.persist;
75+
} else {
76+
throw new Error('No database driver available');
77+
}
78+
} catch (e) {
79+
this.disabled = true;
80+
this.disableReason = e?.message || 'Failed to open code index database';
81+
driverStatus.available = false;
82+
driverStatus.error = this.disableReason;
83+
this._logDisable(this.disableReason);
84+
return null;
85+
}
4086
this._initSchema();
4187
return this.db;
4288
}
4389

90+
_logDisable(reason) {
91+
if (driverStatus.logged) return;
92+
driverStatus.logged = true;
93+
try {
94+
logger?.warn?.(`[index] code index disabled: ${reason}`);
95+
} catch {}
96+
}
97+
4498
_initSchema() {
4599
if (!this.db) return;
46100
const db = this.db;
@@ -68,6 +122,7 @@ export class CodeIndex {
68122
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
69123
CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
70124
`);
125+
if (this._persistFallback) this._persistFallback();
71126
}
72127

73128
async isReady() {
@@ -103,6 +158,7 @@ export class CodeIndex {
103158
);
104159
});
105160
tx(symbols || []);
161+
await this._flushFallback();
106162
return { total: symbols?.length || 0 };
107163
}
108164

@@ -124,6 +180,7 @@ export class CodeIndex {
124180
sig || '',
125181
new Date().toISOString()
126182
);
183+
await this._flushFallback();
127184
}
128185

129186
async removeFile(pathStr) {
@@ -134,6 +191,7 @@ export class CodeIndex {
134191
db.prepare('DELETE FROM files WHERE path = ?').run(p);
135192
});
136193
tx(pathStr);
194+
await this._flushFallback();
137195
}
138196

139197
async replaceSymbolsForPath(pathStr, rows) {
@@ -160,6 +218,7 @@ export class CodeIndex {
160218
);
161219
});
162220
tx(pathStr, rows || []);
221+
await this._flushFallback();
163222
}
164223

165224
async querySymbols({ name, kind, scope = 'all', exact = false }) {
@@ -209,4 +268,19 @@ export class CodeIndex {
209268
db.prepare("SELECT value AS v FROM meta WHERE key = 'lastIndexedAt'").get()?.v || null;
210269
return { total, lastIndexedAt: last };
211270
}
271+
272+
async _flushFallback() {
273+
if (typeof this._persistFallback === 'function') {
274+
try {
275+
await this._persistFallback();
276+
} catch {}
277+
}
278+
}
279+
}
280+
281+
// Test-only helper to reset cached driver status between runs
282+
export function __resetCodeIndexDriverStatusForTest() {
283+
driverStatus.available = null;
284+
driverStatus.error = null;
285+
driverStatus.logged = false;
212286
}

mcp-server/src/core/indexWatcher.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ export class IndexWatcher {
3434
if (this.running) return;
3535
this.running = true;
3636
try {
37+
// Skip watcher entirely when the native SQLite binding is unavailable
38+
const { CodeIndex } = await import('./codeIndex.js');
39+
const probe = new CodeIndex(this.unityConnection);
40+
const driverOk = await probe._ensureDriver();
41+
if (!driverOk || probe.disabled) {
42+
const reason = probe.disableReason || 'SQLite native binding not available';
43+
logger.warn(`[index] watcher: code index disabled (${reason}); stopping watcher`);
44+
this.stop();
45+
return;
46+
}
47+
3748
// Check if code index DB file exists (before opening DB)
3849
const { ProjectInfoProvider } = await import('./projectInfo.js');
3950
const projectInfo = new ProjectInfoProvider(this.unityConnection);

mcp-server/src/core/server.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,12 @@ export async function startServer() {
219219
const ready = await index.isReady();
220220

221221
if (!ready) {
222+
if (index.disabled) {
223+
logger.warn(
224+
`[startup] Code index disabled: ${index.disableReason || 'SQLite native binding missing'}. Skipping auto-build.`
225+
);
226+
return;
227+
}
222228
logger.info('[startup] Code index DB not ready. Starting auto-build...');
223229
const { CodeIndexBuildToolHandler } = await import(
224230
'../handlers/script/CodeIndexBuildToolHandler.js'
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import initSqlJs from 'sql.js';
4+
5+
// Create a lightweight better-sqlite3 compatible surface using sql.js (WASM)
6+
export async function createSqliteFallback(dbPath) {
7+
const wasmPath = path.resolve('node_modules/sql.js/dist/sql-wasm.wasm');
8+
const SQL = await initSqlJs({ locateFile: () => wasmPath });
9+
10+
const loadDb = () => {
11+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
12+
if (fs.existsSync(dbPath)) {
13+
const data = fs.readFileSync(dbPath);
14+
return new SQL.Database(new Uint8Array(data));
15+
}
16+
return new SQL.Database();
17+
};
18+
19+
const db = loadDb();
20+
21+
const persist = () => {
22+
const data = db.export();
23+
fs.writeFileSync(dbPath, Buffer.from(data));
24+
};
25+
26+
// Wrap sql.js Statement to look like better-sqlite3's
27+
const wrapStatement = stmt => ({
28+
run(...params) {
29+
stmt.bind(params);
30+
// sql.js run via stepping through the statement
31+
while (stmt.step()) {
32+
/* consume rows for statements that return data */
33+
}
34+
stmt.reset();
35+
persist();
36+
return this;
37+
},
38+
get(...params) {
39+
stmt.bind(params);
40+
const has = stmt.step();
41+
const row = has ? stmt.getAsObject() : undefined;
42+
stmt.reset();
43+
return row;
44+
},
45+
all(...params) {
46+
stmt.bind(params);
47+
const rows = [];
48+
while (stmt.step()) rows.push(stmt.getAsObject());
49+
stmt.reset();
50+
return rows;
51+
}
52+
});
53+
54+
const prepare = sql => wrapStatement(db.prepare(sql));
55+
56+
// Mimic better-sqlite3 transaction(fn)
57+
const transaction =
58+
fn =>
59+
(...args) => {
60+
const result = fn(...args);
61+
persist();
62+
return result;
63+
};
64+
65+
// Minimal surface used by CodeIndex
66+
return {
67+
exec: sql => {
68+
db.exec(sql);
69+
persist();
70+
},
71+
prepare,
72+
transaction,
73+
persist
74+
};
75+
}

mcp-server/src/handlers/script/CodeIndexBuildToolHandler.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
7777
*/
7878
async _executeBuild(params, job) {
7979
try {
80+
// Fail fast when native SQLite binding is unavailable
81+
const db = await this.index.open();
82+
if (!db) {
83+
const reason = this.index.disableReason || 'Code index unavailable (SQLite driver missing)';
84+
throw new Error(reason);
85+
}
86+
8087
const throttleMs = Math.max(0, Number(params?.throttleMs ?? 0));
8188
const delayStartMs = Math.max(0, Number(params?.delayStartMs ?? 0));
8289
const info = await this.projectInfo.get();

mcp-server/tests/unit/core/codeIndex.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,34 @@ describe('CodeIndex', () => {
2828
});
2929
});
3030

31+
describe('when native binding is missing', () => {
32+
it('disables gracefully and records the reason', async () => {
33+
const { CodeIndex, __resetCodeIndexDriverStatusForTest } = await import(
34+
'../../../src/core/codeIndex.js'
35+
);
36+
37+
mock.method(CodeIndex.prototype, '_ensureDriver', async function () {
38+
this._Database = class {
39+
constructor() {
40+
throw new Error('Could not locate the bindings file. Tried:');
41+
}
42+
};
43+
return true;
44+
});
45+
46+
const mockConnection = { isConnected: () => false };
47+
const index = new CodeIndex(mockConnection);
48+
const ready = await index.isReady();
49+
50+
assert.equal(ready, false);
51+
assert.equal(index.disabled, true);
52+
assert.match(index.disableReason, /bindings file/i);
53+
54+
mock.restoreAll();
55+
__resetCodeIndexDriverStatusForTest();
56+
});
57+
});
58+
3159
describe('SPEC compliance', () => {
3260
it('should provide code index functionality', async () => {
3361
try {

0 commit comments

Comments
 (0)