Skip to content

test(napi): extend the direct-compile fast path to Windows - #36210

Closed
robobun wants to merge 4 commits into
mainfrom
farm/154e1bcd/napi-win-fast-build
Closed

robobun wants to merge 4 commits into
mainfrom
farm/154e1bcd/napi-win-fast-build

Conversation

@robobun

@robobun robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Speeds up the 55 do.test.ts wrappers under test/napi/node-napi-tests/ on Windows by letting the existing tryBuildFast path handle that platform too.

#33622 added a fast path that bypasses node-gyp on POSIX by invoking the platform compiler directly once the node headers are cached. On Windows the function returned false immediately, so every addon still went through bun x node-gyp@11 configure build (python gyp + MSBuild), which is ~4-5s per directory once warm and 30-40s for whichever file runs first in a shard and has to install node-gyp and download headers. The 38s 7_factory_wrap/do.test.ts observed on windows 2019 x64 in build 83901 is that cold cost; the next file in the same shard took 4.55s.

Change:

  • findNodeHeaders now also probes %LOCALAPPDATA%\node-gyp\Cache\<version> and, on Windows, additionally requires the matching <arch>\node.lib import library.
  • win_delay_load_hook.cc is vendored next to the harness (copied from node-gyp, MIT). It is the same hook node-gyp links into every addon so /DELAYLOAD:node.exe resolves to the host process.
  • windowsCompileArgs drives clang-cl to produce build\Debug\<target>.node directly: headers via /I, the gyp defines, the sources plus the delay-load hook, then /link /DLL against node.lib with /DELAYLOAD:node.exe and delayimp.lib. clang-cl is on PATH on every Windows CI/dev image via the scoop LLVM install and auto-detects the MSVC toolchain, so it works outside a Developer Command Prompt.
  • The POSIX command line is unchanged, just pulled out into posixCompileArgs so tryBuildFast can branch on platform.
  • Any failure (headers not cached, clang-cl missing, compile error) returns false and falls back to buildWithNodeGyp exactly as before.

How did you verify your code works?

Timed on a Windows Server 2019 x64 machine, system Bun, node headers already cached:

main this PR
7_factory_wrap/do.test.ts 4.06s 0.48s
full node-napi-tests/ suite (55 files) 377.7s 22.1s

Both runs: 123 pass, 25 todo, 0 fail.

Cold start (no header cache) is unchanged: the fast path sees no headers, returns false, node-gyp downloads them and builds the first addon; every subsequent file in that shard then takes the fast path.

On Linux (debug+ASAN, headers cached) the full suite is still 124 pass / 24 todo / 0 fail and the POSIX fast path still fires for every addon; 7_factory_wrap timing is unchanged since the POSIX path already existed.


no test proof · iteration 0 · docs-only change; test-proof not applicable

The node-napi-tests harness already bypasses node-gyp on POSIX by
invoking the platform compiler directly once the node headers are
cached. On Windows it returned false unconditionally, so every one of
the 55 do.test.ts wrappers paid the full bun x + python gyp + MSBuild
cost (~4-5s warm, 30-40s for whichever file ran first in a shard and
had to download headers).

This teaches findNodeHeaders about the %LOCALAPPDATA%\node-gyp\Cache
layout (including the per-arch node.lib), vendors node-gyp's
win_delay_load_hook.cc, and adds a clang-cl command line that produces
the same build\Debug\<target>.node that MSBuild would. clang-cl is on
PATH on every Windows CI and dev image via scoop llvm and auto-detects
the MSVC toolchain, so no Developer Command Prompt is required. Any
failure (headers not yet cached, clang-cl missing, compile error)
falls back to node-gyp exactly as before.

Measured on a Windows 2019 x64 machine with warm headers:
  7_factory_wrap/do.test.ts   4.06s -> 0.48s
  full node-napi-tests suite  377.7s -> 22.1s  (123 pass, 0 fail)

Linux and macOS are unchanged; the POSIX path is only pulled out into
posixCompileArgs so tryBuildFast can branch on platform.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The N-API test harness now directly compiles addon targets on POSIX and Windows, using platform-specific Node header and compiler configuration. Windows builds include a delay-load hook for renamed host executables, temporary output promotion, failure logging, fallback, and cleanup.

Changes

Cross-platform fast compilation

Layer / File(s) Summary
Platform inputs and compiler commands
test/napi/node-napi-tests/harness.ts, test/napi/node-napi-tests/win_delay_load_hook.cc
Node header discovery now returns include and Windows import-library paths; compiler helpers generate POSIX and Windows arguments, with the Windows delay-load hook included in builds.
Fast-build execution and cleanup
test/napi/node-napi-tests/harness.ts
tryBuildFast validates targets, compiles them concurrently, promotes successful temporary outputs, reports failures, falls back to node-gyp, and cleans Windows scratch files.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: extending the direct-compile fast path to Windows.
Description check ✅ Passed The description includes both required sections and provides detailed implementation notes plus verification results.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Arrived at the same fix independently from a parallel handoff (6_object_wrap, same build 83901). Confirming the approach works end to end on both Windows arches:

  • windows-x64, all 55 do.test.ts files with build/ removed: 123 pass / 0 fail, 35.6s total
  • windows-aarch64: arm64/node.lib picked up correctly, 6_object_wrap 0.82s

Two small notes from comparing against node-gyp's own MSBuild output (CL.command.1.tlog):

  • node-gyp passes /MTd (static debug CRT), not /MDd; /MDd pulls in the debug UCRT DLLs which are not guaranteed outside a VS install. Probably fine in CI either way since the VS toolchain is present, but /MTd matches the real build.
  • /Fo${out}${sep} puts every target's objects in the shared build/Debug; test_cannot_run_js and test_reference_by_node_api_version compile the same source file into two targets in parallel. I hit a clobber there and ended up giving each target its own obj/<target>/ subdir. Edit: re-reading, your tmp dir is already per-target so this is handled.

My branch (farm/9b135bfa/napi-windows-fast-build) also flips 6_object_wrap/do.test.ts to the shared-promise + test.concurrent pattern that test_constructor uses, but that is a rounding error next to the build cost. Dropping mine in favour of this one.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline nit: the /MDd vs /MTd difference raised in the thread was examined — the debug UCRT DLLs ship with the VS toolchain present on every Windows CI image, and a load failure would fall through to the node-gyp path anyway. Also confirmed the per-target .{target}.{pid}.tmp scratch dir avoids the /Fo object-clobber for multi-target gyps (test_cannot_run_js, test_reference_by_node_api_version), and that the POSIX command line is byte-identical after the extraction into posixCompileArgs.

Extended reasoning...

Test-infrastructure-only change with a hard fallback to node-gyp on any failure, so the blast radius is bounded. One low-impact nit surfaced (scratch dir left behind on Windows compile failure). robobun's /MTd suggestion is still open in the thread, so leaving the final call to a human rather than approving over it.

Comment thread test/napi/node-napi-tests/harness.ts Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both review notes (scratch-dir cleanup in finally, /MTd to match node-gyp) are addressed in 3e3d604.

What was reviewed:

  • POSIX path is byte-identical after the extract-to-posixCompileArgs refactor; findNodeHeaders change is additive for win32 only.
  • Fail-safe shape holds: missing headers/node.lib/clang-cl and any nonzero exit all return false → buildWithNodeGyp.
  • headers.lib! is safe — win32 branch of findNodeHeaders only returns when node.lib exists.
  • Mixed C/C++ targets: win_delay_load_hook.cc compiles by extension under clang-cl even when /std:c++20 isn't passed for a pure-C target.
Extended reasoning...

Overview

Test-harness-only change extending the existing tryBuildFast direct-compile optimization (added in #33622 for POSIX) to Windows. Touches test/napi/node-napi-tests/harness.ts and adds a vendored win_delay_load_hook.cc copied verbatim from node-gyp (MIT). No runtime, native, or production code is affected.

The POSIX command line was lifted unchanged into posixCompileArgs; findNodeHeaders gained a %LOCALAPPDATA%\node-gyp\Cache probe and now also returns the <arch>/node.lib import library on Windows; windowsCompileArgs drives clang-cl with /DELAYLOAD:node.exe + the vendored hook; tryBuildFast branches on platform and cleans the per-target scratch dir in a finally.

Security risks

None. This is a test build harness that shells out to a local compiler with paths derived from checked-in binding.gyp files (which parseSimpleBindingGyp already strictly whitelists to target_name/sources/defines). No network, no user input, no runtime surface.

Level of scrutiny

Low-to-medium. It's test infrastructure with a hard fallback: every failure mode (headers not cached, clang-cl not on PATH, compile error, spawn/rename throw) returns false and drops through to the pre-existing buildWithNodeGyp, so the worst case is the status quo. The blast radius of a mistake is Windows CI test wall-clock, not correctness — the addons are still exercised by the same do.test.ts files either way.

Other factors

  • My prior review's only finding (scratch dir leaked on the failure path) was fixed in commit 3e3d604 by moving rm(tmp, {recursive, force}) into a finally; force: true makes it safe when mkdir never ran.
  • robobun's independent verification on windows-x64 (55 files, 123 pass/0 fail) and windows-aarch64 (arm64/node.lib picked up), plus their /MTd note, are both reflected in the current diff. Their per-target /Fo clobber concern was already handled by the per-target tmp dir (they self-corrected).
  • Checked that headers.lib! cannot be undefined on the win32 branch: findNodeHeaders on win32 only returns non-null after existsSync(lib).
  • Checked the mixed-language case: for pure-C targets the hook .cc is still appended; clang-cl selects language by extension so it compiles as C++ without needing the conditionally-added /std:c++20.
  • stdout is now piped and drained alongside stderr (clang-cl writes diagnostics to stdout), satisfying the concurrent-drain rule.

@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Self-review surfaced two more things, both in 8e83bbc:

  • Debug info: the Windows command line now passes /Z7 and /link /DEBUG /PDB:build\Debug\<target>.pdb, matching what node-gyp's common.gypi emits (DebugInformationFormat: 1, GenerateDebugInformation: true) and the POSIX -g. The PDB goes straight to build\Debug\ so the scratch-dir cleanup does not touch it.
  • Cleanup in finally: the rm(tmp, ...) now swallows rejections. A transient EBUSY/EPERM on the scratch dir must not override the function's return value and turn a successful build (or a clean fallback) into a hard failure.

Re-ran the full suite on Windows x64 with warm headers: 123 pass / 25 todo / 0 fail in 27.0s, with a PDB next to every .node.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the earlier scratch-dir cleanup nit and the /MTd note are both addressed, and the POSIX path is byte-for-byte unchanged after the extraction into posixCompileArgs.

What was reviewed:

  • findNodeHeaders return-type change: headers.lib! is safe because the win32 branch only returns non-null when existsSync(lib) passes.
  • Fallback coverage: every new failure exit (clang-cl missing, headers/lib missing, nonzero exit, thrown error) returns false and reaches buildWithNodeGyp, so worst case on a misconfigured Windows box is the pre-PR behavior.
  • stdout switched from ignore to pipe and drained concurrently with stderr/exited — needed since cl-style drivers write diagnostics to stdout, and avoids the pipe-buffer deadlock.
  • Per-target /Fo scratch dir + pid handles the parallel-target .obj clobber; /PDB writes to outDir so it survives the finally rm.
Extended reasoning...

Overview

Extends the existing tryBuildFast node-gyp bypass in test/napi/node-napi-tests/harness.ts to Windows: probes %LOCALAPPDATA%\node-gyp\Cache\<ver> for headers plus the arch-specific node.lib, then drives clang-cl directly with /link /DLL /DELAYLOAD:node.exe against a vendored copy of node-gyp's win_delay_load_hook.cc. The pre-existing POSIX command line is lifted verbatim into posixCompileArgs — I diffed it flag-for-flag against the removed inline array and it is identical. Two files touched, both under test/.

Security risks

None. This is test-harness plumbing that shells out to a local compiler with paths derived from checked-in binding.gyp files that parseSimpleBindingGyp already restricts to {target_name, sources, defines}-only shapes. No runtime, network, or user-facing surface.

Level of scrutiny

Low-to-moderate. Nothing here ships in the binary; the only risk is CI regression, and every failure path returns false to fall through to the exact node-gyp invocation that was the sole Windows path before this PR. The author and a second robobun run independently verified 123 pass / 0 fail across all 55 files on both Windows arches, and confirmed the Linux/ASAN suite is unchanged.

Other factors

My prior review flagged the scratch-dir leak on the failure path; 3e3d604 moved the rm into a finally and 8e83bbc added .catch(() => {}) so a transient EBUSY on cleanup can't override the try/catch return value. robobun's /MTd-vs-/MDd note was also applied. Both threads are resolved. The vendored win_delay_load_hook.cc is the standard node-gyp hook (MIT), guarded by #ifdef _MSC_VER which clang-cl defines, and HOST_BINARY is passed as "node.exe" to match node-gyp's own define.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/napi/node-napi-tests/harness.ts (1)

148-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding lightweight unit tests for the new pure builder/discovery functions.

findNodeHeaders, posixCompileArgs, and windowsCompileArgs are deterministic, side-effect-light functions whose output (arg ordering, flag correctness, candidate-directory logic) directly drives whether the fast path silently falls back to node-gyp. A small test asserting expected arg arrays for representative inputs (C vs C++ target, with/without defines) would guard against future regressions in this path without needing a real compiler.

As per coding guidelines, "Every behavioral change must include an automated test in the same PR."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/napi/node-napi-tests/harness.ts` around lines 148 - 213, Add focused
unit tests for the pure fast-build helpers findNodeHeaders, posixCompileArgs,
and windowsCompileArgs. Cover representative C and C++ targets, targets with and
without defines, expected argument ordering and flags, and candidate-directory
selection; keep tests deterministic without invoking a compiler or node-gyp.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/napi/node-napi-tests/harness.ts`:
- Around line 114-146: Update windowsCompileArgs to emit the PDB into the
per-target scratch directory instead of the shared out directory, then extend
the successful artifact promotion flow to move the PDB alongside the .node file.
Ensure failed builds leave no PDB in outDir and rely on existing scratch
cleanup.

---

Outside diff comments:
In `@test/napi/node-napi-tests/harness.ts`:
- Around line 148-213: Add focused unit tests for the pure fast-build helpers
findNodeHeaders, posixCompileArgs, and windowsCompileArgs. Cover representative
C and C++ targets, targets with and without defines, expected argument ordering
and flags, and candidate-directory selection; keep tests deterministic without
invoking a compiler or node-gyp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ae78854-829e-429f-af1d-bf1afd701117

📥 Commits

Reviewing files that changed from the base of the PR and between 9b678b4 and 8e83bbc.

📒 Files selected for processing (2)
  • test/napi/node-napi-tests/harness.ts
  • test/napi/node-napi-tests/win_delay_load_hook.cc

Comment thread test/napi/node-napi-tests/harness.ts Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-harness-only change with a clean fallback to node-gyp on every failure path; all prior feedback (scratch-dir finally, /MTd, /Z7+PDB, PDB promote-from-scratch) is addressed.

What was reviewed:

  • POSIX command line is byte-identical after the extract to posixCompileArgs; only the stdout: "pipe" change touches non-Windows behavior, and both pipes are drained concurrently with .exited.
  • headers.lib! is safe: findNodeHeaders on win32 only returns non-null when <arch>/node.lib exists; process.arch (x64/arm64) matches node-gyp's cache layout, confirmed on both arches in-thread.
  • Per-target .{name}.{pid}.tmp scratch dir + /Fo trailing separator avoids the parallel-target .obj clobber; finally cleans it with errors swallowed so a transient EBUSY can't mask the real return value.
  • win_delay_load_hook.cc is the node-gyp MIT copy; HOST_BINARY is supplied via /D so _stricmp sees the string literal.
Extended reasoning...

Overview

Extends the existing tryBuildFast direct-compile path in test/napi/node-napi-tests/harness.ts to Windows. Two files touched: the harness (refactor POSIX args into posixCompileArgs, add windowsCompileArgs driving clang-cl, teach findNodeHeaders about %LOCALAPPDATA%\node-gyp\Cache and the <arch>/node.lib import library, add per-target scratch dir + promote-on-success + finally cleanup) and a vendored win_delay_load_hook.cc copied from node-gyp so /DELAYLOAD:node.exe resolves to the host process. No production code is touched.

Security risks

None. This is test-only infrastructure that spawns a local compiler over in-tree source files. No network, no untrusted input, no auth/crypto surface. The vendored hook is a well-known MIT-licensed node-gyp file.

Level of scrutiny

Low-to-moderate. The change is confined to a test harness and every new code path degrades to return false → buildWithNodeGyp, so the worst case for a mistake in the Windows args is "fast path doesn't fire, tests take as long as they do today." The POSIX path is unchanged after the mechanical extraction (verified line-by-line against the removed inline array). The one non-Windows behavioral change is stdout: "ignore" → "pipe", which is now drained concurrently alongside stderr and .exited, so no deadlock risk per the subprocess-drain rule.

Other factors

  • All four review threads (my scratch-dir-leak note, robobun's /MTd and per-target obj-dir observations, the self-review /Z7+PDB addition, and CodeRabbit's PDB-in-scratch note) are resolved and reflected in the final diff.
  • Verified in-thread on windows-x64 (123 pass / 25 todo / 0 fail, 22-27s vs 377s), windows-aarch64 (arm64/node.lib picked up), and Linux debug+ASAN (unchanged 124 pass / 24 todo).
  • The headers.lib! non-null assertion is guarded: on win32 findNodeHeaders only returns non-null when existsSync(lib) is true, and the win32 branch is the only one that reads .lib.
  • If the PDB renameSync were ever to throw (e.g. linker didn't emit one), the catch returns false and node-gyp runs — acceptable degradation, not a test failure.

@github-actions

Copy link
Copy Markdown
Contributor

Closing this PR because it has been inactive for more than 90 days.

@github-actions github-actions Bot closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant