Skip to content

Commit 7ad75d0

Browse files
committed
Get it working
1 parent c62c0f2 commit 7ad75d0

9 files changed

Lines changed: 150 additions & 69 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
name: "CI: Build & Test"
32
on:
43
push:
@@ -9,7 +8,7 @@ on:
98

109
permissions:
1110
contents: read
12-
packages: read # Required for GHCR
11+
packages: read # Required for GHCR
1312

1413
jobs:
1514
job_lint:
@@ -408,9 +407,36 @@ jobs:
408407
- name: Run tests
409408
run: yarn test
410409

410+
job_test_bindings_recompile:
411+
name: Test (recompile from source) (v${{ matrix.node }}) ${{ matrix.os }}
412+
needs: [job_build]
413+
runs-on: ${{ matrix.os }}
414+
env:
415+
DELETE_BINARIES: true
416+
strategy:
417+
fail-fast: false
418+
matrix:
419+
os: [ubuntu-latest, macos-latest, windows-latest]
420+
node: [18, 26]
421+
steps:
422+
- name: Check out current commit
423+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
424+
- name: Set up Node
425+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
426+
with:
427+
node-version: ${{ matrix.node }}
428+
- name: Install dependencies
429+
run: yarn install --ignore-engines --ignore-scripts --frozen-lockfile
430+
- name: Download Tarball
431+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
432+
with:
433+
name: ${{ github.sha }}
434+
- name: Run tests
435+
run: yarn test
436+
411437
job_required_jobs_passed:
412438
name: All required jobs passed
413-
needs: [job_lint, job_test_bindings]
439+
needs: [job_lint, job_test_bindings, job_test_bindings_recompile]
414440
# Always run this, even if a dependent job failed
415441
if: always()
416442
runs-on: ubuntu-latest

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"sentry"
1717
],
1818
"scripts": {
19+
"install": "node -p \"'@sentry/node-native-stacktrace@' + require('./package.json').version\"",
1920
"lint": "yarn lint:eslint && yarn lint:clang",
2021
"lint:eslint": "eslint . --format stylish",
2122
"lint:clang": "node scripts/clang-format.mjs",
@@ -24,21 +25,23 @@
2425
"fix:clang": "node scripts/clang-format.mjs --fix",
2526
"build": "yarn clean && yarn build:lib && yarn build:bindings:configure && yarn build:bindings",
2627
"build:lib": "tsc",
27-
"build:copy-binary": "node -e \"import { copyBinary } from './lib/index.js'; copyBinary();\"",
28+
"build:copy-binary": "node -e \"const { copyBinary } = require('./lib/copy-binary.js'); copyBinary();\"",
2829
"build:bindings:configure": "node-gyp configure",
2930
"build:bindings:configure:arm64": "node-gyp configure --arch=arm64 --target_arch=arm64",
30-
"build:bindings": "node-gyp build && yarn build:copy-binary",
31-
"build:bindings:arm64": "node-gyp build --arch=arm64 && yarn build:copy-binary",
31+
"build:bindings": "yarn build:lib && node-gyp build && yarn build:copy-binary",
32+
"build:bindings:arm64": "yarn build:lib && node-gyp build --arch=arm64 && yarn build:copy-binary",
3233
"build:tarball": "npm pack",
3334
"clean": "node-gyp clean && rm -rf lib && rm -rf build && rm -f *.tgz",
3435
"test": "node ./test/prepare.mjs && vitest run --poolOptions.forks.singleFork --silent=false --disable-console-intercept"
3536
},
37+
"gypfile": false,
3638
"engines": {
3739
"node": ">=18"
3840
},
3941
"dependencies": {
4042
"detect-libc": "^2.0.4",
41-
"node-abi": "^3.89.0"
43+
"node-abi": "^3.92.0",
44+
"node-gyp": "^11.5.0"
4245
},
4346
"devDependencies": {
4447
"@sentry-internal/eslint-config-sdk": "^9.22.0",
@@ -47,7 +50,6 @@
4750
"clang-format": "^1.8.0",
4851
"cross-env": "^7.0.3",
4952
"eslint": "^7.0.0",
50-
"node-gyp": "^11.2.0",
5153
"typescript": "^5.8.3",
5254
"vitest": "^3.1.4"
5355
},

src/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
import * as os from 'node:os';
3+
import { versions } from 'node:process';
4+
import * as libc from 'detect-libc';
5+
import { getAbi } from 'node-abi';
6+
7+
export const stdlib = libc.familySync();
8+
export const platform = process.env['BUILD_PLATFORM'] || os.platform();
9+
export const arch = process.env['BUILD_ARCH'] || os.arch();
10+
export const abi = getAbi(versions.node, 'node');
11+
export const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-');

src/copy-binary.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-disable no-console */
2+
import * as fs from 'node:fs';
3+
import * as path from 'node:path';
4+
import { identifier } from './constants';
5+
6+
const source = path.join(__dirname, '..', 'build', 'Release', 'stack-trace.node');
7+
const target = path.join(__dirname, '..', 'lib', `stack-trace-${identifier}.node`);
8+
9+
/**
10+
* Copies the compiled binary from the build directory to the lib directory with the correct name based on the current platform and Node version.
11+
*
12+
* @hidden We only use this for copying the binary after building, it is not intended to be used by end users.
13+
*/
14+
export function copyBinary(): void {
15+
const build = path.resolve(__dirname, '..', 'lib');
16+
if (!fs.existsSync(build)) {
17+
fs.mkdirSync(build, { recursive: true });
18+
}
19+
20+
if (!fs.existsSync(source)) {
21+
throw new Error(`Source file does not exist: ${ source}`);
22+
} else {
23+
if (fs.existsSync(target)) {
24+
console.log('Target file already exists, overwriting it');
25+
fs.unlinkSync(target);
26+
}
27+
console.log('Copying', source, 'to', target);
28+
fs.copyFileSync(source, target);
29+
}
30+
}

src/index.ts

Lines changed: 51 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,11 @@
22
import type { AsyncLocalStorage } from 'node:async_hooks';
33
import { spawnSync } from 'node:child_process';
44
import * as fs from 'node:fs';
5-
import * as os from 'node:os';
65
import * as path from 'node:path';
7-
import { env, versions } from 'node:process';
6+
import { env } from 'node:process';
87
import { threadId } from 'node:worker_threads';
9-
import * as libc from 'detect-libc';
10-
import { getAbi } from 'node-abi';
11-
12-
const stdlib = libc.familySync();
13-
const platform = process.env['BUILD_PLATFORM'] || os.platform();
14-
const arch = process.env['BUILD_ARCH'] || os.arch();
15-
const abi = getAbi(versions.node, 'node');
16-
const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-');
8+
import { abi, arch, identifier, platform, stdlib } from './constants';
9+
import { copyBinary } from './copy-binary';
1710

1811
type AsyncStorageArgs = {
1912
/** The AsyncLocalStorage instance used to fetch the store */
@@ -56,60 +49,34 @@ interface Native {
5649
getThreadsLastSeen(): Record<string, number>;
5750
}
5851

59-
/**
60-
* Copies the compiled binary from the build directory to the lib directory with the correct name based on the current platform and Node version.
61-
*
62-
* @hidden We only use this for copying the binary after building, it is not intended to be used by end users.
63-
*/
64-
export function copyBinary(): void {
65-
const build = path.resolve(__dirname, '..', 'lib');
66-
if (!fs.existsSync(build)) {
67-
fs.mkdirSync(build, { recursive: true });
68-
}
69-
70-
if (!fs.existsSync(source)) {
71-
console.log('Source file does not exist:', source);
72-
process.exit(1);
73-
} else {
74-
if (fs.existsSync(target)) {
75-
console.log('Target file already exists, overwriting it');
76-
fs.unlinkSync(target);
77-
}
78-
console.log('Copying', source, 'to', target);
79-
fs.copyFileSync(source, target);
80-
}
81-
}
82-
83-
const source = path.join(__dirname, '..', 'build', 'Release', 'stack-trace.node');
84-
const target = path.join(__dirname, '..', 'lib', `stack-trace-${identifier}.node`);
85-
8652
function clean(err: Buffer): string {
8753
return err.toString().trim();
8854
}
8955

9056
function recompileFromSource(): void {
9157
const cwd = path.join(__dirname, '..');
58+
// Resolve node-gyp from its package entry so it's found even when
59+
// node_modules/.bin/ is not in PATH (e.g. outside of npm run scripts).
60+
const nodeGyp = require.resolve('node-gyp/bin/node-gyp.js');
9261
console.log('Compiling from source...');
93-
let spawn = spawnSync('node-gyp', ['configure'], {
62+
let spawn = spawnSync(process.execPath, [nodeGyp, 'configure'], {
9463
cwd,
9564
stdio: ['inherit', 'inherit', 'pipe'],
9665
env: process.env,
97-
shell: true,
9866
});
9967
if (spawn.status !== 0) {
10068
console.log('Failed to configure gyp');
101-
console.log(clean(spawn.stderr));
69+
if (spawn.stderr) console.log(clean(spawn.stderr));
10270
return;
10371
}
104-
spawn = spawnSync('node-gyp', ['build'], {
72+
spawn = spawnSync(process.execPath, [nodeGyp, 'build'], {
10573
cwd,
10674
stdio: ['inherit', 'inherit', 'pipe'],
10775
env: process.env,
108-
shell: true,
10976
});
11077
if (spawn.status !== 0) {
11178
console.log('Failed to build bindings');
112-
console.log(clean(spawn.stderr));
79+
if (spawn.stderr) console.log(clean(spawn.stderr));
11380
return;
11481
}
11582

@@ -280,7 +247,7 @@ function getNativeModule(): Native {
280247
try {
281248
return require('../build/Release/stack-trace.node');
282249
} catch (e) {
283-
console.warn('The \'@sentry-internal/node-native-stacktrace\' binary could not be found. Use \'@electron/rebuild\' to ensure the native module is built for Electron.');
250+
console.warn('The \'@sentry/node-native-stacktrace\' binary could not be found. Use \'@electron/rebuild\' to ensure the native module is built for Electron.');
284251
throw e;
285252
}
286253
}
@@ -290,13 +257,29 @@ function getNativeModule(): Native {
290257
return nativeModule;
291258
}
292259

293-
try {
294-
recompileFromSource();
295-
} catch (e) {
296-
console.warn('Failed to compile from source:', e);
260+
// Exclusive-create a lock file so only one process/thread runs node-gyp at a
261+
// time. Losers (EEXIST) wait for the winner to finish before trying to load.
262+
const lockFile = path.join(__dirname, '..', '.rebuild-lock');
263+
let lockFd: number | undefined;
264+
try { lockFd = fs.openSync(lockFile, 'wx'); } catch { /* another caller holds the lock */ }
265+
266+
if (lockFd !== undefined) {
267+
try {
268+
recompileFromSource();
269+
} catch (e) {
270+
console.warn('Failed to compile from source:', e);
271+
} finally {
272+
try { fs.closeSync(lockFd); } catch { /* ignore */ }
273+
try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
274+
}
275+
} else {
276+
const timer = new Int32Array(new SharedArrayBuffer(4));
277+
while (fs.existsSync(lockFile)) {
278+
Atomics.wait(timer, 0, 0, 250);
279+
}
297280
}
298281

299-
// Try again after attempting to recompile, in case the binary is now available.
282+
// Try again after recompile (or after another caller finished theirs).
300283
nativeModule = tryLoad();
301284

302285
if (nativeModule) {
@@ -308,7 +291,26 @@ function getNativeModule(): Native {
308291

309292
const native = getNativeModule();
310293

294+
/**
295+
* Registers the current thread with the native module.
296+
*
297+
* This should be called on every thread that you want to capture stack traces from.
298+
*
299+
* @param threadName The name of the thread
300+
*
301+
* threadName defaults to the `threadId` if not provided.
302+
*/
311303
export function registerThread(threadName?: string): void;
304+
/**
305+
* Registers the current thread with the native module.
306+
*
307+
* This should be called on every thread that you want to capture stack traces from.
308+
*
309+
* @param storageOrThread Either the name of the thread, or an object containing an AsyncLocalStorage instance and optional storage key.
310+
* @param threadName The name of the thread, if the first argument is an object.
311+
*
312+
* threadName defaults to the `threadId` if not provided.
313+
*/
312314
export function registerThread(storageOrThread: AsyncStorageArgs | string, threadName?: string): void;
313315
/**
314316
* Registers the current thread with the native module.

test/e2e.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const __dirname = import.meta.dirname || new URL('.', import.meta.url).pathname;
77
const NODE_MAJOR_VERSION = parseInt(process.versions.node.split('.')[0], 10);
88

99
// macOS emulated x64 in CI is very slow!
10-
const timeout = process.env.CI && process.platform === 'darwin' ? 60000 : 20000;
10+
const timeout = (process.env.CI && process.platform === 'darwin') || process.env.DELETE_BINARIES ? 60000 : 20000;
1111

1212
async function runTest(...paths) {
1313
console.time('Test Run');

test/prepare.mjs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { execSync, spawnSync } from 'node:child_process';
2-
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
33
import { createRequire } from 'node:module';
44
import { dirname, join, relative } from 'node:path';
55
import { fileURLToPath } from 'node:url';
@@ -40,6 +40,16 @@ function installTarballAsDependency(root) {
4040

4141
console.log('Installing dependencies...');
4242
execSync('yarn install', { cwd: root, stdio: 'inherit' });
43+
44+
// For recompile from source tests, we need to ensure the pre-built binaries are removed
45+
if (process.env.DELETE_BINARIES) {
46+
const unpackedDir = join(root, 'node_modules', '@sentry/node-native-stacktrace', 'lib');
47+
readdirSync(unpackedDir).forEach(file => {
48+
if (file.endsWith('.node')) {
49+
rmSync(join(unpackedDir, file));
50+
}
51+
});
52+
}
4353
}
4454

4555
installTarballAsDependency(__dirname);

tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"compilerOptions": {
33
"target": "ES2020",
4-
"module": "node16",
4+
"module": "commonjs",
55
"declaration": true,
66
"outDir": "lib",
77
"strict": true,
88
"skipLibCheck": true,
99
"forceConsistentCasingInFileNames": true,
10-
"moduleResolution": "node16",
10+
"moduleResolution": "node",
1111
"noEmit": false,
1212
"sourceMap": true,
1313
"declarationMap": true,

yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2291,17 +2291,17 @@ negotiator@^1.0.0:
22912291
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
22922292
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
22932293

2294-
node-abi@^3.89.0:
2295-
version "3.89.0"
2296-
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.89.0.tgz#eea98bf89d4534743bbbf2defa9f4f9bd3bdccfd"
2297-
integrity sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==
2294+
node-abi@^3.92.0:
2295+
version "3.92.0"
2296+
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.92.0.tgz#18e2214677499b8dda81ffcd095afc763d5a9802"
2297+
integrity sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==
22982298
dependencies:
22992299
semver "^7.3.5"
23002300

2301-
node-gyp@^11.2.0:
2302-
version "11.2.0"
2303-
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-11.2.0.tgz#fe2ee7f0511424d6ad70f7a0c88d7346f2fc6a6e"
2304-
integrity sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==
2301+
node-gyp@^11.5.0:
2302+
version "11.5.0"
2303+
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-11.5.0.tgz#82661b5f40647a7361efe918e3cea76d297fcc56"
2304+
integrity sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==
23052305
dependencies:
23062306
env-paths "^2.2.0"
23072307
exponential-backoff "^3.1.1"

0 commit comments

Comments
 (0)