diff --git a/README.md b/README.md index 0ad569f..27f9d09 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,19 @@ Memory comparison between standard Node.js 22 and pointer-compressed Node.js 25 **Non-N-API native addons may crash.** Addons using the older V8 native addon API (like `better-sqlite3`) are not compatible with pointer compression and will segfault. Always prefer N-API-based alternatives. +**NAN addons** (which use the older ABI-specific native addon API) must be rebuilt from source against this Node.js build. After running `npm install`, use the provided script to detect and rebuild any NAN addons automatically: + +```bash +# Default: scans /usr/src/app/node_modules +rebuild-nan-addons.sh + +# Or specify a custom path +REBUILD_NAN_ADDONS_PATH=/path/to/node_modules rebuild-nan-addons.sh + +# Detect only (no rebuild) +REBUILD_NAN_ADDONS_DETECT_ONLY=1 rebuild-nan-addons.sh +``` + ## How It Works The Dockerfile builds Node.js from the v25.x branch with the `--experimental-enable-pointer-compression` configure flag. This enables V8's pointer compression feature which uses 32-bit offsets from a base address instead of full 64-bit pointers. diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile index 16ce77c..8f983ae 100644 --- a/docker/alpine/Dockerfile +++ b/docker/alpine/Dockerfile @@ -58,6 +58,7 @@ RUN apk add --no-cache \ # Copy Node.js installation from builder COPY --from=builder /node-install/usr/local /usr/local +COPY ../scripts/rebuild-nan-addons.sh /usr/local/bin/rebuild-nan-addons.sh # Verify installation RUN node --version && npm --version diff --git a/docker/bookworm/Dockerfile b/docker/bookworm/Dockerfile index bc83d6f..0faad82 100644 --- a/docker/bookworm/Dockerfile +++ b/docker/bookworm/Dockerfile @@ -59,6 +59,8 @@ RUN apt-get purge -y \ # Verify installation RUN node --version && npm --version +COPY ../scripts/rebuild-nan-addons.sh /usr/local/bin/rebuild-nan-addons.sh + # Set working directory for running applications WORKDIR /app diff --git a/docker/slim/Dockerfile b/docker/slim/Dockerfile index af8fa85..52a16c9 100644 --- a/docker/slim/Dockerfile +++ b/docker/slim/Dockerfile @@ -59,6 +59,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Copy Node.js installation from builder COPY --from=builder /node-install/usr/local /usr/local +COPY ../scripts/rebuild-nan-addons.sh /usr/local/bin/rebuild-nan-addons.sh # Verify installation RUN node --version && npm --version diff --git a/scripts/rebuild-nan-addons.sh b/scripts/rebuild-nan-addons.sh new file mode 100755 index 0000000..05920b1 --- /dev/null +++ b/scripts/rebuild-nan-addons.sh @@ -0,0 +1,148 @@ +#!/bin/sh + +node_modules="${REBUILD_NAN_ADDONS_PATH:-/usr/src/app/node_modules}" +detect_only="${REBUILD_NAN_ADDONS_DETECT_ONLY:-0}" +fail_marker="$(mktemp)" +rm -f "$fail_marker" + +if [ ! -d "$node_modules" ]; then + exit 0 +fi + +# Returns 0 if the package uses nan (needs rebuild), 1 if it's N-API or not native +uses_nan() { + pkg_dir="$1" + + # Must have binding.gyp to be a native addon + [ -f "$pkg_dir/binding.gyp" ] || return 1 + + # If source files exist, check them for nan.h include + src_files="$(find "$pkg_dir" -maxdepth 4 \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" \) 2>/dev/null)" + if [ -n "$src_files" ]; then + echo "$src_files" | xargs grep -l '#include.*[<"]nan\.h[>"]' 2>/dev/null | grep -q . && return 0 + return 1 + fi + + # No source files: inspect the prebuilt .node binary for nan symbols + node_bin="$(find "$pkg_dir" -name "*.node" -print -quit 2>/dev/null)" + if [ -n "$node_bin" ]; then + nm -D "$node_bin" 2>/dev/null | grep -q '_ZN3Nan' && return 0 + strings "$node_bin" 2>/dev/null | grep -q 'nan\.h' && return 0 + fi + + return 1 +} + +# Returns 0 if the package uses ABI-versioned prebuilt .node files (node-gyp-build pattern) +# These are NAN-style (not N-API) because they are keyed by Node ABI version. +# Skips packages that also have binding.gyp (already handled by uses_nan). +uses_prebuilt_nan() { + pkg_dir="$1" + + # Skip if it has binding.gyp — handled by the binding.gyp loop + [ -f "$pkg_dir/binding.gyp" ] && return 1 + + # Must have prebuilds/ with ABI-versioned .node files (node-*.node naming = NAN/ABI-specific) + find "$pkg_dir/prebuilds" -name "node-*.node" -print -quit 2>/dev/null | grep -q . || return 1 + + return 0 +} + +rebuild_pkg() { + pkg_dir="$1" + + pkg_json="$pkg_dir/package.json" + pkg_name="$(node -pe "require('$pkg_json').name" 2>/dev/null)" + pkg_version="$(node -pe "require('$pkg_json').version" 2>/dev/null)" + pkg_repo="$(node -pe "var r=require('$pkg_json').repository; typeof r==='string'?r:r&&r.url||''" 2>/dev/null | sed 's|git+||')" + + echo "Found nan addon: $pkg_name@$pkg_version" + + [ "$detect_only" = "1" ] && return 0 + + ( + cd "$pkg_dir" || exit 1 + npm install --ignore-scripts 2>/dev/null || true + + # Check if source files are present + if find . -maxdepth 4 \( -name "*.cc" -o -name "*.cpp" \) -print -quit 2>/dev/null | grep -q .; then + echo " Source present, rebuilding $pkg_name..." + if npm run rebuild; then + echo " OK: $pkg_name" + else + echo " FAILED: rebuild failed for $pkg_name" + exit 1 + fi + else + echo " Source missing for $pkg_name, fetching from $pkg_repo..." + if [ -z "$pkg_repo" ] || [ "$pkg_repo" = "undefined" ] || [ "$pkg_repo" = "null" ]; then + echo " FAILED: no repository URL for $pkg_name" + exit 1 + fi + + tmp_dir="$(mktemp -d)" + cloned=0 + for tag in "v${pkg_version}" "${pkg_version}"; do + if git clone -q --depth=1 --branch="$tag" "$pkg_repo" "$tmp_dir" 2>/dev/null; then + cloned=1 + break + fi + done + + if [ "$cloned" = "1" ]; then + [ -f "$tmp_dir/binding.gyp" ] && mv "$tmp_dir/binding.gyp" . + [ -d "$tmp_dir/bindings" ] && mv "$tmp_dir/bindings" . + rm -rf "$tmp_dir" + if npm run rebuild; then + echo " OK: $pkg_name" + else + echo " FAILED: rebuild failed for $pkg_name" + exit 1 + fi + else + rm -rf "$tmp_dir" + echo " FAILED: could not clone $pkg_repo at v$pkg_version or $pkg_version" + exit 1 + fi + fi + ) || { echo "1" > "$fail_marker"; } +} + +echo "Scanning for nan-dependent native addons in $node_modules..." + +# Pass 1: packages with binding.gyp that include nan.h +find "$node_modules" -maxdepth 3 -name "binding.gyp" | while IFS= read -r binding; do + pkg_dir="$(dirname "$binding")" + + # Skip nested node_modules + rel="${pkg_dir#$node_modules/}" + case "$rel" in + */node_modules/*) continue ;; + esac + + uses_nan "$pkg_dir" || continue + + rebuild_pkg "$pkg_dir" +done + +# Pass 2: packages using node-gyp-build with ABI-versioned prebuilts (e.g. @datadog/pprof) +# These have no binding.gyp in the installed package but ship prebuilds/*/node-.node +find "$node_modules" -maxdepth 3 -type d -name "prebuilds" | while IFS= read -r prebuilds_dir; do + pkg_dir="$(dirname "$prebuilds_dir")" + + # Skip nested node_modules + rel="${pkg_dir#$node_modules/}" + case "$rel" in + */node_modules/*) continue ;; + esac + + uses_prebuilt_nan "$pkg_dir" || continue + + rebuild_pkg "$pkg_dir" +done + +if [ -f "$fail_marker" ]; then + rm -f "$fail_marker" + echo "ERROR: one or more native addon rebuilds failed" + exit 1 +fi