fix: preserve legacy SQLite addons loaded from build directories

This commit is contained in:
whyour
2026-10-23 14:33:13 +08:00
parent 5fdfd9077d
commit c9f1b2d4bb
3 changed files with 39 additions and 8 deletions
@@ -211,15 +211,25 @@ async function prestartBackend(
}
async function cleanSqliteBuildArtifacts(directory) {
const names = await fs.readdir(directory).catch((error) => {
const entries = await fs.readdir(directory, { withFileTypes: true }).catch((error) => {
if (error.code !== 'ENOENT') throw error;
return [];
});
// node-pre-gyp's runtime copy lives in lib/binding. Its build directories
// retain compiler objects and an absolute node_gyp_bins/python3 symlink.
for (const name of names)
if (name === 'build' || /^build-tmp-napi-v\d+$/.test(name))
await fs.rm(path.join(directory, name), { recursive: true, force: true });
if (!entries.length) return;
const pkg = JSON.parse(await fs.readFile(path.join(directory, 'package.json'), 'utf8'));
const runtimePath = path.posix.normalize(pkg.binary?.module_path || '.');
const copiedRuntime = runtimePath.startsWith('lib/binding/');
// Our legacy fork copies its runtime to lib/binding. Other sqlite3 versions
// load build/Release directly: retain that addon and remove only the compiler
// helper directory. Never traverse a build-directory symlink while cleaning.
for (const entry of entries) {
if (entry.name !== 'build' && !/^build-tmp-napi-v\d+$/.test(entry.name)) continue;
const build = path.join(directory, entry.name);
await fs.rm(
copiedRuntime || entry.isSymbolicLink() ? build : path.join(build, 'node_gyp_bins'),
{ recursive: true, force: true },
);
}
}
async function repairDependencies(source, { target, signal, env }) {
+1 -1
View File
@@ -37,7 +37,7 @@ async function fixture(t) {
await fs.mkdir(path.join(root, name), { recursive: true });
await fs.writeFile(root + '/package.json', JSON.stringify({ qinglongMigrationBridge: { format: 1 }, dependencies: { sqlite3: 'legacy' } }));
await fs.writeFile(root + '/pnpm-lock.yaml', 'fixed legacy lock');
await fs.writeFile(root + '/node_modules/sqlite3/package.json', '{}');
await fs.writeFile(root + '/node_modules/sqlite3/package.json', JSON.stringify({ binary: { module_path: './lib/binding/napi-v{napi_build_version}-{platform}-{arch}' } }));
await fs.writeFile(root + '/runtime/include/node/node_version.h', '#define NODE_MAJOR_VERSION 24\n#define NODE_MINOR_VERSION 18\n#define NODE_PATCH_VERSION 1\n');
return { root, runtime: { execPath: root + '/runtime/bin/node', versions: { node: '24.18.1' }, env: { PATH: '/qa/bin', NODE_OPTIONS: '--require=untrusted', CFLAGS: '-march=armv7' } } };
}
+22 -1
View File
@@ -8,6 +8,7 @@ const {
dependencyFailure,
probeLimits,
repairDependencies,
cleanSqliteBuildArtifacts,
} = require('../../cli/src/internal/maintenance/upgradePrestart.cjs');
for (const exitCode of [0, 1])
@@ -291,7 +292,7 @@ test('successful legacy repair publishes only portable runtime files from a pnpm
const pkg = 'node_modules/.pnpm/@whyour+sqlite3@1.1.2/node_modules/@whyour/sqlite3';
if (process.argv[2] === 'install') {
fs.mkdirSync(pkg + '/lib/binding/napi-v6-linux-arm', {recursive:true});
fs.writeFileSync(pkg + '/package.json', '{}');
fs.writeFileSync(pkg + '/package.json', JSON.stringify({binary:{module_path:'./lib/binding/napi-v{napi_build_version}-{platform}-{arch}'}}));
fs.symlinkSync('.pnpm/@whyour+sqlite3@1.1.2/node_modules/@whyour/sqlite3', 'node_modules/sqlite3');
} else if (process.argv[2] === 'rebuild') {
fs.writeFileSync(pkg + '/lib/binding/napi-v6-linux-arm/node_sqlite3.node', 'compiled addon');
@@ -331,6 +332,26 @@ test('successful legacy repair publishes only portable runtime files from a pnpm
await inventory({ source });
});
test('cleanup retains sqlite3 runtimes loaded from build/Release and never follows a build symlink', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-sqlite-release-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const sqlite = path.join(root, 'sqlite3'), outside = path.join(root, 'outside');
await fs.mkdir(path.join(sqlite, 'build/Release'), { recursive: true });
await fs.mkdir(path.join(sqlite, 'build/node_gyp_bins'));
await fs.mkdir(path.join(outside, 'node_gyp_bins'), { recursive: true });
await fs.writeFile(path.join(outside, 'node_gyp_bins/keep'), 'untouched');
await fs.writeFile(path.join(sqlite, 'package.json'), JSON.stringify({ name: 'sqlite3', binary: { napi_versions: [3, 6] } }));
await fs.writeFile(path.join(sqlite, 'build/Release/node_sqlite3.node'), 'runtime addon');
await fs.symlink('/usr/bin/python3', path.join(sqlite, 'build/node_gyp_bins/python3'));
await fs.symlink(outside, path.join(sqlite, 'build-tmp-napi-v3'));
await cleanSqliteBuildArtifacts(sqlite);
assert.equal(await fs.readFile(path.join(sqlite, 'build/Release/node_sqlite3.node'), 'utf8'), 'runtime addon');
await assert.rejects(fs.access(path.join(sqlite, 'build/node_gyp_bins')), { code: 'ENOENT' });
await assert.rejects(fs.lstat(path.join(sqlite, 'build-tmp-napi-v3')), { code: 'ENOENT' });
assert.equal(await fs.readFile(path.join(outside, 'node_gyp_bins/keep'), 'utf8'), 'untouched');
await require('../../cli/src/internal/maintenance/platformBundle.cjs').inventory({ source: sqlite });
});
test('repair classification excludes application, platform, config, database, OOM and timeouts', () => {
for (const error of [
{ code: 'MODULE_NOT_FOUND', message: "Cannot find module './api/missing'" },