From 39b5bb558608a961aa7d74c2e614934898c12c19 Mon Sep 17 00:00:00 2001 From: jblac Date: Tue, 3 Mar 2026 18:42:39 -0700 Subject: [PATCH] build: add --shared-v8 flags Add first-class --shared-v8 support using the same configure_library() pattern used by every other shared dependency in Node.js. This provides --shared-v8-includes, --shared-v8-libpath, and --shared-v8-libname flags with pkg-config fallback. When --shared-v8 is used, the bundled deps/v8/ is completely excluded from compilation. No v8 GYP targets are built, no bundled headers are referenced. The external v8 is linked via the standard shared mechanism. Configure-time validation checks the shared v8 against Node.js requirements. Hard errors on: version mismatch, v8 sandbox enabled, extensible RO snapshot enabled, pointer compression ABI mismatch (auto detected). warning on: V8_PROMISE_INTERNAL_FIELD_COUNT < 1 (async_hooks uses a slower fallback). These are the "floating patch" requirements decomposed into verifiable build flags, not source patches. Snapshot generation (node_mksnapshot) works correctly with shared v8 because it links against the Node.js library, which transitively links against whatever v8 is configured. No snapshot disabling needed. The existing --without-bundled-v8 flag is deprecated and aliased to --shared-v8 for backwards compatibility. Fixes: https://github.com/nodejs/node/issues/53509 --- BUILDING.md | 62 ++++++++++++++++++ configure.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++--- doc/api/cli.md | 1 + node.gyp | 104 +++++++++++++++++++++++------- node.gypi | 6 ++ shell.nix | 2 +- 6 files changed, 309 insertions(+), 33 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 9e6ec9908c58eb..0e97c0222179af 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1109,6 +1109,68 @@ shipping with these options to: external dependencies. There may be little or no test coverage within the Node.js project CI for these non-default options. +### Shared V8 + +Node.js can be built against a shared V8 library using the `--shared-v8` +configure flag. This completely excludes the bundled `deps/v8/` from +compilation and links against an external V8 instead. + +```console +./configure --shared-v8 \ + --shared-v8-includes=/usr/include \ + --shared-v8-libpath=/usr/lib \ + --shared-v8-libname=v8,v8_libplatform +``` + +The following flags are available: + +* `--shared-v8`: Link to a shared V8 library instead of building the + bundled copy. +* `--shared-v8-includes=`: Directory containing V8 header files + (`v8.h`, `v8-platform.h`, `v8config.h`, `v8-version.h`, etc.). +* `--shared-v8-libpath=`: Directory containing the shared V8 + library. +* `--shared-v8-libname=`: Library name(s) to link against, + comma-separated for multiple. Default: `v8,v8_libplatform`. + +The shared V8 must meet the build configuration requirements listed +below. Configure-time validation checks these automatically; hard +requirements produce errors, performance requirements produce warnings. + +#### V8 build configuration spec + +**Hard requirements** (configure errors if violated): + +| Flag | Required value | Reason | +|------|---------------|--------| +| V8 major.minor version | Must match bundled `deps/v8/` | ABI compatibility (e.g., 14.3.x) | +| `v8_enable_sandbox` | `false` | Node.js C++ backing stores are not sandbox-allocated | +| `v8_enable_extensible_ro_snapshot` | `false` | Snapshot compatibility | +| `v8_enable_pointer_compression` | Auto-detected | ABI: struct layout must match. Node.js reads the shared V8's `v8config.h` and auto-matches. | + +**Performance requirements** (configure warns if not met): + +| Flag | Recommended value | Reason | +|------|------------------|--------| +| `v8_promise_internal_field_count` | `1` | Fast async\_hooks Promise tracking. Fallback to symbol-property tracking exists when 0, but is ~2x slower for promise-heavy workloads. | + +**Recommended** (not validated): + +| Flag | Recommended value | Reason | +|------|------------------|--------| +| `v8_use_siphash` | `true` | Hash table randomization | +| `v8_enable_webassembly` | `true` | Unless building with `--v8-lite-mode` | + +**Standard V8 embedder API** (must be present in any compliant V8): + +* `v8::Context::SetPromiseHooks()` +* `v8::Isolate::SetPromiseHook()` +* `v8::Context::SetAlignedPointerInEmbedderData()` +* `v8::SnapshotCreator` / `v8::StartupData` +* `v8::ScriptCompiler::CreateCodeCache()` + +The deprecated `--without-bundled-v8` flag is aliased to `--shared-v8`. + ## Note for downstream distributors of Node.js The Node.js ecosystem is reliant on ABI compatibility within a major release. diff --git a/configure.py b/configure.py index fa47e9c48547f2..a420f66b5401f1 100755 --- a/configure.py +++ b/configure.py @@ -730,6 +730,28 @@ dest='shared_zstd_libpath', help='a directory to search for the shared zstd DLL') +shared_optgroup.add_argument('--shared-v8', + action='store_true', + dest='shared_v8', + default=None, + help='link to a shared v8 DLL instead of static linking') + +shared_optgroup.add_argument('--shared-v8-includes', + action='store', + dest='shared_v8_includes', + help='directory containing v8 header files') + +shared_optgroup.add_argument('--shared-v8-libname', + action='store', + dest='shared_v8_libname', + default='v8,v8_libplatform', + help='alternative lib name to link to [default: %(default)s]') + +shared_optgroup.add_argument('--shared-v8-libpath', + action='store', + dest='shared_v8_libpath', + help='a directory to search for the shared v8 DLL') + parser.add_argument_group(shared_optgroup) for builtin in shareable_builtins: @@ -1087,8 +1109,7 @@ action='store_true', dest='without_bundled_v8', default=False, - help='do not use V8 includes from the bundled deps folder. ' + - '(This mode is not officially supported for regular applications)') + help='DEPRECATED: Use --shared-v8 instead.') parser.add_argument('--verbose', action='store_true', @@ -2015,6 +2036,110 @@ def configure_library(lib, output, pkgname=None): output['libraries'] += pkg_libs.split() +def read_v8_version_from_header(header_path): + """Read V8 version components from v8-version.h.""" + version = {} + with open(header_path, 'r') as f: + for line in f: + for component in ('V8_MAJOR_VERSION', 'V8_MINOR_VERSION', + 'V8_BUILD_NUMBER', 'V8_PATCH_LEVEL'): + if '#define ' + component in line: + version[component] = int(line.strip().split()[-1]) + return version + +def parse_promise_field_count(header_path): + """Read V8_PROMISE_INTERNAL_FIELD_COUNT from v8-promise.h.""" + with open(header_path, 'r') as f: + for line in f: + if '#define V8_PROMISE_INTERNAL_FIELD_COUNT' in line: + # Line format: #define V8_PROMISE_INTERNAL_FIELD_COUNT + return int(line.strip().split()[-1]) + return 0 # V8 default if not defined + +def has_define_in_header(header_path, define_name): + """Check if a header file contains a #define for the given name.""" + with open(header_path, 'r') as f: + for line in f: + if f'#define {define_name}' in line: + return True + return False + +def find_shared_v8_includes(): + """Resolve shared V8 include path from --shared-v8-includes or pkg-config.""" + if options.shared_v8_includes: + return options.shared_v8_includes + (_, pkg_cflags, _, _) = pkg_config('v8') + if pkg_cflags: + for flag in pkg_cflags.split(): + if flag.startswith('-I'): + return flag[2:] + return None + +def validate_shared_v8(shared_includes): + """Validate that the shared V8 meets Node.js build configuration requirements. + Errors are fatal. Configure will not proceed with an incompatible V8.""" + + # --- Version check (hard error on major.minor mismatch) --- + bundled_header = os.path.join('deps', 'v8', 'include', 'v8-version.h') + shared_header = os.path.join(shared_includes, 'v8-version.h') + if not os.path.exists(shared_header): + alt = os.path.join(shared_includes, 'v8', 'v8-version.h') + if os.path.exists(alt): + shared_header = alt + else: + error('Could not find v8-version.h in shared V8 includes at ' + f'{shared_includes}. Cannot validate V8 compatibility. ' + 'Use --shared-v8-includes to specify the correct path.') + + bundled = read_v8_version_from_header(bundled_header) + shared = read_v8_version_from_header(shared_header) + b_ver = f"{bundled['V8_MAJOR_VERSION']}.{bundled['V8_MINOR_VERSION']}.{bundled['V8_BUILD_NUMBER']}" + s_ver = f"{shared['V8_MAJOR_VERSION']}.{shared['V8_MINOR_VERSION']}.{shared['V8_BUILD_NUMBER']}" + + if bundled['V8_MAJOR_VERSION'] != shared['V8_MAJOR_VERSION'] or \ + bundled['V8_MINOR_VERSION'] != shared['V8_MINOR_VERSION']: + error(f'Shared V8 version ({s_ver}) does not match required ' + f'({b_ver}). Major and minor version must match.') + + if bundled['V8_BUILD_NUMBER'] != shared['V8_BUILD_NUMBER']: + warn(f'Shared V8 build number ({s_ver}) differs from bundled ({b_ver}). ' + f'Build may succeed but runtime behavior may differ.') + + # --- Promise internal field count (warning, not error; fallback exists) --- + promise_header = os.path.join(shared_includes, 'v8-promise.h') + if os.path.exists(promise_header): + field_count = parse_promise_field_count(promise_header) + if field_count < 1: + warn(f'Shared V8 has V8_PROMISE_INTERNAL_FIELD_COUNT={field_count}. ' + f'async_hooks will use slower symbol-property fallback. ' + f'For best performance, rebuild V8 with ' + f'v8_promise_internal_field_count=1.') + + # --- Pointer compression: auto-detect from shared V8, set Node.js to match --- + v8config = os.path.join(shared_includes, 'v8config.h') + if os.path.exists(v8config): + shared_has_pc = has_define_in_header(v8config, 'V8_COMPRESS_POINTERS') + if shared_has_pc != bool(options.enable_pointer_compression): + # Auto-match instead of erroring. Node.js adapts to the shared V8 + options.enable_pointer_compression = shared_has_pc + warn(f'Auto-{"enabling" if shared_has_pc else "disabling"} pointer ' + f'compression to match shared V8.') + + shared_has_sandbox = has_define_in_header(v8config, 'V8_ENABLE_SANDBOX') + if shared_has_sandbox: + error('Shared V8 was built with V8_ENABLE_SANDBOX. Node.js does not ' + 'support the V8 sandbox (backing store pointers are in C++ ' + 'memory, not sandbox memory). Rebuild V8 with: ' + 'v8_enable_sandbox=false') + + # --- Extensible RO snapshot: must be disabled for snapshot compatibility --- + shared_has_ext_ro = has_define_in_header(v8config, 'V8_ENABLE_EXTENSIBLE_RO_SNAPSHOT') + if shared_has_ext_ro: + error('Shared V8 was built with V8_ENABLE_EXTENSIBLE_RO_SNAPSHOT. ' + 'Node.js requires this to be disabled for snapshot compatibility. ' + 'Rebuild V8 with: v8_enable_extensible_ro_snapshot=false') + + def configure_v8(o, configs): set_configuration_variable(configs, 'v8_enable_v8_checks', release=0, debug=1) @@ -2064,16 +2189,40 @@ def configure_v8(o, configs): o['variables']['node_enable_v8windbg'] = b(options.enable_v8windbg) if options.enable_d8: o['variables']['test_isolation_mode'] = 'noop' # Needed by d8.gyp. + if options.without_bundled_v8: + if not options.shared_v8: + warn('--without-bundled-v8 is deprecated. Use --shared-v8 instead.') + options.shared_v8 = True + + if options.shared_v8: + o['variables']['node_use_bundled_v8'] = b(False) + if options.enable_d8: - raise Exception('--enable-d8 is incompatible with --without-bundled-v8.') + error('--enable-d8 is incompatible with --shared-v8') if options.enable_v8windbg: - raise Exception('--enable-v8windbg is incompatible with --without-bundled-v8.') - (pkg_libs, pkg_cflags, pkg_libpath, _) = pkg_config("v8") - if pkg_libs and pkg_libpath: - output['libraries'] += [pkg_libpath] + pkg_libs.split() - if pkg_cflags: - output['include_dirs'] += [flag for flag in [flag.strip() for flag in pkg_cflags.split('-I')] if flag] + error('--enable-v8windbg is incompatible with --shared-v8') + + # Standard configure_library call - handles pkg-config, includes, + # libpath, libname exactly like every other shared dependency. + configure_library('v8', o) + + # Ensure the build can find the shared V8 at mksnapshot execution time + if options.shared_v8_libpath: + o['variables']['node_shared_v8_libpath'] = options.shared_v8_libpath + + # validate shared v8 meets Node.js requirements (hard errors on failure) + shared_includes = find_shared_v8_includes() + if shared_includes: + validate_shared_v8(shared_includes) + else: + warn('Could not determine shared v8 include path. ' + 'Skipping build configuration validation. ' + 'use --shared-v8-includes to enable validation.') + + else: + o['variables']['node_use_bundled_v8'] = b(True) + if options.static_zoslib_gyp: o['variables']['static_zoslib_gyp'] = options.static_zoslib_gyp if flavor != 'linux' and options.v8_enable_hugepage: diff --git a/doc/api/cli.md b/doc/api/cli.md index 15dcce10ce71b2..a3f3df5826621d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -4261,6 +4261,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [context-aware]: addons.md#context-aware-addons [debugger]: debugger.md [debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications +[Building to use shared dependencies at runtime]: ../../BUILDING.md#building-to-use-shared-dependencies-at-runtime [deprecation warnings]: deprecations.md#list-of-deprecated-apis [emit_warning]: process.md#processemitwarningwarning-options [environment_variables]: #environment-variables_1 diff --git a/node.gyp b/node.gyp index 2dd7eb1af5865a..fd407eaea3117b 100644 --- a/node.gyp +++ b/node.gyp @@ -26,6 +26,8 @@ 'node_shared_sqlite%': 'false', 'node_shared_temporal_capi%': 'false', 'node_shared_uvwasi%': 'false', + 'node_shared_v8%': 'false', + 'node_shared_v8_libpath%': '', 'node_shared_zlib%': 'false', 'node_shared_zstd%': 'false', 'node_shared%': 'false', @@ -53,22 +55,28 @@ '<@(linked_module_files)', ], 'deps_files': [ - 'deps/v8/tools/splaytree.mjs', - 'deps/v8/tools/codemap.mjs', - 'deps/v8/tools/consarray.mjs', - 'deps/v8/tools/csvparser.mjs', - 'deps/v8/tools/profile.mjs', - 'deps/v8/tools/profile_view.mjs', - 'deps/v8/tools/logreader.mjs', - 'deps/v8/tools/arguments.mjs', - 'deps/v8/tools/tickprocessor.mjs', - 'deps/v8/tools/sourcemap.mjs', - 'deps/v8/tools/tickprocessor-driver.mjs', 'deps/acorn/acorn/dist/acorn.js', 'deps/acorn/acorn-walk/dist/walk.js', 'deps/minimatch/index.js', '<@(node_builtin_shareable_builtins)', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'deps_files': [ + 'deps/v8/tools/splaytree.mjs', + 'deps/v8/tools/codemap.mjs', + 'deps/v8/tools/consarray.mjs', + 'deps/v8/tools/csvparser.mjs', + 'deps/v8/tools/profile.mjs', + 'deps/v8/tools/profile_view.mjs', + 'deps/v8/tools/logreader.mjs', + 'deps/v8/tools/arguments.mjs', + 'deps/v8/tools/tickprocessor.mjs', + 'deps/v8/tools/sourcemap.mjs', + 'deps/v8/tools/tickprocessor-driver.mjs', + ] + }], + ], 'node_sources': [ 'src/api/async_resource.cc', 'src/api/callback.cc', @@ -579,9 +587,13 @@ 'include_dirs': [ 'src', - 'deps/v8/include', 'deps/postject' ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'sources': [ 'src/node_main.cc' @@ -897,11 +909,14 @@ 'dependencies': [ 'node_js2c#host', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'dependencies': ['tools/v8_gypfiles/abseil.gyp:abseil'], + }], + ], 'sources': [ '<@(node_sources)', - # Dependency headers - 'deps/v8/include/v8.h', 'deps/postject/postject-api.h', # javascript files to make for an even more pleasant IDE experience '<@(library_files)', @@ -909,6 +924,11 @@ # node.gyp is added by default, common.gypi is added for change detection 'common.gypi', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'sources': ['deps/v8/include/v8.h'], + }], + ], 'variables': { 'openssl_system_ca_path%': '', @@ -1130,11 +1150,15 @@ 'include_dirs': [ 'src', 'tools/msvs/genfiles', - 'deps/v8/include', 'deps/cares/include', 'deps/uv/include', 'test/cctest', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'defines': [ 'NODE_ARCH="<(target_arch)"', @@ -1176,11 +1200,15 @@ 'include_dirs': [ 'src', 'tools/msvs/genfiles', - 'deps/v8/include', 'deps/cares/include', 'deps/uv/include', 'test/cctest', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'defines': [ 'NODE_ARCH="<(target_arch)"', 'NODE_PLATFORM="<(OS)"', @@ -1225,11 +1253,15 @@ 'include_dirs': [ 'src', 'tools/msvs/genfiles', - 'deps/v8/include', 'deps/cares/include', 'deps/uv/include', 'test/cctest', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'defines': [ 'NODE_ARCH="<(target_arch)"', 'NODE_PLATFORM="<(OS)"', @@ -1279,6 +1311,11 @@ 'dependencies': [ '<(node_lib_target_name)', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'dependencies': ['tools/v8_gypfiles/abseil.gyp:abseil'], + }], + ], 'includes': [ 'node.gypi' @@ -1287,11 +1324,15 @@ 'include_dirs': [ 'src', 'tools/msvs/genfiles', - 'deps/v8/include', 'deps/cares/include', 'deps/uv/include', 'test/cctest', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'defines': [ 'NODE_ARCH="<(target_arch)"', @@ -1401,11 +1442,15 @@ 'src', 'tools', 'tools/msvs/genfiles', - 'deps/v8/include', 'deps/cares/include', 'deps/uv/include', 'test/embedding', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'sources': [ 'src/node_snapshot_stub.cc', @@ -1459,8 +1504,10 @@ # Don't depend on node.gypi - it otherwise links to # the static libraries and resolve symbols at build time. - 'include_dirs': [ - 'deps/v8/include', + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], ], 'sources': [ @@ -1588,10 +1635,14 @@ 'include_dirs': [ 'src', 'tools/msvs/genfiles', - 'deps/v8/include', 'deps/cares/include', 'deps/uv/include', ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], + ], 'defines': [ 'NODE_WANT_INTERNALS=1' ], @@ -1649,6 +1700,9 @@ ['enable_lto=="true"', { 'ldflags': [ '-fno-lto' ], }], + ['node_shared_v8=="true" and OS!="win" and node_shared_v8_libpath!=""', { + 'ldflags': ['-Wl,-rpath,<(node_shared_v8_libpath)'], + }], ], }, # node_mksnapshot ], # end targets @@ -1668,7 +1722,11 @@ 'dependencies': ['<(node_lib_target_name)'], 'include_dirs': [ 'src', - 'deps/v8/include', + ], + 'conditions': [ + ['node_shared_v8=="false"', { + 'include_dirs': ['deps/v8/include'], + }], ], 'sources': [ '<@(library_files)', diff --git a/node.gypi b/node.gypi index 559330fba8b1ca..4c29a0a885c565 100644 --- a/node.gypi +++ b/node.gypi @@ -96,6 +96,12 @@ 'tools/v8_gypfiles/v8.gyp:v8_libplatform', ], }], + [ 'node_shared_v8=="false"', { + 'dependencies': [ + 'tools/v8_gypfiles/v8.gyp:v8_snapshot', + 'tools/v8_gypfiles/v8.gyp:v8_libplatform', + ], + }], [ 'node_use_v8_platform=="true"', { 'defines': [ 'NODE_USE_V8_PLATFORM=1', diff --git a/shell.nix b/shell.nix index c531642893b8ff..d043989e9bcbf2 100644 --- a/shell.nix +++ b/shell.nix @@ -107,7 +107,7 @@ pkgs.mkShell { BUILD_WITH = if (ninja != null) then "ninja" else "make"; NINJA = pkgs.lib.optionalString (ninja != null) "${pkgs.lib.getExe ninja}"; CONFIG_FLAGS = builtins.toString ( - configureFlags ++ pkgs.lib.optional (useSeparateDerivationForV8 != false) "--without-bundled-v8" + configureFlags ++ pkgs.lib.optional (useSeparateDerivationForV8 != false) "--shared-v8" ); NOSQLITE = pkgs.lib.optionalString (!withSQLite) "1"; }