Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
127 changes: 127 additions & 0 deletions tools/fetch-prebuild.js
Original file line number Diff line number Diff line change
@@ -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)
})
Loading