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.
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.
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.
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.emccworks 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-cliyourself 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.
Related
- Driving Wasm builds with a justfile — one set of recipes for PowerShell and sh.
- Building Wasm in Docker for consistent output — sidestepping host differences for releases.
- Debugging Wasm from VS Code — editor setup that works in both native and WSL.
- Rust to Wasm compilation guide — what to build once the tools work.
← Back to Cross-Platform Build Automation