From c23588b610b751678514e1b59a13ee4458e34dd6 Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Tue, 18 Aug 2026 19:31:10 +0600 Subject: [PATCH 1/6] test(prebuild): probe that a published better-sqlite3 asset loads and serves FTS5 A require() that returns an object only proves a file resolved. The probe downloads the release asset for a given target, installs the JS wrapper with --ignore-scripts so no locally built binding can stand in for it, then builds an FTS5 index and asserts a MATCH returns exactly the one row that matches and nothing for a term that matches none. --expect-fail inverts the exit code over the load only: the asset must still download and extract, so a control cannot pass by 404ing on a mistyped target. The version comes from package-lock.json, never a literal, so a pin bump cannot leave the probe verifying the previous release. --- scripts/verify-better-sqlite3-prebuild.mjs | 205 +++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 scripts/verify-better-sqlite3-prebuild.mjs diff --git a/scripts/verify-better-sqlite3-prebuild.mjs b/scripts/verify-better-sqlite3-prebuild.mjs new file mode 100644 index 000000000..a41614076 --- /dev/null +++ b/scripts/verify-better-sqlite3-prebuild.mjs @@ -0,0 +1,205 @@ +#!/usr/bin/env node +/** + * Verify that a PUBLISHED better-sqlite3 prebuild asset genuinely loads, and that the + * native surface it exposes actually works. + * + * Loading is not the bar. A `require()` that returns an object only proves a file + * resolved; it does not prove the extension's SQLite build carries FTS5, which is the + * one compile-time option wigolo's cache cannot run without. So every positive run + * builds an FTS5 index and drives a `MATCH` through it, and asserts a non-matching + * query returns nothing — a MATCH that returns every row is not a MATCH. + * + * The asset is downloaded from the release, not taken from `node_modules`: the point is + * to verify the artifact users receive, on the platform they receive it for. The JS + * wrapper is installed with `--ignore-scripts` so nothing can quietly compile a fresh + * binding and verify itself. + * + * Usage: + * node scripts/verify-better-sqlite3-prebuild.mjs # host target + * node scripts/verify-better-sqlite3-prebuild.mjs --target win32-arm64 + * node scripts/verify-better-sqlite3-prebuild.mjs --target linux-x64 --expect-fail + * node scripts/verify-better-sqlite3-prebuild.mjs --abi 115 --expect-fail + * node scripts/verify-better-sqlite3-prebuild.mjs --missing-binding --expect-fail + * + * `--expect-fail` inverts the exit code, but only over the LOAD. Download and extract + * must still succeed: a control that "passes" because the asset 404'd would prove the + * URL was wrong, not that the binding was rejected. + */ + +import { createRequire } from 'node:module'; +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +function parseArgs(argv) { + const opts = { target: null, abi: null, expectFail: false, missingBinding: false }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--expect-fail') opts.expectFail = true; + else if (arg === '--missing-binding') opts.missingBinding = true; + else if (arg === '--target') opts.target = argv[++i]; + else if (arg === '--abi') opts.abi = argv[++i]; + else if (arg.startsWith('--target=')) opts.target = arg.slice('--target='.length); + else if (arg.startsWith('--abi=')) opts.abi = arg.slice('--abi='.length); + else throw new Error(`unknown argument: ${arg}`); + } + return opts; +} + +/** The pin is the lockfile's, never a literal here — a version bump must not silently + * leave this probe verifying the previous release. */ +function lockedVersion() { + const lockPath = path.join(REPO_ROOT, 'package-lock.json'); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.['node_modules/better-sqlite3']; + if (!entry?.version) { + throw new Error(`no "node_modules/better-sqlite3" entry with a version in ${lockPath}`); + } + return entry.version; +} + +function assetName(version, abi, target) { + return `better-sqlite3-v${version}-node-v${abi}-${target}.tar.gz`; +} + +async function download(url, dest) { + const res = await fetch(url, { redirect: 'follow' }); + if (!res.ok) throw new Error(`GET ${url} -> HTTP ${res.status} ${res.statusText}`); + const bytes = Buffer.from(await res.arrayBuffer()); + fs.writeFileSync(dest, bytes); + return bytes.length; +} + +/** The wrapper only — `--ignore-scripts` keeps node-gyp and prebuild-install out, so the + * binding under test is the downloaded one and nothing else. */ +function installWrapper(version, dir) { + const marker = path.join(dir, 'node_modules', 'better-sqlite3', 'package.json'); + if (fs.existsSync(marker)) return path.dirname(marker); + fs.mkdirSync(dir, { recursive: true }); + execFileSync( + process.platform === 'win32' ? 'npm.cmd' : 'npm', + [ + 'install', + `better-sqlite3@${version}`, + '--ignore-scripts', + '--no-audit', + '--no-fund', + '--no-package-lock', + '--prefix', + dir, + ], + { stdio: 'inherit', cwd: dir } + ); + if (!fs.existsSync(marker)) throw new Error(`wrapper install produced no ${marker}`); + return path.dirname(marker); +} + +function driveFts5(Database, bindingPath) { + const db = new Database(':memory:', { nativeBinding: bindingPath }); + try { + const sqliteVersion = db.prepare('SELECT sqlite_version() AS v').get().v; + db.exec('CREATE VIRTUAL TABLE docs USING fts5(title, body)'); + const insert = db.prepare('INSERT INTO docs (title, body) VALUES (?, ?)'); + insert.run('prebuild-under-test', 'the published asset carries a working fts5 module'); + insert.run('unrelated-row', 'nothing here should answer the query below'); + insert.run('second-unrelated-row', 'nor should this one'); + + const hits = db + .prepare('SELECT title FROM docs WHERE docs MATCH ? ORDER BY rank') + .all('fts5'); + const titles = hits.map((r) => r.title); + if (titles.length !== 1 || titles[0] !== 'prebuild-under-test') { + throw new Error(`MATCH returned ${JSON.stringify(titles)}, expected ["prebuild-under-test"]`); + } + + // A MATCH that answers everything is indistinguishable from a table scan. + const misses = db.prepare('SELECT title FROM docs WHERE docs MATCH ?').all('nonexistentterm'); + if (misses.length !== 0) { + throw new Error(`non-matching MATCH returned ${misses.length} rows, expected 0`); + } + return { sqliteVersion, titles }; + } finally { + db.close(); + } +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + const version = lockedVersion(); + const abi = opts.abi ?? process.versions.modules; + const target = opts.target ?? `${process.platform}-${process.arch}`; + const hostTarget = `${process.platform}-${process.arch}`; + + const work = path.join(os.tmpdir(), `bs3-prebuild-probe-${abi}-${target}`); + fs.rmSync(work, { recursive: true, force: true }); + fs.mkdirSync(work, { recursive: true }); + + const wrapperDir = installWrapper(version, path.join(os.tmpdir(), 'bs3-prebuild-wrapper')); + + console.log(`better-sqlite3 version : ${version} (from package-lock.json)`); + console.log(`host : node ${process.version} / ${hostTarget} / ABI ${process.versions.modules}`); + console.log(`target under test : ${target} / ABI ${abi}`); + console.log(`wrapper (--ignore-scripts): ${wrapperDir}`); + + let bindingPath; + if (opts.missingBinding) { + bindingPath = path.join(work, 'build', 'Release', 'better_sqlite3.node'); + console.log(`binding : ${bindingPath} (deliberately absent)`); + if (fs.existsSync(bindingPath)) throw new Error('the "missing" binding exists — control is void'); + } else { + const name = assetName(version, abi, target); + const url = `https://github.com/WiseLibs/better-sqlite3/releases/download/v${version}/${name}`; + const tarball = path.join(work, name); + const bytes = await download(url, tarball); + execFileSync('tar', ['-xzf', tarball, '-C', work], { stdio: 'inherit' }); + bindingPath = path.join(work, 'build', 'Release', 'better_sqlite3.node'); + if (!fs.existsSync(bindingPath)) { + throw new Error(`${name} extracted without build/Release/better_sqlite3.node`); + } + const sha = createHash('sha256').update(fs.readFileSync(bindingPath)).digest('hex'); + console.log(`asset : ${name} (${bytes} bytes)`); + console.log(`asset url : ${url}`); + console.log(`binding : ${bindingPath}`); + console.log(`binding sha256 : ${sha}`); + } + + const require = createRequire(import.meta.url); + const Database = require(wrapperDir); + + let result = null; + let failure = null; + try { + result = driveFts5(Database, bindingPath); + } catch (err) { + failure = err; + } + + if (opts.expectFail) { + if (failure) { + console.log(`\nNEGATIVE CONTROL HELD — ${target} / ABI ${abi} was REJECTED on ${hostTarget}`); + console.log(` rejection: ${String(failure.message).split('\n')[0]}`); + return; + } + console.error( + `\nNEGATIVE CONTROL FAILED — ${target} / ABI ${abi} LOADED and ran FTS5 on ${hostTarget}.` + + ' A control that cannot fail proves nothing about the positive runs beside it.' + ); + process.exitCode = 1; + return; + } + + if (failure) throw failure; + console.log(`sqlite : ${result.sqliteVersion}`); + console.log(`fts5 MATCH : ${JSON.stringify(result.titles)}`); + console.log(`\nVERIFIED — ${target} prebuild loaded and served an FTS5 MATCH on ${hostTarget}`); +} + +main().catch((err) => { + console.error(`\nFAILED — ${err?.stack ?? err}`); + process.exitCode = 1; +}); From 5f4acb8a770af73c5bdba80e6030438af8480766 Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Tue, 18 Aug 2026 19:31:22 +0600 Subject: [PATCH 2/6] ci: verify the two win32 better-sqlite3 prebuilds on real Windows kernels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight of the ten Node-22 (ABI 127) prebuild targets for the pinned 12.9.0 were verified from the published release asset. win32-x64 and win32-arm64 were not: the build machine had no Windows kernel, so only the binaries' PE shape was confirmed. This runs the same probe on windows-latest and on the GitHub-hosted windows-11-arm runner, which is free and generally available for public repos. Five negative controls per leg — wrong arch, wrong platform, wrong libc, wrong ABI, absent binding — each of which must be rejected, because a probe that cannot fail says nothing about the binding it loaded. No continue-on-error: a job that reports success regardless of its steps is the vacuous green this phase removes. No npm ci either, which keeps the job clear of the v13 install-script problem and stops prebuild-install from supplying the binding under test. --- .github/workflows/ci.yml | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a2a6d375..85aeebd30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,6 +301,78 @@ jobs: RUN_STUDIO_E2E: '1' + # better-sqlite3 publishes 10 Node-22 (ABI 127) prebuild targets for the pinned 12.9.0. + # Eight of them — darwin-arm64/x64, linux-x64/arm64/arm, linuxmusl-x64/arm64/arm — were + # verified on the build machine by loading the PUBLISHED release asset and driving it + # through an FTS5 MATCH. The two win32 targets could not be: there was no Windows kernel + # there, and the previous probe confirmed only the binaries' PE shape rather than + # synthesise a pass. This job closes that gap on real Windows kernels, at the same + # standard — a `require()` that returns an object only proves a file resolved, so each + # run builds an FTS5 index and queries it. + # + # windows-11-arm is a GitHub-hosted arm64 runner, free and generally available for PUBLIC + # repositories since 2025-08-07 (it is not a self-hosted label and needs no setup). This + # repository is public. On a private fork the label does not resolve and this leg will not + # start — that is a visibility fact about the fork, not a workflow bug. + # + # Deliberately no `npm ci`: the probe installs the JS wrapper alone with + # `--ignore-scripts`, so prebuild-install and node-gyp cannot supply a locally built + # binding and let the probe verify itself. That also keeps this job independent of the + # better-sqlite3 v13 problem parked in PR #337 (v13 dropped its `install` script, so npm + # supplies an implicit `node-gyp rebuild` and `npm ci` hard-fails on a Windows box with no + # Visual Studio). The pin stays 12.9.0 and the probe reads it from package-lock.json. + # + # No `continue-on-error` anywhere in this job. The whole point of the Q6 phase is deleting + # greens that report success regardless of what their steps did. + win32-prebuild: + name: better-sqlite3 prebuild loads (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - runner: windows-latest + target: win32-x64 + wrong_arch: win32-arm64 + - runner: windows-11-arm + target: win32-arm64 + wrong_arch: win32-x64 + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: 22 + + - name: Published ${{ matrix.target }} prebuild loads + serves an FTS5 MATCH + shell: bash + run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.target }} + + # Five controls, each of which MUST be rejected. Without them a green above is + # unfalsifiable: a probe that cannot fail says nothing about the binding it loaded. + # `--expect-fail` inverts only the LOAD — the asset must still download and extract, + # so a control cannot "pass" by 404ing on a mistyped target. + - name: Negative control — wrong arch (${{ matrix.wrong_arch }}) + shell: bash + run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.wrong_arch }} --expect-fail + + - name: Negative control — wrong platform (linux-x64 ELF) + shell: bash + run: node scripts/verify-better-sqlite3-prebuild.mjs --target linux-x64 --expect-fail + + - name: Negative control — wrong libc (linuxmusl-x64) + shell: bash + run: node scripts/verify-better-sqlite3-prebuild.mjs --target linuxmusl-x64 --expect-fail + + - name: Negative control — wrong ABI (Node 18, v115) + shell: bash + run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.target }} --abi 115 --expect-fail + + - name: Negative control — binding absent + shell: bash + run: node scripts/verify-better-sqlite3-prebuild.mjs --missing-binding --expect-fail + # Clean-machine smoke on every desktop OS: a fresh global install, a real # `init`, then tool calls that LOAD and EXERCISE every dependency subsystem — # native SQLite, browser engine, ML reranker, semantic embeddings, and (when a From cd709523f579d0f2911f8f81774dc39ee62015c1 Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Tue, 18 Aug 2026 19:35:21 +0600 Subject: [PATCH 3/6] fix(prebuild): unpack the wrapper from the registry tarball, not via npm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both win32 legs failed identically on the first CI run with 'spawnSync npm.cmd EINVAL' — the CVE-2024-27980 hardening, which refuses to spawn a .cmd without shell: true. Same trap the init e2e test hit. Rather than reach for shell: true, the probe now downloads the tarball the lockfile resolves to, verifies it against the lockfile's integrity hash, and untars it. No install lifecycle exists at all, so the earlier --ignore-scripts guarantee is now structural: there is no script to ignore. better-sqlite3's wrapper requires 'bindings' lazily and only when nativeBinding is null, and this probe always passes a path, so the unpacked wrapper needs no dependency tree. Confirms windows-11-arm resolved and allocated: the arm64 leg ran on image windows-11-arm64 with Node 22.23.2/arm64 and reached the same npm failure. --- .github/workflows/ci.yml | 14 +++-- scripts/verify-better-sqlite3-prebuild.mjs | 70 ++++++++++++---------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85aeebd30..c5f8dd25d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -315,12 +315,14 @@ jobs: # repository is public. On a private fork the label does not resolve and this leg will not # start — that is a visibility fact about the fork, not a workflow bug. # - # Deliberately no `npm ci`: the probe installs the JS wrapper alone with - # `--ignore-scripts`, so prebuild-install and node-gyp cannot supply a locally built - # binding and let the probe verify itself. That also keeps this job independent of the - # better-sqlite3 v13 problem parked in PR #337 (v13 dropped its `install` script, so npm - # supplies an implicit `node-gyp rebuild` and `npm ci` hard-fails on a Windows box with no - # Visual Studio). The pin stays 12.9.0 and the probe reads it from package-lock.json. + # Deliberately no `npm ci`, and no npm at all: the probe unpacks the JS wrapper straight + # from the registry tarball the lockfile resolves to, checked against the lockfile's + # integrity hash. No install lifecycle runs, so prebuild-install and node-gyp cannot + # supply a locally built binding and let the probe verify itself. That also keeps this job + # independent of the better-sqlite3 v13 problem parked in PR #337 (v13 dropped its + # `install` script, so npm supplies an implicit `node-gyp rebuild` and `npm ci` hard-fails + # on a Windows box with no Visual Studio). The pin stays 12.9.0 and the probe reads it + # from package-lock.json. # # No `continue-on-error` anywhere in this job. The whole point of the Q6 phase is deleting # greens that report success regardless of what their steps did. diff --git a/scripts/verify-better-sqlite3-prebuild.mjs b/scripts/verify-better-sqlite3-prebuild.mjs index a41614076..c1c9b6749 100644 --- a/scripts/verify-better-sqlite3-prebuild.mjs +++ b/scripts/verify-better-sqlite3-prebuild.mjs @@ -11,8 +11,8 @@ * * The asset is downloaded from the release, not taken from `node_modules`: the point is * to verify the artifact users receive, on the platform they receive it for. The JS - * wrapper is installed with `--ignore-scripts` so nothing can quietly compile a fresh - * binding and verify itself. + * wrapper is unpacked straight from the registry tarball, so no install lifecycle runs and + * nothing can quietly compile a fresh binding and verify itself. * * Usage: * node scripts/verify-better-sqlite3-prebuild.mjs # host target @@ -53,14 +53,14 @@ function parseArgs(argv) { /** The pin is the lockfile's, never a literal here — a version bump must not silently * leave this probe verifying the previous release. */ -function lockedVersion() { +function lockedPackage() { const lockPath = path.join(REPO_ROOT, 'package-lock.json'); const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); const entry = lock.packages?.['node_modules/better-sqlite3']; - if (!entry?.version) { - throw new Error(`no "node_modules/better-sqlite3" entry with a version in ${lockPath}`); + if (!entry?.version || !entry.resolved || !entry.integrity) { + throw new Error(`no complete "node_modules/better-sqlite3" entry in ${lockPath}`); } - return entry.version; + return { version: entry.version, resolved: entry.resolved, integrity: entry.integrity }; } function assetName(version, abi, target) { @@ -75,28 +75,37 @@ async function download(url, dest) { return bytes.length; } -/** The wrapper only — `--ignore-scripts` keeps node-gyp and prebuild-install out, so the - * binding under test is the downloaded one and nothing else. */ -function installWrapper(version, dir) { - const marker = path.join(dir, 'node_modules', 'better-sqlite3', 'package.json'); - if (fs.existsSync(marker)) return path.dirname(marker); +/** + * The JS wrapper, unpacked straight from the registry tarball the lockfile resolves to and + * checked against the lockfile's integrity hash. + * + * `npm install` is deliberately not used. It would need `npm.cmd` on Windows, which Node + * refuses to spawn without `shell: true` (the CVE-2024-27980 hardening) — the same trap + * that made `tests/e2e/init-command.e2e.test.ts` spawn a child that never started. Beyond + * dodging that, unpacking directly means no install lifecycle exists at all, so neither + * prebuild-install nor node-gyp can supply a binding and let this probe verify itself. The + * wrapper's only non-relative dependency is `bindings`, which `lib/database.js` requires + * lazily and only when `nativeBinding` is null — this probe always passes it a path. + */ +async function fetchWrapper(pkg, dir) { + const wrapperDir = path.join(dir, 'package'); + if (fs.existsSync(path.join(wrapperDir, 'lib', 'database.js'))) return wrapperDir; + fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); - execFileSync( - process.platform === 'win32' ? 'npm.cmd' : 'npm', - [ - 'install', - `better-sqlite3@${version}`, - '--ignore-scripts', - '--no-audit', - '--no-fund', - '--no-package-lock', - '--prefix', - dir, - ], - { stdio: 'inherit', cwd: dir } - ); - if (!fs.existsSync(marker)) throw new Error(`wrapper install produced no ${marker}`); - return path.dirname(marker); + const tarball = path.join(dir, 'wrapper.tgz'); + await download(pkg.resolved, tarball); + + const [algo, expected] = pkg.integrity.split('-'); + const actual = createHash(algo).update(fs.readFileSync(tarball)).digest('base64'); + if (actual !== expected) { + throw new Error(`${pkg.resolved} ${algo} is ${algo}-${actual}, lockfile says ${pkg.integrity}`); + } + + execFileSync('tar', ['-xzf', tarball, '-C', dir], { stdio: 'inherit' }); + if (!fs.existsSync(path.join(wrapperDir, 'lib', 'database.js'))) { + throw new Error(`registry tarball extracted without package/lib/database.js in ${dir}`); + } + return wrapperDir; } function driveFts5(Database, bindingPath) { @@ -130,7 +139,8 @@ function driveFts5(Database, bindingPath) { async function main() { const opts = parseArgs(process.argv.slice(2)); - const version = lockedVersion(); + const pkg = lockedPackage(); + const version = pkg.version; const abi = opts.abi ?? process.versions.modules; const target = opts.target ?? `${process.platform}-${process.arch}`; const hostTarget = `${process.platform}-${process.arch}`; @@ -139,12 +149,12 @@ async function main() { fs.rmSync(work, { recursive: true, force: true }); fs.mkdirSync(work, { recursive: true }); - const wrapperDir = installWrapper(version, path.join(os.tmpdir(), 'bs3-prebuild-wrapper')); + const wrapperDir = await fetchWrapper(pkg, path.join(os.tmpdir(), 'bs3-prebuild-wrapper')); console.log(`better-sqlite3 version : ${version} (from package-lock.json)`); console.log(`host : node ${process.version} / ${hostTarget} / ABI ${process.versions.modules}`); console.log(`target under test : ${target} / ABI ${abi}`); - console.log(`wrapper (--ignore-scripts): ${wrapperDir}`); + console.log(`wrapper (no install lifecycle): ${wrapperDir}`); let bindingPath; if (opts.missingBinding) { From 192fedcf4f3b35801363ca29c0b0c3cac3aba884 Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Tue, 18 Aug 2026 19:38:49 +0600 Subject: [PATCH 4/6] fix(prebuild): name the tarball relative to cwd so Windows tar unpacks it Both win32 legs failed with 'tar (child): Cannot connect to C: resolve failed'. Under shell: bash a Windows runner resolves tar to GNU tar from Git for Windows, which reads an absolute C:\... argument as a host:path remote spec. Passing the name relative to cwd removes the colon, and bsdtar in System32 accepts the same form, so one call works on every runner. --- scripts/verify-better-sqlite3-prebuild.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/verify-better-sqlite3-prebuild.mjs b/scripts/verify-better-sqlite3-prebuild.mjs index c1c9b6749..fd8f372a7 100644 --- a/scripts/verify-better-sqlite3-prebuild.mjs +++ b/scripts/verify-better-sqlite3-prebuild.mjs @@ -67,6 +67,19 @@ function assetName(version, abi, target) { return `better-sqlite3-v${version}-node-v${abi}-${target}.tar.gz`; } +/** + * Extract `/` in place. + * + * The tarball is named RELATIVELY with `cwd`, never as an absolute path with `-C`. Under + * `shell: bash` on a Windows runner, PATH resolves `tar` to GNU tar from Git for Windows, + * which reads `C:\...` as a `host:path` remote spec and fails with + * "Cannot connect to C: resolve failed". A relative name has no colon, so GNU tar and the + * bsdtar shipped in System32 both behave. + */ +function extract(dir, name) { + execFileSync('tar', ['-xzf', name], { stdio: 'inherit', cwd: dir }); +} + async function download(url, dest) { const res = await fetch(url, { redirect: 'follow' }); if (!res.ok) throw new Error(`GET ${url} -> HTTP ${res.status} ${res.statusText}`); @@ -101,7 +114,7 @@ async function fetchWrapper(pkg, dir) { throw new Error(`${pkg.resolved} ${algo} is ${algo}-${actual}, lockfile says ${pkg.integrity}`); } - execFileSync('tar', ['-xzf', tarball, '-C', dir], { stdio: 'inherit' }); + extract(dir, 'wrapper.tgz'); if (!fs.existsSync(path.join(wrapperDir, 'lib', 'database.js'))) { throw new Error(`registry tarball extracted without package/lib/database.js in ${dir}`); } @@ -166,7 +179,7 @@ async function main() { const url = `https://github.com/WiseLibs/better-sqlite3/releases/download/v${version}/${name}`; const tarball = path.join(work, name); const bytes = await download(url, tarball); - execFileSync('tar', ['-xzf', tarball, '-C', work], { stdio: 'inherit' }); + extract(work, name); bindingPath = path.join(work, 'build', 'Release', 'better_sqlite3.node'); if (!fs.existsSync(bindingPath)) { throw new Error(`${name} extracted without build/Release/better_sqlite3.node`); From 2dcda2b69af9d475e547a1d74c621da7a16ae4b1 Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Tue, 18 Aug 2026 19:42:17 +0600 Subject: [PATCH 5/6] =?UTF-8?q?test(ci):=20TEMPORARY=20=E2=80=94=20feed=20?= =?UTF-8?q?the=20wrong-arch=20control=20an=20asset=20that=20loads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A control asserted to hold is worth nothing until it has been seen to break. This hands the wrong-arch control each runner's OWN target, so the binding loads and serves an FTS5 MATCH and the step must fail. Reverted in the next commit; this exists so the green beside it has a measured failure mode. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5f8dd25d..3961a9f34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,9 +355,11 @@ jobs: # unfalsifiable: a probe that cannot fail says nothing about the binding it loaded. # `--expect-fail` inverts only the LOAD — the asset must still download and extract, # so a control cannot "pass" by 404ing on a mistyped target. + # TEMPORARY MUTATION — hands this control the runner's OWN target, which loads. + # The step MUST go red. Reverted in the next commit. - name: Negative control — wrong arch (${{ matrix.wrong_arch }}) shell: bash - run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.wrong_arch }} --expect-fail + run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.target }} --expect-fail - name: Negative control — wrong platform (linux-x64 ELF) shell: bash From 3d97e1444f17263bc8a1eef9c913de3141123e87 Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Tue, 18 Aug 2026 19:44:10 +0600 Subject: [PATCH 6/6] =?UTF-8?q?Revert=20"test(ci):=20TEMPORARY=20=E2=80=94?= =?UTF-8?q?=20feed=20the=20wrong-arch=20control=20an=20asset=20that=20load?= =?UTF-8?q?s"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2dcda2b69af9d475e547a1d74c621da7a16ae4b1. --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3961a9f34..c5f8dd25d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,11 +355,9 @@ jobs: # unfalsifiable: a probe that cannot fail says nothing about the binding it loaded. # `--expect-fail` inverts only the LOAD — the asset must still download and extract, # so a control cannot "pass" by 404ing on a mistyped target. - # TEMPORARY MUTATION — hands this control the runner's OWN target, which loads. - # The step MUST go red. Reverted in the next commit. - name: Negative control — wrong arch (${{ matrix.wrong_arch }}) shell: bash - run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.target }} --expect-fail + run: node scripts/verify-better-sqlite3-prebuild.mjs --target ${{ matrix.wrong_arch }} --expect-fail - name: Negative control — wrong platform (linux-x64 ELF) shell: bash