Development Environment

April 29, 2026 · View on GitHub

How to set up zwasm for local development and how the same toolchain is exercised in CI. zwasm officially supports three host environments: macOS (Apple Silicon and Intel), Linux x86_64 / aarch64, and Windows x86_64. All three are part of the GitHub Actions matrix and all three are expected to satisfy the Commit Gate locally.

Source of Truth for Tool Versions

Two files together define every pinned tool zwasm depends on:

FileAudienceAuthoritative for
flake.nixLinux / macOSEverything Nix manages (Zig, WASI SDK, plus all of buildInputs)
.github/versions.lockWindows + CI YAMLThe same pins, mirrored as bash-sourceable KEY=value

flake.nix is the originator. versions.lock is the mirror used wherever Nix cannot reach: Windows native installs, GitHub Actions steps that need a version string before Nix is available (e.g. actions/setup-zig, cargo install --version), and any consumer that reads it as plain text. Bumping a pin requires editing both. See D136 in .dev/decisions.md for the rationale.

scripts/sync-versions.sh (run by scripts/gate-merge.sh) mechanises the consistency check; the Merge Gate item #9 in CLAUDE.md invokes it automatically.

Required Manually (per host OS)

These tools are not delivered by Nix or by the project; the developer has to install them once:

OSRequired by hand
macOSgit, Nix (Determinate installer recommended), direnv + nix-direnv
Linux x86_64git, Nix, direnv + nix-direnv
Linux (OrbStack)Same as Linux. VM bootstrap covered in setup-orbstack.md.
Windows x86_64git (Git for Windows — supplies bash + curl + tar + unzip), Python 3.x, PowerShell 7, winget

Everything else (Zig, wasm-tools, wasmtime, WASI SDK, hyperfine, jq, yq, Go, TinyGo, Node.js, Bun) is delivered by flake.nix on Linux/macOS, and by scripts/windows/install-tools.ps1 on Windows. The Windows installer covers Zig, wasm-tools, wasmtime, WASI SDK (plus Microsoft Visual C++ Redistributable, a WASI SDK clang dependency), Rust (via rustup-init, including the wasm32-wasip1 target), Go, and TinyGo — i.e. the same tooling that flake.nix provides for the realworld test suite on Linux/macOS, all pinned via versions.lock.

macOS

One-time bootstrap

# 1. Nix (Determinate installer)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

# 2. direnv + nix-direnv
brew install direnv nix-direnv
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc   # or bash equivalent
mkdir -p ~/.config/nix && echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf

# 3. Allow this repo's flake
cd /path/to/zwasm
direnv allow

After direnv allow, every shell entering the repo has Zig 0.16.0, Python 3.13, wasm-tools, wasmtime, WASI SDK, hyperfine, jq, yq-go, TinyGo, Go, Node.js, Bun, and GNU coreutils on PATH automatically. There is no nix develop --command wrapper to remember.

Daily

bash scripts/gate-commit.sh        # Commit Gate (steps 1-5 + 8)
bash scripts/gate-commit.sh --bench # + quick bench
bash scripts/gate-merge.sh         # Merge Gate (Commit + sync + CI check)
bash scripts/run-bench.sh --quick  # Bench wrapper
bash scripts/sync-versions.sh      # Manual versions.lock ↔ flake.nix check

Individual commands still work for ad-hoc loops:

zig build test
python test/spec/run_spec.py --build --summary
python test/e2e/run_e2e.py --convert --summary
python test/realworld/build_all.py && python test/realworld/run_compat.py
bash test/c_api/run_ffi_test.sh --build

The full Commit Gate / Merge Gate checklist lives in CLAUDE.md.

Linux (Ubuntu x86_64)

Native Linux (host or full VM)

Same procedure as macOS, swapping the package manager for Nix installation:

sh <(curl -L https://nixos.org/nix/install) --daemon
# direnv + nix-direnv via apt or your package manager
sudo apt install direnv
nix profile install nixpkgs#nix-direnv
direnv allow

OrbStack VM on Apple Silicon

Used for x86_64 verification of code changes that need to land on Linux amd64 before merging (Merge Gate requirement).

  • VM creation and tool installation: setup-orbstack.md
  • Run-time commands: ubuntu-testing-guide.md

Note: setup-orbstack.md still bootstraps tools manually (matching versions.lock pins as of 2026-04-29 — Zig 0.16.0, wasm-tools 1.246.1, wasmtime 42.0.1, WASI SDK 30). Migrating the VM to Nix devshell + direnv is tracked as W50 (Plan B sub-3 follow-up). After pulling a change that bumps a pin, re-run the affected steps in setup-orbstack.md, or install Nix inside the VM and use the macOS recipe above.

Windows

Windows uses native tooling, not WSL. The whole reason Windows is in the matrix is to validate native PE/COFF and MSVC behaviour; routing through WSL would re-test Linux. See D136 for the rationale.

One-time bootstrap

Pre-install the three things install-tools.ps1 cannot bootstrap from versions.lock alone. Run once in any PowerShell (admin not required for user-scope installs):

winget install --id Git.Git -e
winget install --id Microsoft.PowerShell -e
winget install --id Python.Python.3.14 -e

Then run the installer. It reads .github/versions.lock and provisions Zig, wasm-tools, wasmtime, WASI SDK into %LOCALAPPDATA%\zwasm-tools, wires them into the user-scope PATH, sets WASI_SDK_PATH, and auto-installs Microsoft Visual C++ Redistributable (a WASI SDK clang dependency) via winget when missing:

pwsh -NoLogo -ExecutionPolicy Bypass -File scripts\windows\install-tools.ps1

The script is idempotent — re-running on the same versions skips the download. Pass -Force to re-extract anyway, or -OnlyTool zig / -OnlyTool wasm-tools etc. to install one tool. Open a fresh shell afterwards so the new PATH / WASI_SDK_PATH / CARGO_HOME / RUSTUP_HOME take effect.

The realworld toolchains (Rust, Go, TinyGo) are now part of the same installer; -OnlyTool rust / -OnlyTool go / -OnlyTool tinygo install one at a time. Rust is installed via rustup-init.exe into a self-contained %LOCALAPPDATA%\zwasm-tools\rust-<toolchain>\CARGO_HOME and RUSTUP_HOME are set in user-scope env so future shells use that install rather than %USERPROFILE%\.cargo. The wasm32-wasip1 target is added automatically.

Daily (under Git for Windows bash)

bash scripts/gate-commit.sh        # Commit Gate (full Mac/Ubuntu parity)
bash scripts/gate-commit.sh --bench # + quick bench

Individual commands also work:

zig build test
zig build c-test                          # C API tests via Zig harness
python test/spec/run_spec.py --build --summary
python test/e2e/run_e2e.py --convert --summary
python test/realworld/build_all.py
python test/realworld/run_compat.py

Currently-skipped CI items on Windows (Plan C tracker)

After W50 (CI Nix-ify, shipped 2026-04-29 PM via PRs #80..#83) and W53 (rustup-init stdout pollution fix, 2026-04-29 evening), the Windows test job runs the full Commit Gate via pwsh scripts/windows/install-tools.ps1 + bash scripts/gate-commit.sh plus the same five extras that test-nix runs on Linux/macOS (c-test / static-lib / static-link / Rust example / memory check). The remaining Windows-skipped CI item is benchmark — intentionally Ubuntu-only per CLAUDE.md's bench policy. Closing it formally is Plan C-g, sequenced behind C-g's 3-platform baseline reset (see checklist.md C-g + W47 entries):

StepBlockerFix shape
benchmark jobhyperfine install via DEB; bench/ci_compare.sh GNU dependenciesAdd Windows install step + audit ci_compare.sh portability

(Memory check is no longer in this table — PR #64 added a PowerShell Process.PeakWorkingSet64 branch for the Windows runner. zig build shared-lib is no longer in this table — the guard was a no-op since Zig produces zwasm.dll + zwasm.lib natively from addLibrary({.linkage = .dynamic}). zig build static-lib + static link tests are no longer in this table — the script now uses zig cc in place of system cc, which is portable; PIE coverage is preserved on Linux; Rust static-link uses zwasm.lib on Windows. examples/rust cargo run is no longer in this table — build.rs now has a Windows arm that copies zwasm.dll next to the cargo target binary for runtime discovery. Binary size check + size-matrix are no longer in this table — build.zig exposes -Dstrip=true which strips the CLI binary at link time via LLD, portable across ELF / Mach-O / PE. Run FFI tests is no longer in this table — test_ffi.c gained #ifdef _WIN32 branches for LoadLibraryA / CreateThread / _pipe, and the runner switched to zig cc.)

Nix devshell contents (current)

flake.nix provides the following on Linux/macOS via buildInputs:

PackageUsed by
zigBin (0.16.0)All builds and tests
wasmtimeRealworld compat comparison runtime
bun, nodejsbench/run_wasm.mjs, bench/run_wasm_wasi.mjs
yq-go, jqBench result transformation, history.yaml editing
hyperfineAll bench scripts
tinygobench/wasm/tgo_*.wasm and realworld/tinygo/
wasm-toolsjson-from-wast for spec test conversion, component inspection
gorealworld/go/ (GOOS=wasip1 GOARCH=wasm)
gnused, coreutilsStable shell env for bench / test scripts
python3All test/**/*.py runners
wasiSdkBin (30)WASI_SDK_PATH for realworld C / C++

Rust toolchain is not in flake.nixrealworld/build_all.py expects rustup from ~/.cargo/bin. CI installs it via rustup. The nix devshell user is expected to install Rust outside Nix. (This is called out in flake.nix as a comment.)

CI ↔ Local Gate Mapping

GateLocalCI (current)
Commit Gate (CLAUDE.md items 1-5+8)bash scripts/gate-commit.shci.yml > test job (3 OS, runs same commands)
Merge Gate (Mac + Ubuntu both clean)bash scripts/gate-merge.sh (Mac + OrbStack Ubuntu)ci.yml > test (3 OS) + size-matrix
Bench (regression + record)bash scripts/run-bench.shci.yml > benchmark (Ubuntu only today)
Nightly fuzzbash test/fuzz/fuzz_overnight.shnightly.yml > fuzz

A future PR will switch CI Linux/macOS jobs to DeterminateSystems/nix-installer-action + DeterminateSystems/magic-nix-cache-action, calling the same gate scripts under nix develop --command. Windows CI will then run scripts/windows/install-tools.ps1 (reads versions.lock) and then the gate scripts under Git Bash. This ensures CI and local invoke the exact same commands.

References

  • flake.nix — the SSoT for Linux/macOS pins
  • .github/versions.lock — the SSoT mirror for Windows / CI YAML
  • .github/workflows/ci.yml — current CI definition
  • .dev/decisions.md — D136 captures the SSoT design and Plan B/C scope
  • .dev/references/setup-orbstack.md — Apple Silicon Ubuntu VM bootstrap (manual; Nix migration tracked as Plan B follow-up)
  • .dev/references/ubuntu-testing-guide.md — Run-time recipes inside the OrbStack VM
  • CLAUDE.md — Commit Gate / Merge Gate checklists