Setting up Wasm Toolchains on Windows

This page answers one question: what is the least painful way to get a working WebAssembly toolchain on a Windows machine, and what goes wrong when you do?

Prerequisites

  • [ ] Windows 10 22H2 or Windows 11, with administrator rights for the first install.
  • [ ] winget (included in current Windows releases) or a willingness to download installers.
  • [ ] Git for Windows, installed with the option to check out files as-is or with LF line endings.

Native or WSL: decide first

There are two realistic setups. A native setup installs every tool as a Windows program and builds from PowerShell. A WSL 2 setup installs a Linux distribution inside Windows and runs the Linux toolchain there, with the editor on the Windows side talking to it remotely. Both produce identical WebAssembly — the output does not depend on the host operating system — so the choice is about friction, not results.

Native builds integrate with Windows editors and debuggers directly and avoid a second file system. WSL builds match what CI runs, let you copy Linux instructions verbatim, and avoid nearly every Windows-specific problem listed further down. The deciding question is usually how much of the project’s scripting is written for sh.

Choosing between a native and a WSL toolchain on Windows If the project's scripts assume a Unix shell or it uses Emscripten heavily, WSL 2 avoids most friction. If it is a Rust-only project driven by cargo and wasm-pack, a native install works well. Mixed teams can use native tools plus a container for release builds. How is the build driven? sh scripts, Makefiles WSL 2 Linux instructions work verbatim cargo + wasm-pack only Native install fewest moving parts mixed team, strict releases Native + container Docker image for releases

Step 1 — native Rust and wasm-pack

winget install Rustlang.Rustup
# reopen the terminal so PATH includes %USERPROFILE%\.cargo\bin
rustup default stable-x86_64-pc-windows-msvc
rustup target add wasm32-unknown-unknown wasm32-wasip1

The MSVC host toolchain needs the Visual Studio Build Tools for native dependencies such as build scripts and proc-macros — those still compile for Windows even when the final target is Wasm. Install the “Desktop development with C++” workload, or the smaller Build Tools package:

winget install Microsoft.VisualStudio.2022.BuildTools --override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --quiet"

Then install the Wasm tools from release binaries rather than compiling them:

cargo install cargo-binstall
cargo binstall wasm-pack wasm-bindgen-cli --no-confirm

Step 2 — native Binaryen and WABT

Binaryen and WABT publish Windows release archives. Unpack them into one tools directory and add it to the user PATH once:

$tools = "$env:USERPROFILE\wasm-tools"
New-Item -ItemType Directory -Force $tools | Out-Null
Invoke-WebRequest https://github.com/WebAssembly/binaryen/releases/download/version_118/binaryen-version_118-x86_64-windows.tar.gz -OutFile "$tools\binaryen.tar.gz"
tar -xzf "$tools\binaryen.tar.gz" -C $tools
[Environment]::SetEnvironmentVariable("Path", "$env:Path;$tools\binaryen-version_118\bin", "User")

tar ships with Windows 10 and later, so no extra archiver is needed. After reopening the terminal, wasm-opt --version should print version 118.

Step 3 — emsdk on Windows

Emscripten supports Windows natively, but emsdk’s activation scripts behave differently in each shell. In PowerShell:

git clone https://github.com/emscripten-core/emsdk.git $env:USERPROFILE\emsdk
cd $env:USERPROFILE\emsdk
.\emsdk.ps1 install 3.1.61
.\emsdk.ps1 activate 3.1.61 --permanent

--permanent writes the environment variables to the user profile so every new terminal has emcc on the PATH. Without it, activation lasts only for the current session — the most common reason a second terminal reports emcc as not found.

Step 4 — or do all of it in WSL 2

wsl --install -d Ubuntu-24.04

Inside the distribution, follow the Linux instructions unchanged:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
rustup target add wasm32-unknown-unknown
cargo install cargo-binstall && cargo binstall -y wasm-pack wasm-bindgen-cli

The rule that matters in WSL is where the source lives. Keep the repository inside the Linux file system — ~/src/app, not /mnt/c/Users/.... Builds that read thousands of files across the Windows/Linux boundary run several times slower, and file watchers often miss changes there. VS Code’s WSL extension opens a folder in the Linux file system as if it were local.

Clean release build time by setup on the same laptop A clean wasm-pack release build of one crate on a single Windows laptop. WSL 2 with the source in the Linux file system is fastest; the same WSL install building from the Windows drive is slowest because every file read crosses the file-system boundary. seconds, lower is better native PowerShell, Defender on 141 s native, build dir excluded 102 s WSL 2, source in ~/src 88 s WSL 2, source on /mnt/c 377 s Excluding the cargo and target directories from real-time scanning recovered most of the native gap.

Step 4b — testing in browsers from WSL

One question comes up in every WSL setup: the build runs in Linux, but the browser runs in Windows. That works better than people expect. WSL 2 forwards localhost ports to Windows automatically, so a dev server started inside the distribution on port 8080 is reachable from Edge or Chrome on the Windows side at http://localhost:8080. Secure-context features such as SharedArrayBuffer treat localhost as secure, so cross-origin isolation works without certificates.

Headless browser tests are the exception. wasm-pack test --headless --chrome inside WSL needs a Linux browser and driver installed in the distribution — it cannot drive the Windows browser. Install them once:

sudo apt-get install -y chromium-browser chromium-chromedriver
wasm-pack test --headless --chrome

If the distribution’s Chromium is a snap package that refuses to start headless, install Firefox and geckodriver instead; wasm-pack test --headless --firefox behaves the same. The broader setup is in running Wasm tests in headless browsers.

Debugging is the other cross-boundary concern. Breakpoints in Rust source work from VS Code running on Windows when it is connected to WSL with the remote extension, because the debugger adapter runs inside the distribution where the source paths make sense. Opening the same folder through the \\wsl$ share instead gives you an editor that shows the files but a debugger that cannot map Wasm locations back to them, since the paths baked into the DWARF data are Linux paths.

Step 5 — fix the problems that only Windows has

Most Windows failures in Wasm projects come from four sources, and each has a specific fix.

Line endings. Git for Windows defaults to converting LF to CRLF on checkout. Shell scripts with CRLF fail inside WSL or Git Bash with /bin/bash^M: bad interpreter, and some code generators produce different output. Fix it once per repository:

# .gitattributes
* text=auto eol=lf
*.wasm binary
*.png binary

Long paths. Deep node_modules trees and cargo’s registry paths can exceed the 260-character limit, giving errors like The filename or extension is too long. Enable long paths in the OS and in Git:

New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name LongPathsEnabled -Value 1 -PropertyType DWORD -Force
git config --global core.longpaths true

Antivirus scanning. Real-time scanning inspects every file the compiler writes, and a Rust build writes tens of thousands. Exclude the cargo home and the project’s target/ directory, or use a Dev Drive, which Windows 11 scans asynchronously.

Windows-only failures and where each is fixed Four Windows-specific problems in Wasm builds, the symptom each produces, and the one-time fix — git attributes, the long-paths registry setting, antivirus exclusions, and a single project shell. problem symptom one-time fix CRLF line endings bad interpreter: ^M .gitattributes eol=lf 260-char paths filename too long LongPathsEnabled + core.longpaths real-time scanning builds 30-40% slower exclude target/ or Dev Drive shell mismatch script works in one shell only just with windows-shell

Shell differences. A script that works in Git Bash may not work in PowerShell and vice versa. Pick one shell for the project’s tooling — or use just with windows-shell set, which runs the same recipes in either.

Expected output

A quick check that every tool resolves from a fresh PowerShell window:

rustc --version; wasm-pack --version; wasm-bindgen --version; wasm-opt --version; emcc --version | Select-Object -First 1
rustc 1.81.0 (eeb90cda1 2024-09-04)
wasm-pack 0.13.0
wasm-bindgen 0.2.93
wasm-opt version 118 (version_118)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.61

Gotchas

  • error: linker 'link.exe' not found. Build scripts and proc-macros compile for the host, which needs the MSVC linker. Install the C++ build tools workload.
  • emcc works in one terminal and not another. emsdk was activated without --permanent.
  • wasm-pack hangs downloading wasm-bindgen. Corporate proxies often block its download. Install wasm-bindgen-cli yourself at the matching version; wasm-pack will use the one on the PATH.
  • Builds are fast in WSL but the editor is slow. The editor is reading the Linux files over the network share. Use the WSL remote extension rather than opening \\wsl$\... directly.

Performance note

The single biggest improvement on Windows was not a tool choice but excluding build directories from real-time scanning: a 28% faster native build for a two-minute change. The worst configuration — WSL with the source on the Windows drive — was more than four times slower than WSL with the source in Linux, which is why the source location rule is worth enforcing for every developer.

Frequently Asked Questions

Does the Windows build produce a different .wasm file? No, given the same tool versions and the path remapping from producing reproducible Wasm binaries. Without remapping, embedded paths differ, which changes the hash but not the behaviour.

Can I use the GNU host toolchain instead of MSVC? stable-x86_64-pc-windows-gnu works and avoids installing Visual Studio components, but some crates with native build steps assume MSVC. Prefer MSVC unless you have a reason.

Do I need Docker Desktop for the container-based release build? Docker Desktop uses WSL 2 as its backend, so a team that already runs WSL gets it with little extra setup. Podman Desktop is an alternative with the same WSL backend and no licensing questions for larger companies. Either runs the same Linux build image that CI uses, which keeps release artifacts identical regardless of which developer’s machine produced them.

What about Windows on ARM? Rust and wasm-pack support it natively. Binaryen and emsdk ARM builds are newer; the x64 binaries run under emulation if a native build is missing.

← Back to Cross-Platform Build Automation