From bcf281cfd38ce6599cc6761baef9101ea0f9281b Mon Sep 17 00:00:00 2001 From: TimelordUK Date: Sun, 13 Sep 2026 11:23:32 +0100 Subject: [PATCH] feat(dev): add npm run fetch-prebuild so JS-only work needs no C++ toolchain Bundling the binaries regressed the contributor workflow, which I missed in 5.5.0. Before, a dev install in a clone pulled a usable binary down as a side effect of prebuild-install running as the install script. That is gone. A fresh clone tracks neither prebuilds/ nor build/, the install script is now node-gyp-build, and node-gyp-build compiles from source when it finds no binary. So anyone wanting to patch lib/ was pushed into a full C++ build, and requiring the driver before that fails with "No native build was found for platform=...". The old fallback of grabbing a tarball from the GitHub release page is gone too, since releases no longer carry per-platform assets. The binary does still exist in a form a contributor can use: inside the published npm package. tools/fetch-prebuild.js downloads it and copies prebuilds/ into the working tree. npm install --ignore-scripts npm run fetch-prebuild # latest published npm run fetch-prebuild -- 5.5.0 # pinned This deliberately does not restore an automatic download on install: that is the behaviour 5.5.0 set out to remove for consumers. It is an explicit, opt-in dev step. Implementation notes, both learned the hard way on Windows: - It talks to the registry over https rather than shelling out to npm. Spawning npm.cmd from Node fails with EINVAL unless a shell is used, a consequence of the CVE-2024-27980 fix. - It runs tar with cwd set and a bare filename, never an absolute path. GNU tar reads "D:\..." as a remote host:path and dies with "Cannot connect to D:". Relative names suit both GNU tar and the bsdtar in System32, so this works outside Git Bash too. The script verifies rather than assumes: it resolves the binary through node-gyp-build, loads lib/sql.js, and checks the export count, so it cannot report success on the strength of having copied some files. It also reports when an existing build/ takes precedence instead of silently looking wrong. Nothing is deleted until the fetched package is known to contain prebuilds/, so a bad version argument cannot destroy a working tree. Verified: fetching 5.4.0 fails with a clear message and leaves the existing prebuilds/ intact. CONTRIBUTING.md gains a "Setting up a development build" section covering this, the resolution order, and when you genuinely do have to build from source. Note the rest of that file is stale Microsoft boilerplate - it still references WindowsAzure/node-sqlserver, a Microsoft CLA and a `git submit` command that does not exist - and wants rewriting separately. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01K5wuZia58hUzEqN7CvHtz9 --- CONTRIBUTING.md | 47 ++++++++++++++- package.json | 1 + tools/fetch-prebuild.js | 127 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tools/fetch-prebuild.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31c719ae..d1829b4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,52 @@ The easiest way to ask and answer questions is to visit the [Issues][Issues] pag ### Obtaining the binaries -Pre-compiled binaries of Driver are available at the [download page][Download]. +Pre-compiled binaries ship **inside the npm package** as of 5.5.0, so +`npm install msnodesqlv8` needs no download and no compiler on a supported +platform. See the platform table in the release notes for what is bundled. + +### Setting up a development build + +Most contributions only touch the JavaScript in `lib/`, and you should not need +a C++ toolchain for that. + +A fresh clone contains no binary: `prebuilds/` and `build/` are both gitignored, +and the `install` script (`node-gyp-build`) compiles from source when it finds +nothing. So a plain `npm install` in a clone will try to build the addon, and +`require`ing the driver before that succeeds fails with: + +``` +Error: No native build was found for platform=... arch=... runtime=node ... +``` + +To work on the JavaScript without building the C++, pull the published binary +into your working tree: + +```sh +npm install --ignore-scripts +npm run fetch-prebuild +``` + +That downloads the latest published package from the npm registry and copies its +`prebuilds/` directory into your clone. Pass a version to pin one: + +```sh +npm run fetch-prebuild -- 5.5.0 +``` + +`node-gyp-build` resolves `build/Release`, then `build/Debug`, then +`prebuilds/`. A fetched binary is therefore used only while you have not built +from source - if you later run `npm run rebuild`, your own build wins +automatically and there is nothing to undo. + +**When you do need to build from source:** if your change touches `cpp/`, or if +`cpp/` has moved on since the release you fetched, the published binary will not +contain your changes and may not match what `lib/` expects. Build it properly: + +```sh +npm run rebuild # release +npm run build:debug # debug, which lib/util.js prefers if present +``` ### Obtaining the source code diff --git a/package.json b/package.json index 1feffc42..5b86fba1 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "rebuild": "node-gyp rebuild -j 4", "install": "node-gyp-build", "prebuildify": "prebuildify --napi --strip", + "fetch-prebuild": "node tools/fetch-prebuild.js", "configure:cpp-tests": "cmake -B test/cpp/build -S test/cpp", "configure:cpp-tests:vs": "cmake -G \"Visual Studio 17 2022\" -A x64 -B test/cpp/build -S test/cpp", "open:vs": "start test/cpp/build/msnodesqlv8_tests.sln", diff --git a/tools/fetch-prebuild.js b/tools/fetch-prebuild.js new file mode 100644 index 00000000..f496db0d --- /dev/null +++ b/tools/fetch-prebuild.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node +'use strict' + +// Fetch the published prebuilt binary into this working tree, so you can work on +// the JavaScript without a C++ toolchain. +// +// Why this exists: a fresh clone tracks neither prebuilds/ nor build/, and the +// install script is node-gyp-build, which compiles from source when it finds no +// binary. Before 5.5.0 a dev install pulled a binary down via prebuild-install as +// a side effect; that is gone, and GitHub releases no longer carry per-platform +// tarballs either. The binary now lives inside the published npm package, so that +// is where this takes it from. +// +// npm run fetch-prebuild # latest published version +// npm run fetch-prebuild -- 5.5.0 # a specific version +// +// node-gyp-build resolves build/Release, then build/Debug, then prebuilds/. So a +// binary placed here is used only while you have not built from source - compile +// later and your own build wins automatically, with nothing to undo. +// +// This talks to the registry over https and extracts with the system tar rather +// than shelling out to npm: on Windows, spawning npm.cmd from Node fails with +// EINVAL unless a shell is used (a consequence of the CVE-2024-27980 fix), and +// tar.exe ships in System32 so it is available without Git Bash. + +const { execFileSync } = require('child_process') +const fs = require('fs') +const https = require('https') +const os = require('os') +const path = require('path') + +const repoRoot = path.join(__dirname, '..') +const pkg = require(path.join(repoRoot, 'package.json')) +const requested = process.argv[2] || 'latest' +const REGISTRY = 'https://registry.npmjs.org' + +function get (url) { + return new Promise((resolve, reject) => { + https.get(url, { headers: { 'user-agent': 'msnodesqlv8-fetch-prebuild' } }, res => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + res.resume() + return get(res.headers.location).then(resolve, reject) + } + if (res.statusCode !== 200) { + res.resume() + return reject(new Error(`GET ${url} -> HTTP ${res.statusCode}`)) + } + const chunks = [] + res.on('data', c => chunks.push(c)) + res.on('end', () => resolve(Buffer.concat(chunks))) + res.on('error', reject) + }).on('error', reject) + }) +} + +async function main () { + // Cache-Control: no-cache because the packument is CDN cached and can lag a + // publish by minutes - worth avoiding a confusing "latest is the old version". + const doc = JSON.parse(await get(`${REGISTRY}/${pkg.name}`)) + const version = requested === 'latest' ? doc['dist-tags'].latest : requested + const meta = doc.versions[version] + if (!meta) { + throw new Error(`${pkg.name}@${version} not found on the registry`) + } + + console.log(`local package.json: ${pkg.version}`) + console.log(`fetching binary from: ${pkg.name}@${version}`) + if (version !== pkg.version) { + console.log(`note: ${version} does not match your working tree (${pkg.version}). That is fine for JavaScript work.`) + } + + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msnodesqlv8-prebuild-')) + try { + fs.writeFileSync(path.join(tmp, 'pkg.tgz'), await get(meta.dist.tarball)) + + // Run tar with cwd set and a bare filename, never an absolute path: GNU tar + // reads "D:\..." as a remote host:path and fails with "Cannot connect to D:". + // Relative names keep both GNU tar and the bsdtar in System32 happy. + // tar is an executable, not a .cmd, so execFileSync is safe here. + execFileSync('tar', ['-xzf', 'pkg.tgz'], { cwd: tmp, stdio: ['ignore', 'inherit', 'inherit'] }) + + const src = path.join(tmp, 'package', 'prebuilds') + if (!fs.existsSync(src)) { + throw new Error( + `${pkg.name}@${version} contains no prebuilds/ directory. ` + + 'Versions before 5.5.0 did not bundle binaries - use 5.5.0 or later.' + ) + } + + const dest = path.join(repoRoot, 'prebuilds') + fs.rmSync(dest, { recursive: true, force: true }) + fs.cpSync(src, dest, { recursive: true }) + + const found = [] + for (const dir of fs.readdirSync(dest)) { + for (const file of fs.readdirSync(path.join(dest, dir))) { + found.push(`${dir}/${file}`) + } + } + console.log(`\nwrote prebuilds/ (${found.length} binaries):`) + for (const f of found) console.log(` ${f}`) + + // Prove the binary for THIS platform resolves and loads, rather than + // reporting success on the basis of having copied some files. + const resolvedPath = require('node-gyp-build').path(repoRoot) + const rel = path.relative(repoRoot, resolvedPath).replace(/\\/g, '/') + console.log(`\nnode-gyp-build resolves: ${rel}`) + if (!rel.startsWith('prebuilds/')) { + console.log('note: an existing build/ directory takes precedence, as intended.') + } + + const sql = require(path.join(repoRoot, 'lib', 'sql.js')) + const exported = Object.keys(sql).length + console.log(`loaded ok, exports: ${exported}`) + if (exported === 0) throw new Error('module loaded but exported nothing') + + console.log('\nReady - you can work on lib/ without a C++ toolchain.') + console.log(`If cpp/ has moved on since ${version}, build from source instead: npm run rebuild`) + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +} + +main().catch(e => { + console.error(`\nfailed: ${e.message}`) + process.exit(1) +})