diff --git a/cli/src/internal/maintenance/upgradePrestart.cjs b/cli/src/internal/maintenance/upgradePrestart.cjs index effbafc0..d865792a 100644 --- a/cli/src/internal/maintenance/upgradePrestart.cjs +++ b/cli/src/internal/maintenance/upgradePrestart.cjs @@ -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 }) { diff --git a/test/back/migration-sqlite-armv6.test.cjs b/test/back/migration-sqlite-armv6.test.cjs index a82f1aeb..3d033c80 100644 --- a/test/back/migration-sqlite-armv6.test.cjs +++ b/test/back/migration-sqlite-armv6.test.cjs @@ -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' } } }; } diff --git a/test/back/upgrade-prestart.test.cjs b/test/back/upgrade-prestart.test.cjs index 5d6bb6db..d242f887 100644 --- a/test/back/upgrade-prestart.test.cjs +++ b/test/back/upgrade-prestart.test.cjs @@ -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'" },