diff --git a/package.json b/package.json index 159ff63..94ae690 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "sentry" ], "scripts": { - "install": "node scripts/check-build.mjs", "lint": "yarn lint:eslint && yarn lint:clang", "lint:eslint": "eslint . --format stylish", "lint:clang": "node scripts/clang-format.mjs", @@ -25,16 +24,16 @@ "fix:clang": "node scripts/clang-format.mjs --fix", "build": "yarn clean && yarn build:lib && yarn build:bindings:configure && yarn build:bindings", "build:lib": "tsc", + "build:copy-binary": "node -e \"const { copyBinary } = require('./lib/copy-binary.js'); copyBinary();\"", "build:bindings:configure": "node-gyp configure", "build:bindings:configure:arm64": "node-gyp configure --arch=arm64 --target_arch=arm64", - "build:bindings": "node-gyp build && node scripts/copy-target.mjs", - "build:bindings:arm64": "node-gyp build --arch=arm64 && node scripts/copy-target.mjs", + "build:bindings": "yarn build:lib && node-gyp build && yarn build:copy-binary", + "build:bindings:arm64": "yarn build:lib && node-gyp build --arch=arm64 && yarn build:copy-binary", "build:tarball": "npm pack", "clean": "node-gyp clean && rm -rf lib && rm -rf build && rm -f *.tgz", - "test": "yarn test:install && yarn test:prepare && vitest run --poolOptions.forks.singleFork --silent=false --disable-console-intercept", - "test:prepare": "node ./test/prepare.mjs", - "test:install": "cross-env ALWAYS_THROW=true yarn install" + "test": "node ./test/prepare.mjs && vitest run --poolOptions.forks.singleFork --silent=false --disable-console-intercept" }, + "gypfile": false, "engines": { "node": ">=18" }, diff --git a/scripts/binaries.mjs b/scripts/binaries.mjs deleted file mode 100644 index 68ac5f0..0000000 --- a/scripts/binaries.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import libc from 'detect-libc'; -import * as abi from 'node-abi'; -import os from 'os'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export function getModuleName() { - const stdlib = libc.familySync(); - const platform = process.env['BUILD_PLATFORM'] || os.platform(); - const arch = process.env['BUILD_ARCH'] || os.arch(); - const identifier = [platform, arch, stdlib, abi.getAbi(process.versions.node, 'node')].filter(Boolean).join('-'); - return `stack-trace-${identifier}.node`; -} - -export const source = path.join(__dirname, '..', 'build', 'Release', 'stack-trace.node'); -export const target = path.join(__dirname, '..', 'lib', getModuleName()); diff --git a/scripts/check-build.mjs b/scripts/check-build.mjs deleted file mode 100644 index 03eb347..0000000 --- a/scripts/check-build.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import * as child_process from 'node:child_process'; -import * as fs from 'node:fs'; -import { createRequire } from 'node:module'; -import * as binaries from './binaries.mjs'; - -const require = createRequire(import.meta.url); - -function clean(err) { - return err.toString().trim(); -} - -async function recompileFromSource() { - console.log('Compiling from source...'); - let spawn = child_process.spawnSync('node-gyp', ['configure'], { - stdio: ['inherit', 'inherit', 'pipe'], - env: process.env, - shell: true, - }); - if (spawn.status !== 0) { - console.log('Failed to configure gyp'); - console.log(clean(spawn.stderr)); - return; - } - spawn = child_process.spawnSync('node-gyp', ['build'], { - stdio: ['inherit', 'inherit', 'pipe'], - env: process.env, - shell: true, - }); - if (spawn.status !== 0) { - console.log('Failed to build bindings'); - console.log(clean(spawn.stderr)); - return; - } - - await import('./copy-target.mjs'); -} - -if (fs.existsSync(binaries.target)) { - try { - require(binaries.target); - console.log('Precompiled binary found, skipping build from source.'); - } catch (e) { - console.log('Precompiled binary found but failed loading'); - if (process.env.ALWAYS_THROW) { - throw e; - } else { - console.log(e); - } - - await recompileFromSource(); - } -} else { - console.log('No precompiled binary found'); - await recompileFromSource(); -} diff --git a/scripts/copy-target.mjs b/scripts/copy-target.mjs deleted file mode 100644 index c570ef1..0000000 --- a/scripts/copy-target.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import * as binaries from './binaries.mjs'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const build = path.resolve(__dirname, '..', 'lib'); -if (!fs.existsSync(build)) { - fs.mkdirSync(build, { recursive: true }); -} - -const source = binaries.source; -const target = binaries.target; - -if (!fs.existsSync(source)) { - console.log('Source file does not exist:', source); - process.exit(1); -} else { - if (fs.existsSync(target)) { - console.log('Target file already exists, overwriting it'); - fs.unlinkSync(target); - } - console.log('Copying', source, 'to', target); - fs.copyFileSync(source, target); -} diff --git a/src/copy-binary.ts b/src/copy-binary.ts new file mode 100644 index 0000000..0ed51c1 --- /dev/null +++ b/src/copy-binary.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { identifier } from './identifier'; + +const source = path.join(__dirname, '..', 'build', 'Release', 'stack-trace.node'); +const target = path.join(__dirname, '..', 'lib', `stack-trace-${identifier}.node`); + +/** + * Copies the compiled binary from the build directory to the lib directory with the correct name based on the current platform and Node version. + * + * @hidden We only use this for copying the binary after building, it is not intended to be used by end users. + */ +export function copyBinary(): void { + const build = path.resolve(__dirname, '..', 'lib'); + if (!fs.existsSync(build)) { + fs.mkdirSync(build, { recursive: true }); + } + + if (!fs.existsSync(source)) { + console.log('Source file does not exist:', source); + process.exit(1); + } else { + if (fs.existsSync(target)) { + console.log('Target file already exists, overwriting it'); + fs.unlinkSync(target); + } + console.log('Copying', source, 'to', target); + fs.copyFileSync(source, target); + } +} diff --git a/src/identifier.ts b/src/identifier.ts new file mode 100644 index 0000000..01225d7 --- /dev/null +++ b/src/identifier.ts @@ -0,0 +1,12 @@ + +import * as os from 'node:os'; +import { versions } from 'node:process'; +import * as libc from 'detect-libc'; +import { getAbi } from 'node-abi'; + +const stdlib = libc.familySync(); +const platform = process.env['BUILD_PLATFORM'] || os.platform(); +const arch = process.env['BUILD_ARCH'] || os.arch(); +const abi = getAbi(versions.node, 'node'); + +export const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); diff --git a/src/index.ts b/src/index.ts index f81c67c..b589af7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,17 @@ +/* eslint-disable no-console */ import type { AsyncLocalStorage } from 'node:async_hooks'; -import { arch as _arch, platform as _platform } from 'node:os'; -import { join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import * as os from 'node:os'; +import * as path from 'node:path'; import { env, versions } from 'node:process'; import { threadId } from 'node:worker_threads'; -import { familySync } from 'detect-libc'; +import * as libc from 'detect-libc'; import { getAbi } from 'node-abi'; +import { copyBinary } from './copy-binary'; -const stdlib = familySync(); -const platform = process.env['BUILD_PLATFORM'] || _platform(); -const arch = process.env['BUILD_ARCH'] || _arch(); +const stdlib = libc.familySync(); +const platform = process.env['BUILD_PLATFORM'] || os.platform(); +const arch = process.env['BUILD_ARCH'] || os.arch(); const abi = getAbi(versions.node, 'node'); const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); @@ -53,172 +56,251 @@ interface Native { getThreadsLastSeen(): Record; } -// eslint-disable-next-line complexity -function getNativeModule(): Native { - // If a binary path is specified, use that. - if (env['SENTRY_STACK_TRACE_BINARY_PATH']) { - const envPath = env['SENTRY_STACK_TRACE_BINARY_PATH']; - return require(envPath); - } +function clean(err: Buffer): string { + return err.toString().trim(); +} - // If a user specifies a different binary dir, they are in control of the binaries being moved there - if (env['SENTRY_STACK_TRACE_BINARY_DIR']) { - const binaryPath = join(resolve(env['SENTRY_STACK_TRACE_BINARY_DIR']), `stack-trace-${identifier}.node`); - return require(binaryPath); +function recompileFromSource(): void { + const cwd = path.join(__dirname, '..'); + console.log('Compiling from source...'); + let spawn = spawnSync('node-gyp', ['configure'], { + cwd, + stdio: ['inherit', 'inherit', 'pipe'], + env: process.env, + shell: true, + }); + if (spawn.status !== 0) { + console.log('Failed to configure gyp'); + console.log(clean(spawn.stderr)); + return; } - - if (process.versions.electron) { - try { - return require('../build/Release/stack-trace.node'); - } catch (e) { - // eslint-disable-next-line no-console - console.warn('The \'@sentry/node-native-stacktrace\' binary could not be found. Use \'@electron/rebuild\' to ensure the native module is built for Electron.'); - throw e; - } + spawn = spawnSync('node-gyp', ['build'], { + cwd, + stdio: ['inherit', 'inherit', 'pipe'], + env: process.env, + shell: true, + }); + if (spawn.status !== 0) { + console.log('Failed to build bindings'); + console.log(clean(spawn.stderr)); + return; } - // We need the fallthrough so that in the end, we can fallback to the dynamic require. - // This is for cases where precompiled binaries were not provided, but may have been compiled from source. - if (platform === 'darwin') { - if (arch === 'x64') { - if (abi === '108') { - return require('./stack-trace-darwin-x64-108.node'); - } - if (abi === '115') { - return require('./stack-trace-darwin-x64-115.node'); - } - if (abi === '127') { - return require('./stack-trace-darwin-x64-127.node'); - } - if (abi === '137') { - return require('./stack-trace-darwin-x64-137.node'); - } - if (abi === '147') { - return require('./stack-trace-darwin-x64-147.node'); - } - } + console.log('Successfully compiled from source...'); - if (arch === 'arm64') { - if (abi === '108') { - return require('./stack-trace-darwin-arm64-108.node'); - } - if (abi === '115') { - return require('./stack-trace-darwin-arm64-115.node'); - } - if (abi === '127') { - return require('./stack-trace-darwin-arm64-127.node'); - } - if (abi === '137') { - return require('./stack-trace-darwin-arm64-137.node'); - } - if (abi === '147') { - return require('./stack-trace-darwin-arm64-147.node'); - } - } - } - - if (platform === 'win32') { - if (arch === 'x64') { - if (abi === '108') { - return require('./stack-trace-win32-x64-108.node'); - } - if (abi === '115') { - return require('./stack-trace-win32-x64-115.node'); - } - if (abi === '127') { - return require('./stack-trace-win32-x64-127.node'); - } - if (abi === '137') { - return require('./stack-trace-win32-x64-137.node'); - } - if (abi === '147') { - return require('./stack-trace-win32-x64-147.node'); - } - } - } + copyBinary(); +} - if (platform === 'linux') { - if (arch === 'x64') { - if (stdlib === 'musl') { +// eslint-disable-next-line complexity +function tryLoad(): Native | undefined { + try { + // We could just dynamically require the module based on the identifier, but + // doing so means that bundlers will not pick these files up. + if (platform === 'darwin') { + if (arch === 'x64') { if (abi === '108') { - return require('./stack-trace-linux-x64-musl-108.node'); + return require('./stack-trace-darwin-x64-108.node'); } if (abi === '115') { - return require('./stack-trace-linux-x64-musl-115.node'); + return require('./stack-trace-darwin-x64-115.node'); } if (abi === '127') { - return require('./stack-trace-linux-x64-musl-127.node'); + return require('./stack-trace-darwin-x64-127.node'); } if (abi === '137') { - return require('./stack-trace-linux-x64-musl-137.node'); + return require('./stack-trace-darwin-x64-137.node'); } if (abi === '147') { - return require('./stack-trace-linux-x64-musl-147.node'); + return require('./stack-trace-darwin-x64-147.node'); } } - if (stdlib === 'glibc') { + + if (arch === 'arm64') { if (abi === '108') { - return require('./stack-trace-linux-x64-glibc-108.node'); + return require('./stack-trace-darwin-arm64-108.node'); } if (abi === '115') { - return require('./stack-trace-linux-x64-glibc-115.node'); + return require('./stack-trace-darwin-arm64-115.node'); } if (abi === '127') { - return require('./stack-trace-linux-x64-glibc-127.node'); + return require('./stack-trace-darwin-arm64-127.node'); } if (abi === '137') { - return require('./stack-trace-linux-x64-glibc-137.node'); + return require('./stack-trace-darwin-arm64-137.node'); } if (abi === '147') { - return require('./stack-trace-linux-x64-glibc-147.node'); + return require('./stack-trace-darwin-arm64-147.node'); } } } - if (arch === 'arm64') { - if (stdlib === 'musl') { + + if (platform === 'win32') { + if (arch === 'x64') { if (abi === '108') { - return require('./stack-trace-linux-arm64-musl-108.node'); + return require('./stack-trace-win32-x64-108.node'); } if (abi === '115') { - return require('./stack-trace-linux-arm64-musl-115.node'); + return require('./stack-trace-win32-x64-115.node'); } if (abi === '127') { - return require('./stack-trace-linux-arm64-musl-127.node'); + return require('./stack-trace-win32-x64-127.node'); } if (abi === '137') { - return require('./stack-trace-linux-arm64-musl-137.node'); + return require('./stack-trace-win32-x64-137.node'); } if (abi === '147') { - return require('./stack-trace-linux-arm64-musl-147.node'); + return require('./stack-trace-win32-x64-147.node'); } } + } - if (stdlib === 'glibc') { - if (abi === '108') { - return require('./stack-trace-linux-arm64-glibc-108.node'); - } - if (abi === '115') { - return require('./stack-trace-linux-arm64-glibc-115.node'); + if (platform === 'linux') { + if (arch === 'x64') { + if (stdlib === 'musl') { + if (abi === '108') { + return require('./stack-trace-linux-x64-musl-108.node'); + } + if (abi === '115') { + return require('./stack-trace-linux-x64-musl-115.node'); + } + if (abi === '127') { + return require('./stack-trace-linux-x64-musl-127.node'); + } + if (abi === '137') { + return require('./stack-trace-linux-x64-musl-137.node'); + } + if (abi === '147') { + return require('./stack-trace-linux-x64-musl-147.node'); + } } - if (abi === '127') { - return require('./stack-trace-linux-arm64-glibc-127.node'); + if (stdlib === 'glibc') { + if (abi === '108') { + return require('./stack-trace-linux-x64-glibc-108.node'); + } + if (abi === '115') { + return require('./stack-trace-linux-x64-glibc-115.node'); + } + if (abi === '127') { + return require('./stack-trace-linux-x64-glibc-127.node'); + } + if (abi === '137') { + return require('./stack-trace-linux-x64-glibc-137.node'); + } + if (abi === '147') { + return require('./stack-trace-linux-x64-glibc-147.node'); + } } - if (abi === '137') { - return require('./stack-trace-linux-arm64-glibc-137.node'); + } + if (arch === 'arm64') { + if (stdlib === 'musl') { + if (abi === '108') { + return require('./stack-trace-linux-arm64-musl-108.node'); + } + if (abi === '115') { + return require('./stack-trace-linux-arm64-musl-115.node'); + } + if (abi === '127') { + return require('./stack-trace-linux-arm64-musl-127.node'); + } + if (abi === '137') { + return require('./stack-trace-linux-arm64-musl-137.node'); + } + if (abi === '147') { + return require('./stack-trace-linux-arm64-musl-147.node'); + } } - if (abi === '147') { - return require('./stack-trace-linux-arm64-glibc-147.node'); + + if (stdlib === 'glibc') { + if (abi === '108') { + return require('./stack-trace-linux-arm64-glibc-108.node'); + } + if (abi === '115') { + return require('./stack-trace-linux-arm64-glibc-115.node'); + } + if (abi === '127') { + return require('./stack-trace-linux-arm64-glibc-127.node'); + } + if (abi === '137') { + return require('./stack-trace-linux-arm64-glibc-137.node'); + } + if (abi === '147') { + return require('./stack-trace-linux-arm64-glibc-147.node'); + } } } } + + return require(`./stack-trace-${identifier}.node`); + } catch { + return undefined; + } +} + +function getNativeModule(): Native { + // If a binary path is specified, use that. + if (env['SENTRY_STACK_TRACE_BINARY_PATH']) { + const envPath = env['SENTRY_STACK_TRACE_BINARY_PATH']; + return require(envPath); + } + + // If a user specifies a different binary dir, they are in control of the binaries being moved there + if (env['SENTRY_STACK_TRACE_BINARY_DIR']) { + const binaryPath = path.join(path.resolve(env['SENTRY_STACK_TRACE_BINARY_DIR']), `stack-trace-${identifier}.node`); + return require(binaryPath); + } + + if (process.versions.electron) { + try { + return require('../build/Release/stack-trace.node'); + } catch (e) { + 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.'); + throw e; + } } - return require(`./stack-trace-${identifier}.node`); + let nativeModule = tryLoad(); + if (nativeModule) { + return nativeModule; + } + + try { + recompileFromSource(); + } catch (e) { + console.warn('Failed to compile from source:', e); + } + + // Try again after attempting to recompile, in case the binary is now available. + nativeModule = tryLoad(); + + if (nativeModule) { + return nativeModule; + } + + throw new Error('Failed to load native module. A prebuilt binary for your platform and Node version was not found and recompiling from source failed.'); } const native = getNativeModule(); +/** + * Registers the current thread with the native module. + * + * This should be called on every thread that you want to capture stack traces from. + * + * @param threadName The name of the thread + * + * threadName defaults to the `threadId` if not provided. + */ export function registerThread(threadName?: string): void; +/** + * Registers the current thread with the native module. + * + * This should be called on every thread that you want to capture stack traces from. + * + * @param storageOrThread Either the name of the thread, or an object containing an AsyncLocalStorage instance and optional storage key. + * @param threadName The name of the thread, if the first argument is an object. + * + * threadName defaults to the `threadId` if not provided. + */ export function registerThread(storageOrThread: AsyncStorageArgs | string, threadName?: string): void; /** * Registers the current thread with the native module.