diff --git a/.evergreen.yml b/.evergreen.yml index a0edaa6..5aa9589 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -14,8 +14,13 @@ functions: set -e set -x - export NODE_VERSION=20.13.0 - bash .evergreen/install-node.sh + if [ "$OS" == "Windows_NT" ]; then + export NODE_GYP_FORCE_PYTHON="C:\python\Python311\python.exe" + export PATH="/cygdrive/c/python/Python311/Scripts:/cygdrive/c/python/Python311:$PATH" + fi + + export NODE_VERSION=22.17.0 + . .evergreen/install-node.sh install: - command: shell.exec params: @@ -25,6 +30,11 @@ functions: set -e set -x + if [ "$OS" == "Windows_NT" ]; then + export NODE_GYP_FORCE_PYTHON="C:\python\Python311\python.exe" + export PATH="/cygdrive/c/python/Python311/Scripts:/cygdrive/c/python/Python311:$PATH" + fi + . .evergreen/use-node.sh npm install check: @@ -32,10 +42,17 @@ functions: params: working_dir: src shell: bash + env: + NODE_VERSION: ${node_version} script: | set -e set -x + if [ "$OS" == "Windows_NT" ]; then + export NODE_GYP_FORCE_PYTHON="C:\python\Python311\python.exe" + export PATH="/cygdrive/c/python/Python311/Scripts:/cygdrive/c/python/Python311:$PATH" + fi + . .evergreen/use-node.sh npm run build npm run lint @@ -51,10 +68,29 @@ functions: AZURE_TEST_CONFIG: ${azure_test_config} AZURE_TEST_CREDENTIALS: ${azure_test_credentials} DISTRO_ID: ${distro_id} + BOXEDNODE_DCHECKS_ENABLED: ${dchecks_enabled} script: | set -e set -x + if [ `uname` == "Darwin" ]; then + # the CI macOS machines have an outdated Clang that + # cannot build recent Node.js versions, so we use + # the LLVM version installed via Homebrew + # (both on arm64 and x64) + + LLVM_PREFIX="$(brew --prefix llvm)" + export PATH="$LLVM_PREFIX/bin:$PATH" + export CC="$LLVM_PREFIX/bin/clang" + export CXX="$LLVM_PREFIX/bin/clang++" + export LDFLAGS="-L$LLVM_PREFIX/lib -L$LLVM_PREFIX/lib/c++ -L$LLVM_PREFIX/lib/unwind" + export CPPFLAGS="-I$LLVM_PREFIX/include" + export CMAKE_PREFIX_PATH="$LLVM_PREFIX" + + $CC --version + $CXX --version + fi + rm -rf /tmp/m && mkdir -pv /tmp/m # Node.js compilation can fail on long path prefixes trap "rm -rf /tmp/m" EXIT export TMP=/tmp/m @@ -64,49 +100,81 @@ functions: # able to compile OpenSSL with assembly support, # so we revert back to the slower version. if [ "$OS" == "Windows_NT" ]; then - export PATH="/cygdrive/c/python/Python310/Scripts:/cygdrive/c/python/Python310:/cygdrive/c/Python310/Scripts:/cygdrive/c/Python310:$PATH" + export NODE_GYP_FORCE_PYTHON="C:\python\Python311\python.exe" + export PATH="/cygdrive/c/python/Python311/Scripts:/cygdrive/c/python/Python311:$PATH" export BOXEDNODE_CONFIGURE_ARGS='openssl-no-asm' elif uname -a | grep -q 'Darwin.*x86_64'; then export BOXEDNODE_CONFIGURE_ARGS='--openssl-no-asm' fi . .evergreen/use-node.sh - npm run build - TEST_NODE_VERSION="$TEST_NODE_VERSION" npm run test-ci + export TEST_NODE_VERSION="$TEST_NODE_VERSION" + if [ "$OS" == "Windows_NT" ]; then + # The CI machines we have for Windows don't have a working + # installation of VS2022, (vswhere.exe can't find the correct path) + # So we run the scripts to set up the environment manually, and they + # only work properly in CMD + WIN_PATH=$(cygpath -aw .evergreen/test-in-vcdev-env.bat) + cmd.exe /c "$WIN_PATH" + else + npm run build + npm run test-ci + fi tasks: - - name: test_n14 + - name: test_n20 commands: - func: checkout - func: install_node - func: install - func: test vars: - node_version: "14.21.3" - - name: test_n16 + node_version: "20.19.5" + - name: test_n22 commands: - func: checkout - func: install_node - func: install - func: test vars: - node_version: "16.20.1" - - name: test_n18 + node_version: "22.21.0" + - name: test_n24 commands: - func: checkout - func: install_node - func: install - func: test vars: - node_version: "18.17.0" - - name: test_n20 + node_version: "24.10.0" + + - name: test_n20_dchecks + commands: + - func: checkout + - func: install_node + - func: install + - func: test + vars: + node_version: "20.19.5" + dchecks_enabled: 1 + - name: test_n22_dchecks commands: - func: checkout - func: install_node - func: install - func: test vars: - node_version: "20.13.0" + node_version: "22.21.0" + dchecks_enabled: 1 + - name: test_n24_dchecks + commands: + - func: checkout + - func: install_node + - func: install + - func: test + vars: + node_version: "24.10.0" + dchecks_enabled: 1 + - name: check commands: - func: checkout @@ -119,32 +187,35 @@ buildvariants: display_name: 'Ubuntu 20.04 x64' run_on: ubuntu2004-large tasks: - - test_n14 - - test_n16 - - test_n18 - test_n20 + - test_n22 + - test_n24 - check + - name: ubuntu_arm64_dchecks + display_name: 'Ubuntu 24.04 arm64 (Node.js/V8 DCHECKs)' + run_on: ubuntu2404-arm64-latest-xlarge + tasks: + - test_n20_dchecks + - test_n22_dchecks + - test_n24_dchecks - name: macos_x64_test - display_name: 'macOS 11.00 x64' - run_on: macos-1100 + display_name: 'macOS 14.00 x64' + run_on: macos-14 tasks: - - test_n14 - - test_n16 - - test_n18 - test_n20 + - test_n22 + - test_n24 - name: macos_arm64_test - display_name: 'macOS 11.00 arm64' - run_on: macos-1100-arm64 + display_name: 'macOS 14.00 arm64' + run_on: macos-14-arm64 tasks: - - test_n14 - - test_n16 - - test_n18 - test_n20 + - test_n22 + - test_n24 - name: windows_x64_test display_name: 'Windows x64' - run_on: windows-vsCurrent-xlarge + run_on: windows-2022-xlarge tasks: - - test_n14 - - test_n16 - - test_n18 - test_n20 + - test_n22 + - test_n24 diff --git a/.evergreen/install-node.sh b/.evergreen/install-node.sh index 8a35449..ec5627e 100644 --- a/.evergreen/install-node.sh +++ b/.evergreen/install-node.sh @@ -3,11 +3,14 @@ set -e set -x +# so we use the devtools binaries first (for gcc/g++) +export PATH="/opt/devtools/bin:$PATH" + export BASEDIR="$PWD" mkdir -p .deps cd .deps -NVM_URL="https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh" +NVM_URL="https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh" # this needs to be explicitly exported for the nvm install below export NVM_DIR="$PWD/nvm" @@ -21,12 +24,18 @@ if [[ "$OS" == "Windows_NT" ]]; then mv -v node-v$NODE_VERSION-win-x64/* node/bin chmod a+x node/bin/* export PATH="$PWD/node/bin:$PATH" -# install Node.js on Linux/MacOS else - curl -o- $NVM_URL | bash - set +x - [ -s "${NVM_DIR}/nvm.sh" ] && source "${NVM_DIR}/nvm.sh" - nvm install --no-progress "$NODE_VERSION" + if [ "$(uname -s)" == "Darwin" ] ; then # install Node.js on MacOS + curl -o- $NVM_URL | bash + set +x + [ -s "${NVM_DIR}/nvm.sh" ] && source "${NVM_DIR}/nvm.sh" + nvm install --no-progress "$NODE_VERSION" + else # Linux already has its own toolchain in evergreen + mkdir -p node + NODE_MAJOR=$(echo $NODE_VERSION | awk -F . '{print $1}') + ln -s "/opt/devtools/node$NODE_MAJOR/bin/" "$PWD/node/bin" + export PATH="$PWD/node/bin:$PATH" + fi fi which node && node -v || echo "node not found, PATH=$PATH" diff --git a/.evergreen/test-in-vcdev-env.bat b/.evergreen/test-in-vcdev-env.bat new file mode 100644 index 0000000..56232e7 --- /dev/null +++ b/.evergreen/test-in-vcdev-env.bat @@ -0,0 +1,6 @@ +CALL "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvars64.bat" +REM APPDATA is empty in CMD, and npm requires it to be a valid path +SET APPDATA="npm-cache" + +CALL npm run build +CALL npm run test-ci diff --git a/.evergreen/use-node.sh b/.evergreen/use-node.sh index b657e8c..d7c5082 100644 --- a/.evergreen/use-node.sh +++ b/.evergreen/use-node.sh @@ -1,8 +1,16 @@ if [[ "$OS" == "Windows_NT" ]]; then - export PATH="$PWD/.deps/node/bin:$PATH" + export APPDATA="npm-cache" + export PATH="$PWD/.deps/node/bin:$PATH" else - export NVM_DIR="$PWD/.deps/nvm" - [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" + # so we use the devtools binaries first (for gcc/g++) + export PATH="/opt/devtools/bin:$PATH" + + if [ "$(uname -s)" == "Darwin" ] ; then # in OSX use nvm + export NVM_DIR="$PWD/.deps/nvm" + [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" + else # In Linux, use .deps/node/bin because it was set up with symlink to an existing node in the toolchain + export PATH="$PWD/.deps/node/bin:$PATH" + fi fi echo "updated PATH=$PATH" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 574fcf1..3ad2497 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,13 +42,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Use Node.js v18.x + - name: Use Node.js v24.x if: matrix.language == 'cpp' - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 18.x + node-version: 24.x - - name: Use Node.js v18.x + - name: Use Node.js v24.x if: matrix.language == 'cpp' run: | npm install diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 293c59b..96f4400 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,3 +1,7 @@ +# This is a generated file. Please change .github/workflows/nodejs.yml.in +# and run the following command to update the GHA Workflow +# $> npm run update-gha-workflow +# -------------------- on: [pull_request] name: CI @@ -13,19 +17,55 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - node-version: [14.x, 16.x, 18.x, 20.x] + node-version: [20.x, 22.x, 24.x] + test-to-run: ["works in a simple case","works with a Nan addon","works with a N-API addon","passes through env vars and runs the pre-compile hook","works with code caching support","works with snapshot support (compressBlobs = "] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v6 with: check-latest: true node-version: ${{ matrix.node-version }} - - name: Install npm@8.x - if: ${{ matrix.node-version == '14.x' }} - run: npm install -g npm@8.x - name: Install Dependencies run: npm install + - name: Lint + run: npm run lint + - name: Build + run: npm run build - name: Test - run: npm test + run: npm run test-ci -- -f "${{ matrix.test-to-run }}" + + test-windows: + name: Windows tests + strategy: + fail-fast: false + matrix: + os: [windows-latest] + node-version: [20.x, 22.x, 24.x] + vs-version: ['17'] # 17 => VS2022 + test-to-run: ["works in a simple case","works with a Nan addon","works with a N-API addon","passes through env vars and runs the pre-compile hook","works with code caching support","works with snapshot support (compressBlobs = "] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + check-latest: true + node-version: ${{ matrix.node-version }} + - uses: microsoft/setup-msbuild@v2 + name: Setup MSBuild + with: + vs-version: ${{ matrix.vs-version }} + msbuild-architecture: x64 + - name: Install Dependencies + run: npm install + - name: Lint + run: npm run lint + - name: Build + run: npm run build + - name: Test + run: npm run test-ci -- -f "${{ matrix.test-to-run }}" + env: + BOXEDNODE_MAKE_ARGS: "debug" + diff --git a/.github/workflows/nodejs.yml.in b/.github/workflows/nodejs.yml.in new file mode 100644 index 0000000..579c3ce --- /dev/null +++ b/.github/workflows/nodejs.yml.in @@ -0,0 +1,67 @@ +on: [pull_request] + +name: CI + +defaults: + run: + shell: bash + +jobs: + test-posix: + name: Unix tests + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node-version: [20.x, 22.x, 24.x] + test-to-run: <> + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + check-latest: true + node-version: ${{ matrix.node-version }} + - name: Install Dependencies + run: npm install + - name: Lint + run: npm run lint + - name: Build + run: npm run build + - name: Test + run: npm run test-ci -- -f "${{ matrix.test-to-run }}" + + test-windows: + name: Windows tests + strategy: + fail-fast: false + matrix: + os: [windows-latest] + node-version: [20.x, 22.x, 24.x] + vs-version: ['17'] # 17 => VS2022 + test-to-run: <> + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + check-latest: true + node-version: ${{ matrix.node-version }} + - uses: microsoft/setup-msbuild@v2 + name: Setup MSBuild + with: + vs-version: ${{ matrix.vs-version }} + msbuild-architecture: x64 + - name: Install Dependencies + run: npm install + - name: Lint + run: npm run lint + - name: Build + run: npm run build + - name: Test + run: npm run test-ci -- -f "${{ matrix.test-to-run }}" + env: + BOXEDNODE_MAKE_ARGS: "debug" + diff --git a/package.json b/package.json index 7d7f769..85f65f6 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "boxednode": "bin/boxednode.js" }, "engines": { - "node": ">= 12.4.0" + "node": ">= 20.19.5" }, "scripts": { "lint": "eslint **/*.ts bin/*.js", @@ -19,7 +19,8 @@ "test-ci": "nyc mocha --colors -r ts-node/register test/*.ts", "build": "npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", "prepack": "npm run build", - "compile-ts": "tsc -p tsconfig.json" + "compile-ts": "tsc -p tsconfig.json", + "update-gha-workflow": "./scripts/update-gha-workflow.sh" }, "keywords": [ "node.js", @@ -55,7 +56,7 @@ "nyc": "^15.1.0", "ts-node": "^10.8.1", "typescript": "^4.0.3", - "weak-napi": "2.0.2" + "weak-napi": "node-ffi-napi/weak-napi#5449c78739d69aa286e43c7baf9819440b4544bb" }, "dependencies": { "@pkgjs/nv": "^0.2.1", @@ -63,10 +64,9 @@ "cli-progress": "^3.8.2", "gyp-parser": "^1.0.4", "node-fetch": "^2.6.1", - "node-gyp": "^9.0.0", + "node-gyp": "^11.5.0", "pkg-up": "^3.1.0", - "rimraf": "^3.0.2", - "semver": "^7.3.2", + "rimraf": "^6.1.0", "tar": "^6.0.5", "yargs": "^16.0.3" } diff --git a/resources/main-template.cc b/resources/main-template.cc index c63b118..88720c6 100644 --- a/resources/main-template.cc +++ b/resources/main-template.cc @@ -23,46 +23,11 @@ using namespace node; using namespace v8; -// 18.11.0 is the minimum version that has https://github.com/nodejs/node/pull/44121 -#if !NODE_VERSION_AT_LEAST(18, 11, 0) -#define USE_OWN_LEGACY_PROCESS_INITIALIZATION 1 -#endif - -// 20.0.0 will have https://github.com/nodejs/node/pull/45888, possibly the PR -// will be backported to older versions but for now this is the one where we -// can be sure of its presence. -#if NODE_VERSION_AT_LEAST(20, 0, 0) -#define NODE_VERSION_SUPPORTS_EMBEDDER_SNAPSHOT 1 -#endif - -// 20.13.0 has https://github.com/nodejs/node/pull/52595 for better startup snapshot -// initialization performance. -#if NODE_VERSION_AT_LEAST(20, 13, 0) -#define NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT 1 -#endif - // Snapshot config is supported since https://github.com/nodejs/node/pull/50453 -#if NODE_VERSION_AT_LEAST(20, 12, 0) && !defined(BOXEDNODE_SNAPSHOT_CONFIG_FLAGS) +#ifndef BOXEDNODE_SNAPSHOT_CONFIG_FLAGS #define BOXEDNODE_SNAPSHOT_CONFIG_FLAGS (SnapshotFlags::kWithoutCodeCache) #endif -// 18.1.0 is the current minimum version that has https://github.com/nodejs/node/pull/42809, -// which introduced crashes when using workers, and later 18.9.0 is the current -// minimum version to contain https://github.com/nodejs/node/pull/44252, which -// introcued crashes when using the vm module. -// We should be able to remove this restriction again once Node.js stops relying -// on global state for determining whether snapshots are enabled or not -// (after https://github.com/nodejs/node/pull/45888, hopefully). -#if NODE_VERSION_AT_LEAST(18, 1, 0) && !defined(NODE_VERSION_SUPPORTS_EMBEDDER_SNAPSHOT) -#define PASS_NO_NODE_SNAPSHOT_OPTION 1 -#endif - -#ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION -namespace boxednode { -void InitializeOncePerProcess(); -void TearDownOncePerProcess(); -} -#endif namespace boxednode { namespace { struct TimingEntry { @@ -88,9 +53,7 @@ void MarkTime(const char* category, const char* label) { Local GetBoxednodeMainScriptSource(Isolate* isolate); Local GetBoxednodeCodeCacheBuffer(Isolate* isolate); std::vector GetBoxednodeSnapshotBlobVector(); -#ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT std::optional GetBoxednodeSnapshotBlobSV(); -#endif void GetTimingData(const FunctionCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); @@ -187,16 +150,14 @@ static int RunNodeInstance(MultiIsolatePlatform* platform, platform, &errors, args, - exec_args -#ifdef BOXEDNODE_SNAPSHOT_CONFIG_FLAGS - , SnapshotConfig { BOXEDNODE_SNAPSHOT_CONFIG_FLAGS, std::nullopt } -#endif + exec_args, + SnapshotConfig { BOXEDNODE_SNAPSHOT_CONFIG_FLAGS, std::nullopt } ); Isolate* isolate = setup->isolate(); + Locker locker(isolate); { - Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); @@ -240,13 +201,10 @@ static int RunNodeInstance(MultiIsolatePlatform* platform, ArrayBufferAllocator::Create(); #ifdef BOXEDNODE_CONSUME_SNAPSHOT - assert(EmbedderSnapshotData::CanUseCustomSnapshotPerIsolate()); node::EmbedderSnapshotData::Pointer snapshot_blob; -#ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT if (const auto snapshot_blob_sv = boxednode::GetBoxednodeSnapshotBlobSV()) { snapshot_blob = EmbedderSnapshotData::FromBlob(snapshot_blob_sv.value()); } -#endif if (!snapshot_blob) { std::vector snapshot_blob_vec = boxednode::GetBoxednodeSnapshotBlobVector(); boxednode::MarkTime("Node.js Instance", "Decoded snapshot"); @@ -388,8 +346,14 @@ static int RunNodeInstance(MultiIsolatePlatform* platform, platform->AddIsolateFinishedCallback(isolate, [](void* data) { *static_cast(data) = true; }, &platform_finished); - platform->UnregisterIsolate(isolate); + + // https://github.com/nodejs/node/commit/5d3e1b555c0902db1e99577a3429cffedcf3bbdc +#if NODE_VERSION_AT_LEAST(24, 0, 0) + platform->DisposeIsolate(isolate); +#else isolate->Dispose(); + platform->UnregisterIsolate(isolate); +#endif // Wait until the platform has cleaned up all relevant resources. while (!platform_finished) @@ -409,22 +373,10 @@ static int BoxednodeMain(std::vector args) { if (args.size() > 0) { args.insert(args.begin() + 1, "--"); -#ifdef PASS_NO_NODE_SNAPSHOT_OPTION - args.insert(args.begin() + 1, "--no-node-snapshot"); -#endif } // Parse Node.js CLI options, and print any errors that have occurred while // trying to parse them. -#ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION - boxednode::InitializeOncePerProcess(); - int exit_code = node::InitializeNodeWithArgs(&args, &exec_args, &errors); - for (const std::string& error : errors) - fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); - if (exit_code != 0) { - return exit_code; - } -#else #if OPENSSL_VERSION_MAJOR >= 3 if (args.size() > 1) args.insert(args.begin() + 1, "--openssl-shared-config"); @@ -443,7 +395,6 @@ static int BoxednodeMain(std::vector args) { } args = result->args(); exec_args = result->exec_args(); -#endif #ifdef BOXEDNODE_CONSUME_SNAPSHOT if (args.size() > 0) { @@ -465,13 +416,8 @@ static int BoxednodeMain(std::vector args) { int ret = RunNodeInstance(platform.get(), args, exec_args); V8::Dispose(); -#ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION - V8::ShutdownPlatform(); - boxednode::TearDownOncePerProcess(); -#else V8::DisposePlatform(); node::TearDownOncePerProcess(); -#endif return ret; } @@ -514,386 +460,6 @@ int main(int argc, char** argv) { } #endif -// The code below is mostly lifted directly from node.cc -#ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION - -#if defined(__APPLE__) || defined(__linux__) || defined(_WIN32) -#define NODE_USE_V8_WASM_TRAP_HANDLER 1 -#else -#define NODE_USE_V8_WASM_TRAP_HANDLER 0 -#endif - -#if NODE_USE_V8_WASM_TRAP_HANDLER -#if defined(_WIN32) -#include "v8-wasm-trap-handler-win.h" -#else -#include -#include "v8-wasm-trap-handler-posix.h" -#endif -#endif // NODE_USE_V8_WASM_TRAP_HANDLER - -#if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32) -static PVOID old_vectored_exception_handler; -#endif - -#if defined(_MSC_VER) -#include -#include -#define STDIN_FILENO 0 -#else -#include -#include // getrlimit, setrlimit -#include // tcgetattr, tcsetattr -#include // STDIN_FILENO, STDERR_FILENO -#endif - -#include -#include - -namespace boxednode { - -#if HAVE_OPENSSL -static void CheckEntropy() { - for (;;) { - int status = RAND_status(); - assert(status >= 0); // Cannot fail. - if (status != 0) - break; - - // Give up, RAND_poll() not supported. - if (RAND_poll() == 0) - break; - } -} - -static bool EntropySource(unsigned char* buffer, size_t length) { - // Ensure that OpenSSL's PRNG is properly seeded. - CheckEntropy(); - // RAND_bytes() can return 0 to indicate that the entropy data is not truly - // random. That's okay, it's still better than V8's stock source of entropy, - // which is /dev/urandom on UNIX platforms and the current time on Windows. - return RAND_bytes(buffer, length) != -1; -} -#endif - -void ResetStdio(); - -#ifdef __POSIX__ -static constexpr unsigned kMaxSignal = 32; - -typedef void (*sigaction_cb)(int signo, siginfo_t* info, void* ucontext); - -void SignalExit(int signo, siginfo_t* info, void* ucontext) { - ResetStdio(); - raise(signo); -} -#endif - -#if NODE_USE_V8_WASM_TRAP_HANDLER -#if defined(_WIN32) -static LONG TrapWebAssemblyOrContinue(EXCEPTION_POINTERS* exception) { - if (v8::TryHandleWebAssemblyTrapWindows(exception)) { - return EXCEPTION_CONTINUE_EXECUTION; - } - return EXCEPTION_CONTINUE_SEARCH; -} -#else -static std::atomic previous_sigsegv_action; - -void TrapWebAssemblyOrContinue(int signo, siginfo_t* info, void* ucontext) { - if (!v8::TryHandleWebAssemblyTrapPosix(signo, info, ucontext)) { - sigaction_cb prev = previous_sigsegv_action.load(); - if (prev != nullptr) { - prev(signo, info, ucontext); - } else { - // Reset to the default signal handler, i.e. cause a hard crash. - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = SIG_DFL; - int ret = sigaction(signo, &sa, nullptr); - assert(ret == 0); - - ResetStdio(); - raise(signo); - } - } -} -#endif // defined(_WIN32) -#endif // NODE_USE_V8_WASM_TRAP_HANDLER - -#ifdef __POSIX__ -void RegisterSignalHandler(int signal, - sigaction_cb handler, - bool reset_handler) { - assert(handler != nullptr); -#if NODE_USE_V8_WASM_TRAP_HANDLER - if (signal == SIGSEGV) { - assert(previous_sigsegv_action.is_lock_free()); - assert(!reset_handler); - previous_sigsegv_action.store(handler); - return; - } -#endif // NODE_USE_V8_WASM_TRAP_HANDLER - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_sigaction = handler; - sa.sa_flags = reset_handler ? SA_RESETHAND : 0; - sigfillset(&sa.sa_mask); - int ret = sigaction(signal, &sa, nullptr); - assert(ret == 0); -} -#endif // __POSIX__ - -#ifdef __POSIX__ -static struct { - int flags; - bool isatty; - struct stat stat; - struct termios termios; -} stdio[1 + STDERR_FILENO]; -#endif // __POSIX__ - - -inline void PlatformInit() { -#ifdef __POSIX__ -#if HAVE_INSPECTOR - sigset_t sigmask; - sigemptyset(&sigmask); - sigaddset(&sigmask, SIGUSR1); - const int err = pthread_sigmask(SIG_SETMASK, &sigmask, nullptr); -#endif // HAVE_INSPECTOR - - // Make sure file descriptors 0-2 are valid before we start logging anything. - for (auto& s : stdio) { - const int fd = &s - stdio; - if (fstat(fd, &s.stat) == 0) - continue; - // Anything but EBADF means something is seriously wrong. We don't - // have to special-case EINTR, fstat() is not interruptible. - if (errno != EBADF) - assert(0); - if (fd != open("/dev/null", O_RDWR)) - assert(0); - if (fstat(fd, &s.stat) != 0) - assert(0); - } - -#if HAVE_INSPECTOR - CHECK_EQ(err, 0); -#endif // HAVE_INSPECTOR - - // TODO(addaleax): NODE_SHARED_MODE does not really make sense here. -#ifndef NODE_SHARED_MODE - // Restore signal dispositions, the parent process may have changed them. - struct sigaction act; - memset(&act, 0, sizeof(act)); - - // The hard-coded upper limit is because NSIG is not very reliable; on Linux, - // it evaluates to 32, 34 or 64, depending on whether RT signals are enabled. - // Counting up to SIGRTMIN doesn't work for the same reason. - for (unsigned nr = 1; nr < kMaxSignal; nr += 1) { - if (nr == SIGKILL || nr == SIGSTOP) - continue; - act.sa_handler = (nr == SIGPIPE || nr == SIGXFSZ) ? SIG_IGN : SIG_DFL; - int ret = sigaction(nr, &act, nullptr); - assert(ret == 0); - } -#endif // !NODE_SHARED_MODE - - // Record the state of the stdio file descriptors so we can restore it - // on exit. Needs to happen before installing signal handlers because - // they make use of that information. - for (auto& s : stdio) { - const int fd = &s - stdio; - int err; - - do - s.flags = fcntl(fd, F_GETFL); - while (s.flags == -1 && errno == EINTR); // NOLINT - assert(s.flags != -1); - - if (uv_guess_handle(fd) != UV_TTY) continue; - s.isatty = true; - - do - err = tcgetattr(fd, &s.termios); - while (err == -1 && errno == EINTR); // NOLINT - assert(err == 0); - } - - RegisterSignalHandler(SIGINT, SignalExit, true); - RegisterSignalHandler(SIGTERM, SignalExit, true); - -#if NODE_USE_V8_WASM_TRAP_HANDLER -#if defined(_WIN32) - { - constexpr ULONG first = TRUE; - old_vectored_exception_handler = - AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue); - } -#else - // Tell V8 to disable emitting WebAssembly - // memory bounds checks. This means that we have - // to catch the SIGSEGV in TrapWebAssemblyOrContinue - // and pass the signal context to V8. - { - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_sigaction = TrapWebAssemblyOrContinue; - sa.sa_flags = SA_SIGINFO; - int ret = sigaction(SIGSEGV, &sa, nullptr); - assert(ret == 0); - } -#endif // defined(_WIN32) - V8::EnableWebAssemblyTrapHandler(false); -#endif // NODE_USE_V8_WASM_TRAP_HANDLER - - // Raise the open file descriptor limit. - struct rlimit lim; - if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) { - // Do a binary search for the limit. - rlim_t min = lim.rlim_cur; - rlim_t max = 1 << 20; - // But if there's a defined upper bound, don't search, just set it. - if (lim.rlim_max != RLIM_INFINITY) { - min = lim.rlim_max; - max = lim.rlim_max; - } - do { - lim.rlim_cur = min + (max - min) / 2; - if (setrlimit(RLIMIT_NOFILE, &lim)) { - max = lim.rlim_cur; - } else { - min = lim.rlim_cur; - } - } while (min + 1 < max); - } -#endif // __POSIX__ -#ifdef _WIN32 - for (int fd = 0; fd <= 2; ++fd) { - auto handle = reinterpret_cast(_get_osfhandle(fd)); - if (handle == INVALID_HANDLE_VALUE || - GetFileType(handle) == FILE_TYPE_UNKNOWN) { - // Ignore _close result. If it fails or not depends on used Windows - // version. We will just check _open result. - _close(fd); - if (fd != _open("nul", _O_RDWR)) - assert(0); - } - } -#endif // _WIN32 -} - - -// Safe to call more than once and from signal handlers. -void ResetStdio() { - uv_tty_reset_mode(); -#ifdef __POSIX__ - for (auto& s : stdio) { - const int fd = &s - stdio; - - struct stat tmp; - if (-1 == fstat(fd, &tmp)) { - assert(errno == EBADF); // Program closed file descriptor. - continue; - } - - bool is_same_file = - (s.stat.st_dev == tmp.st_dev && s.stat.st_ino == tmp.st_ino); - if (!is_same_file) continue; // Program reopened file descriptor. - - int flags; - do - flags = fcntl(fd, F_GETFL); - while (flags == -1 && errno == EINTR); // NOLINT - assert(flags != -1); - - // Restore the O_NONBLOCK flag if it changed. - if (O_NONBLOCK & (flags ^ s.flags)) { - flags &= ~O_NONBLOCK; - flags |= s.flags & O_NONBLOCK; - - int err; - do - err = fcntl(fd, F_SETFL, flags); - while (err == -1 && errno == EINTR); // NOLINT - assert(err != -1); - } - - if (s.isatty) { - sigset_t sa; - int err, ret; - - // We might be a background job that doesn't own the TTY so block SIGTTOU - // before making the tcsetattr() call, otherwise that signal suspends us. - sigemptyset(&sa); - sigaddset(&sa, SIGTTOU); - - ret = pthread_sigmask(SIG_BLOCK, &sa, nullptr); - assert(ret == 0); - do - err = tcsetattr(fd, TCSANOW, &s.termios); - while (err == -1 && errno == EINTR); // NOLINT - ret = pthread_sigmask(SIG_UNBLOCK, &sa, nullptr); - assert(ret == 0); - - // Normally we expect err == 0. But if macOS App Sandbox is enabled, - // tcsetattr will fail with err == -1 and errno == EPERM. - if (err != 0) { - assert(err == -1 && errno == EPERM); - } - } - } -#endif // __POSIX__ -} - -static void InitializeOpenSSL() { -#if HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL) - // In the case of FIPS builds we should make sure - // the random source is properly initialized first. -#if OPENSSL_VERSION_MAJOR >= 3 - // Use OPENSSL_CONF environment variable is set. - const char* conf_file = getenv("OPENSSL_CONF"); - - OPENSSL_INIT_SETTINGS* settings = OPENSSL_INIT_new(); - OPENSSL_INIT_set_config_filename(settings, conf_file); - OPENSSL_INIT_set_config_appname(settings, "openssl_conf"); - OPENSSL_INIT_set_config_file_flags(settings, - CONF_MFLAGS_IGNORE_MISSING_FILE); - - OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, settings); - OPENSSL_INIT_free(settings); - - if (ERR_peek_error() != 0) { - fprintf(stderr, "OpenSSL configuration error:\n"); - ERR_print_errors_fp(stderr); - exit(1); - } -#else // OPENSSL_VERSION_MAJOR < 3 - if (FIPS_mode()) { - OPENSSL_init(); - } -#endif - V8::SetEntropySource(boxednode::EntropySource); -#endif -} - -void InitializeOncePerProcess() { - atexit(ResetStdio); - PlatformInit(); - InitializeOpenSSL(); -} - -void TearDownOncePerProcess() { -#if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32) - RemoveVectoredExceptionHandler(old_vectored_exception_handler); -#endif -} - -} // namespace boxednode - -#endif // USE_OWN_LEGACY_PROCESS_INITIALIZATION - namespace boxednode { REPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER } diff --git a/scripts/update-gha-workflow.sh b/scripts/update-gha-workflow.sh new file mode 100755 index 0000000..874b2d1 --- /dev/null +++ b/scripts/update-gha-workflow.sh @@ -0,0 +1,36 @@ +#/bin/bash + +set -e + +TEMPLATE_FILE=.github/workflows/nodejs.yml.in +OUTPUT_FILE=.github/workflows/nodejs.yml +TEST_SUITE=test/index.ts + +rm $OUTPUT_FILE + +echo "# This is a generated file. Please change $TEMPLATE_FILE" > $OUTPUT_FILE +echo "# and run the following command to update the GHA Workflow" >> $OUTPUT_FILE +echo "# $> npm run update-gha-workflow" >> $OUTPUT_FILE +echo "# --------------------" >> $OUTPUT_FILE + +# This AWK script seems complicated, but it's actually really simple: +# 1. It grabs all the lines with the "it" function +# 2. Removes quotes (single quotes and backticks) +# 3. If it contains a $, assumes it's a literal template, so keeps everything before the $ (for name matching) +# 4. Iterates over all matches and generates a single-line JSON array. +RESULT=$(awk ' +/it\(/ { + s = $0 + sub(/.*it\(\s*['\''`"]/, "", s) + if (s ~ /\$/) sub(/\$.*/, "", s) + sub(/['\''`"].*/, "", s) + names[++n] = s +} +END { + printf "[" + for (i = 1; i <= n; i++) { printf "\"%s\"%s", names[i], (i < n ? "," : "") } + print "]" +} +' $TEST_SUITE) + +sed "s/<>/$RESULT/g" $TEMPLATE_FILE >> $OUTPUT_FILE diff --git a/src/helpers.ts b/src/helpers.ts index e559466..83fd6d9 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -113,7 +113,6 @@ export async function createUncompressedBlobDefinition (fnName: string, source: ${Uint8Array.prototype.toString.call(source) || '0'} }; -#ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT std::optional ${fnName}SV() { return { { @@ -122,7 +121,6 @@ export async function createUncompressedBlobDefinition (fnName: string, source: } }; } -#endif std::vector ${fnName}Vector() { return std::vector( @@ -166,11 +164,9 @@ export async function createCompressedBlobDefinition (fnName: string, source: Ui return dst;`} } -#ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT std::optional ${fnName}SV() { return {}; } -#endif ${blobTypedArrayAccessors(fnName, source.length)} `; diff --git a/src/index.ts b/src/index.ts index c7c96c7..baa9154 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import tar from 'tar'; import path from 'path'; import zlib from 'zlib'; import os from 'os'; -import rimraf from 'rimraf'; +import { rimraf } from 'rimraf'; import crypto from 'crypto'; import { promisify } from 'util'; import { promises as fs, createReadStream, createWriteStream } from 'fs'; @@ -191,17 +191,8 @@ async function compileNode ( env: env }; - // Node.js 19.4.0 is currently the minimum version that has https://github.com/nodejs/node/pull/45887. - // We want to disable the shared-ro-heap flag since it would require - // all snapshots used by Node.js to be equal, something that we don't - // want to or need to guarantee as embedders. - const nodeVersion = await getNodeVersionFromSourceDirectory(sourcePath); - if (nodeVersion[0] > 19 || (nodeVersion[0] === 19 && nodeVersion[1] >= 4)) { - if (process.platform !== 'win32') { - buildArgs = ['--disable-shared-readonly-heap', ...buildArgs]; - } else { - buildArgs = ['no-shared-roheap', ...buildArgs]; - } + if (process.env.BOXEDNODE_DCHECKS_ENABLED === '1') { + buildArgs = ['--debug-node', '--v8-with-dchecks', ...buildArgs]; } if (process.platform !== 'win32') { @@ -248,14 +239,25 @@ async function compileNode ( // conflicting arguments have been passed manually. const vcbuildArgs: string[] = [...buildArgs, ...makeArgs, 'projgen']; if (!vcbuildArgs.includes('debug') && !vcbuildArgs.includes('release')) { vcbuildArgs.push('release'); } - if (!vcbuildArgs.some((arg) => /^vs/.test(arg))) { vcbuildArgs.push('vs2019'); } + if (!vcbuildArgs.includes('x86') && + !vcbuildArgs.includes('x64') && + !vcbuildArgs.includes('ia32') && + !vcbuildArgs.includes('arm64') + ) { + vcbuildArgs.push('x64'); + } + if (!vcbuildArgs.some((arg) => /^vs/.test(arg))) { vcbuildArgs.push('vs2022'); } for (const module of linkedJSModules) { vcbuildArgs.push('link-module', module); } - await spawnBuildCommand(['cmd', '/c', '.\\vcbuild.bat', ...vcbuildArgs], options); - return path.join(sourcePath, 'Release', 'node.exe'); + await spawnBuildCommand(['cmd', '/c', '.\\vcbuild.bat', ...vcbuildArgs], options); + if (vcbuildArgs.includes('debug')) { + return path.join(sourcePath, 'Debug', 'node.exe'); + } else { + return path.join(sourcePath, 'Release', 'node.exe'); + } } } @@ -350,25 +352,9 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L enableBindingsPatch })); - /** - * Since Node 20.x, external source code linked from `lib` directory started - * failing the Node.js build process because of the file being linked multiple - * times which is why we do not link the external files anymore from `lib` - * directory and instead from a different directory, `lib-boxednode`. This - * however does not work for any node version < 20 which is why we are - * conditionally generating the entry point and configure params here based on - * Node version. - */ - const { customCodeSource, customCodeConfigureParam, customCodeEntryPoint } = nodeVersion[0] >= 20 - ? { - customCodeSource: path.join(nodeSourcePath, 'lib-boxednode', `${namespace}.js`), - customCodeConfigureParam: `./lib-boxednode/${namespace}.js`, - customCodeEntryPoint: `lib-boxednode/${namespace}` - } : { - customCodeSource: path.join(nodeSourcePath, 'lib', namespace, `${namespace}.js`), - customCodeConfigureParam: `./lib/${namespace}/${namespace}.js`, - customCodeEntryPoint: `${namespace}/${namespace}` - }; + const customCodeSource = path.join(nodeSourcePath, 'lib-boxednode', `${namespace}.js`); + const customCodeConfigureParam = `./lib-boxednode/${namespace}.js`; + const customCodeEntryPoint = `lib-boxednode/${namespace}`; await fs.mkdir(path.dirname(customCodeSource), { recursive: true }); await fs.writeFile(customCodeSource, entryPointTrampolineSource); @@ -392,6 +378,20 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L ? createCompressedBlobDefinition : createUncompressedBlobDefinition; + async function cleanUpTemporaryDirectoriesIfNecessary () { + if (process.platform === 'win32' && nodeVersion[0] >= 24) { + // Compiling with a snapshot requires compiling twice: a base image + // and the image with the snapshot embedded. In that situation, clang-cl + // complains that there are precompiled headers that changed between + // compilations and does not refresh them, but kills the compilation + // process with an error. Due to this, before attempting the second + // compilation, we will delete all pch files. + const nodeCompilationOutputDirectory = path.join((await fs.realpath(nodeSourcePath)), 'out'); + logger.stepStarting(`(win32) Wiping output directory at ${nodeCompilationOutputDirectory}`); + await rimraf(nodeCompilationOutputDirectory, { glob: false }); + logger.stepCompleted(); + } + } async function writeMainFileAndCompile ({ codeCacheBlob = new Uint8Array(0), codeCacheMode = 'ignore', @@ -447,6 +447,7 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L logger); } + await cleanUpTemporaryDirectoriesIfNecessary(); let binaryPath: string; if (!options.useCodeCache && !options.useNodeSnapshot) { binaryPath = await writeMainFileAndCompile(); @@ -464,6 +465,7 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L throw new Error('Empty code cache/snapshot result'); } logger.stepCompleted(); + await cleanUpTemporaryDirectoriesIfNecessary(); binaryPath = await writeMainFileAndCompile(options.useNodeSnapshot ? { snapshotBlob: result, snapshotMode: 'consume' @@ -480,7 +482,7 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L if (options.clean) { logger.stepStarting('Cleaning temporary directory'); - await promisify(rimraf)(options.tmpdir, { glob: false }); + await rimraf(options.tmpdir, { glob: false }); logger.stepCompleted(); } } diff --git a/test/compile-main-template-only.sh b/test/compile-main-template-only.sh index 7e4316a..4f12078 100755 --- a/test/compile-main-template-only.sh +++ b/test/compile-main-template-only.sh @@ -9,12 +9,13 @@ cd "$(dirname $0)/.." if [ ! -e main-template-build ]; then mkdir main-template-build pushd main-template-build - curl -O https://nodejs.org/dist/v20.12.0/node-v20.12.0.tar.xz + curl -O https://nodejs.org/dist/v24.10.0/node-v24.10.0.tar.xz tar --strip-components=1 -xf node-*.tar.xz popd fi g++ \ + -Imain-template-build/deps/brotli/c/include/ \ -Imain-template-build/src \ -Imain-template-build/deps/v8/include \ -Imain-template-build/deps/uv/include \ @@ -23,6 +24,7 @@ g++ \ -DREPLACE_WITH_ENTRY_POINT='"placeholder"' \ -DBOXEDNODE_CODE_CACHE_MODE='"placeholder"' \ -DREPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER= \ + -std=c++20 \ -fPIC -shared \ -o main-template-build/out.so \ -include resources/add-node_api.h \ diff --git a/test/index.ts b/test/index.ts index f0fafbf..534f5ce 100644 --- a/test/index.ts +++ b/test/index.ts @@ -3,7 +3,6 @@ import path from 'path'; import os from 'os'; import assert from 'assert'; import childProcess from 'child_process'; -import semver from 'semver'; import { promisify } from 'util'; import pkgUp from 'pkg-up'; import { promises as fs } from 'fs'; @@ -11,6 +10,9 @@ import { promises as fs } from 'fs'; const execFile = promisify(childProcess.execFile); const exeSuffix = process.platform === 'win32' ? '.exe' : ''; +// we are using 4h because compiling in windows might take more time +const DEFAULT_TIMEOUT = 4 * 60 * 60 * 1000; + describe('basic functionality', () => { // Test the currently running Node.js version. Other versions can be checked // manually that way, or through the CI matrix. @@ -18,7 +20,7 @@ describe('basic functionality', () => { describe(`On Node v${version}`, function () { it('works in a simple case', async function () { - this.timeout(2 * 60 * 60 * 1000); // 2 hours + this.timeout(DEFAULT_TIMEOUT); await compileJSFileAsBinary({ nodeVersionRange: version, sourceFile: path.resolve(__dirname, 'resources/example.js'), @@ -112,11 +114,7 @@ describe('basic functionality', () => { }); it('works with a Nan addon', async function () { - if (semver.lt(version, '12.19.0')) { - return this.skip(); // no addon support available - } - - this.timeout(2 * 60 * 60 * 1000); // 2 hours + this.timeout(DEFAULT_TIMEOUT); await compileJSFileAsBinary({ nodeVersionRange: version, sourceFile: path.resolve(__dirname, 'resources/example.js'), @@ -139,11 +137,7 @@ describe('basic functionality', () => { }); it('works with a N-API addon', async function () { - if (semver.lt(version, '14.13.0')) { - return this.skip(); // no N-API addon support available - } - - this.timeout(2 * 60 * 60 * 1000); // 2 hours + this.timeout(DEFAULT_TIMEOUT); await compileJSFileAsBinary({ nodeVersionRange: version, sourceFile: path.resolve(__dirname, 'resources/example.js'), @@ -166,7 +160,7 @@ describe('basic functionality', () => { }); it('passes through env vars and runs the pre-compile hook', async function () { - this.timeout(2 * 60 * 60 * 1000); // 2 hours + this.timeout(DEFAULT_TIMEOUT); let ranPreCompileHook = false; async function preCompileHook (nodeSourceTree: string) { ranPreCompileHook = true; @@ -189,7 +183,7 @@ describe('basic functionality', () => { }); it('works with code caching support', async function () { - this.timeout(2 * 60 * 60 * 1000); // 2 hours + this.timeout(DEFAULT_TIMEOUT); await compileJSFileAsBinary({ nodeVersionRange: version, sourceFile: path.resolve(__dirname, 'resources/example.js'), @@ -216,9 +210,9 @@ describe('basic functionality', () => { for (const compressBlobs of [false, true]) { it(`works with snapshot support (compressBlobs = ${compressBlobs})`, async function () { - this.timeout(2 * 60 * 60 * 1000); // 2 hours + this.timeout(DEFAULT_TIMEOUT); await compileJSFileAsBinary({ - nodeVersionRange: '^20.13.0', + nodeVersionRange: version, sourceFile: path.resolve(__dirname, 'resources/snapshot-echo-args.js'), targetFile: path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), useNodeSnapshot: true,