GitNexus/gitnexus/scripts/install-duckdb-extension.mjs
Gergő Magyar 2b111f4488
fix(docker): verify bundled FTS artifacts (#3669)
The live ARM64 CDN artifact was rebuilt with a symbol absent from core
0.21.1. Verify the packaged artifact through the runtime path resolver
instead, retaining a mandatory native LOAD gate without a download.

ARM64 emulation reproduces the CDN failure; the bundled artifact loads,
builds an FTS index, and answers a query. 47 focused tests and both
TypeScript checks pass. Full local suite is still running.

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-21 17:27:30 +00:00

135 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
// Positive on-disk-corruption signatures. `FORCE INSTALL` re-downloads even when
// a file is already present; we only want that when the LOAD error proves the
// existing file is bad (truncated/wrong-platform, #2374). For everything else —
// a missing file (plain INSTALL downloads it), or a permanent non-file failure a
// re-download can never fix (missing runtime dep: "cannot open shared object") —
// plain INSTALL avoids re-downloading ~2 MB on every analyze run forever.
// Exported so a parity test keeps this byte-identical to the copy in
// src/core/lbug/extension-load-error.ts (this `.mjs` cannot import that `.ts`), #2383 F5b.
export const FILE_CORRUPTION_SIGNATURES = [
/invalid elf/i,
/file too short/i,
/not a valid/i,
/bad magic/i,
/wrong architecture/i,
/mach-o/i,
/truncat/i,
];
/**
* Decide the install verb from the LOAD error that triggered this install.
* `FORCE INSTALL` only when the error positively indicates file-level breakage;
* otherwise plain `INSTALL` (missing file, missing-dependency dlopen failure,
* or unknown/absent error).
*/
export function chooseInstallVerb(loadError) {
if (loadError && FILE_CORRUPTION_SIGNATURES.some((re) => re.test(loadError))) {
return 'FORCE INSTALL';
}
return 'INSTALL';
}
function parseLbugMaxDbSize(raw) {
const parsed = raw ? Number(raw) : NaN;
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`Invalid LadybugDB max DB size for extension installer: ${raw ?? '<missing>'}`);
}
return Math.floor(parsed);
}
function resolveMaxDbSize() {
// argv[3] is the optional positional size; ignore it when it is actually a
// flag token (e.g. `--verify-only`) and fall back to the env default.
const sizeArg =
process.argv[3] && !process.argv[3].startsWith('--') ? process.argv[3] : undefined;
return parseLbugMaxDbSize(sizeArg ?? process.env.GITNEXUS_LBUG_MAX_DB_SIZE);
}
/** Open a scratch LadybugDB and return its connection plus a disposer. */
async function defaultConnect(lbugMaxDbSize) {
const require = createRequire(import.meta.url);
const lbugModule = require('@ladybugdb/core');
const lbug = lbugModule.default ?? lbugModule;
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-ext-install-'));
const dbPath = path.join(tmpDir, 'install.lbug');
const db = new lbug.Database(dbPath, 0, false, false, lbugMaxDbSize);
const conn = new lbug.Connection(db);
return {
conn,
dispose: async () => {
await conn.close().catch(() => {});
await db.close().catch(() => {});
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
},
};
}
/**
* Install (or verify) an optional LadybugDB extension in this short-lived process.
*
* @param {string} extensionName
* @param {object} [options]
* @param {boolean} [options.verifyOnly] LOAD-only Docker build gate — no install.
* @param {boolean} [options.verifyBundled] LOAD the packaged FTS artifact without a download.
* @param {string} [options.loadError] The parent's LOAD failure; selects the verb.
* @param {(size: number) => Promise<{conn: {query: (sql: string) => Promise<unknown>}, dispose: () => Promise<void>}>} [options.connect]
* Connection factory; injectable for offline unit tests.
*/
export async function installDuckDbExtension(extensionName, options = {}) {
const { verifyOnly = false, verifyBundled = false, loadError, connect } = options;
if (!extensionName || !EXTENSION_NAME_PATTERN.test(extensionName)) {
throw new Error(`Invalid DuckDB extension name: ${extensionName ?? '<missing>'}`);
}
let loadTarget = extensionName;
if (verifyBundled) {
if (extensionName !== 'fts') throw new Error('Bundled verification only supports fts');
const { resolveVendoredFtsPath } = await import('../dist/core/lbug/vendored-extension-path.js');
const bundled = resolveVendoredFtsPath();
if (!bundled)
throw new Error(`No packaged FTS artifact for ${process.platform}-${process.arch}`);
loadTarget = `'${bundled.replace(/\\/g, '/').replace(/'/g, "''")}'`;
}
const makeConnection = connect ?? (() => defaultConnect(resolveMaxDbSize()));
const { conn, dispose } = await makeConnection();
try {
if (verifyOnly || verifyBundled) {
// LOAD must succeed without INSTALL or a network fallback, whether the
// caller verifies the packaged path or a named extension under HOME.
await conn.query(`LOAD EXTENSION ${loadTarget}`);
console.log(
`[install-ext] LOAD-only verify OK for '${extensionName}' (HOME=${process.env.HOME})`,
);
} else {
// Plain INSTALL is a no-op when the file already exists; escalate to FORCE
// only when the LOAD error proves the on-disk file is broken (#2374).
await conn.query(`${chooseInstallVerb(loadError)} ${extensionName}`);
}
} finally {
await dispose();
}
}
// Only run when executed directly — imported (e.g. by unit tests) it stays inert.
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
installDuckDbExtension(process.argv[2] ?? process.env.GITNEXUS_LBUG_EXTENSION_NAME, {
verifyOnly: process.argv.includes('--verify-only'),
verifyBundled: process.argv.includes('--verify-bundled'),
loadError: process.env.GITNEXUS_LBUG_EXTENSION_LOAD_ERROR,
}).catch((err) => {
console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
process.exitCode = 1;
});
}