Skip to content

win-arm64 Node 20 binary built without ICU/snapshots, breaks pkg at runtime #152

@meop

Description

@meop

Problem

The pre-built Node.js v20.11.1 binary for win-arm64 (in the v3.5 release) is built with --without-intl, which disables ICU, V8 snapshots, and code cache. This makes the binary unusable with @yao-pkg/pkg — any packaged executable silently exits with code 4 (InternalJSEvaluationFailure) on Windows ARM64.

The win-x64 binary is built with --with-intl=small-icu and works correctly.

Build config comparison

Config key win-x64 win-arm64
node_use_node_snapshot true false
node_use_node_code_cache true false
icu_small true false
v8_enable_i18n_support 1 0

Root cause

In lib/build.ts, compileOnWindows():

if (
  major < 24 &&
  hostArch !== targetArch &&
  !config_flags.includes('--with-intl=full-icu')
) {
  config_flags.push('--without-intl');
}

When cross-compiling from x64 → arm64 for Node < 24, ICU is completely disabled. This was added in commit 748f67a as a workaround because small-icu cross-compilation failed.

Without ICU, V8 snapshots can't be generated. Without snapshots, the pkg bootstrap module (internal/bootstrap/pkg.js) must be evaluated fresh from source at runtime. This evaluation triggers prepareMainThreadExecution() which loads internal modules that depend on ICU/Intl — causing InternalJSEvaluationFailure (exit code 4) with no output.

Why x64 works

The x64 binary has node_use_node_snapshot: true. The V8 snapshot was created at build time with full ICU support, so all bootstrap evaluation is pre-baked. The pkg VFS mechanism works through the snapshot path.

Impact

This affects all consumers of pkg that ship win-arm64 executables, including pnpm (pnpm/pnpm#9207). The arm64 exe files are published in releases but are completely non-functional.

Suggested fix

Since pkg-fetch now has access to native windows-11-arm GitHub Actions runners (per recent workflow changes), the Node 20 arm64 binary could be rebuilt natively with --with-intl=small-icu instead of cross-compiling with --without-intl.

Alternatively, using --with-intl=full-icu for cross-compilation would work (larger binary but functional).

Reproduction

# Download the v3.5 release binaries
gh release download v3.5 --repo yao-pkg/pkg-fetch -p "node-v20.11.1-win-arm64" -p "node-v20.11.1-win-x64"

# Check configs (on any machine with python3)
python3 -c "
data = open('node-v20.11.1-win-arm64', 'rb').read()
print('arm64:', 'node_use_node_snapshot' in str(data))
idx = data.find(b'node_use_node_snapshot')
print(data[idx-5:idx+50])
"
# Shows: "node_use_node_snapshot": false

# Then package any trivial app and run the arm64 exe on a Windows ARM64 machine
# → exits with code 4, no output

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions