Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# GHC WASM Cross-Compiler Build Instructions

## Prerequisites

### Nix Only - No ghcup!

All dependencies are provided via Nix. You only need:

```bash
# Verify Nix is installed
nix --version
```

That's it! No ghcup, no manual WASM SDK installation.

## Building WASM Cross-Compiler

### Option A: Build on linux-0 (Recommended)

```bash
./build-wasm-on-linux0.sh
```

This will:
1. Sync source code to linux-0
2. Run the Makefile-based build in nix-shell
3. Takes 1-2 hours

### Option B: Build Locally

```bash
./build-wasm-make.sh
```

Or manually:
```bash
nix-shell
make CABAL=_build/stage0/bin/cabal stage2
make CABAL=_build/stage0/bin/cabal stage3-wasm32-unknown-wasi
```

## Build Process

The build happens in two stages:

### Stage 2: Bootstrap Compiler
```bash
make CABAL=_build/stage0/bin/cabal stage2
```
Builds GHC itself using the GHC 9.8.4 bootstrap compiler from Nix.

**Time:** ~45-60 minutes

### Stage 3: WASM Cross-Compiler
```bash
make CABAL=_build/stage0/bin/cabal stage3-wasm32-unknown-wasi
```
Builds the WASM cross-compiler using stage2.

**Time:** ~30-45 minutes

## Build Output

The WASM cross-compiler will be at:
```
_build/stage3/bin/wasm32-unknown-wasi-ghc
```

## Testing

```bash
# Create a simple program
echo 'main = putStrLn "Hello from WASM!"' > hello.hs

# Compile to WASM (in nix-shell)
nix-shell --run '_build/stage3/bin/wasm32-unknown-wasi-ghc hello.hs -o hello.wasm'

# Run with wasmtime
wasmtime hello.wasm
```

## What Nix Provides

The `shell.nix` environment includes:

- **GHC 9.8.4** - Bootstrap compiler
- **Cabal, Happy, Alex** - Build tools
- **LLVM 18 with Clang** - WASM backend support
- **WASM Cross-Compiler** - From `pkgsCross.wasi32` (Clang 19 with wasilibc)
- **Build essentials** - autoconf, automake, python3, etc.

**100% from nixpkgs** - No ghcup, no manual downloads, no external WASM SDK!

## Build Times

- **Stage 2**: ~45-60 minutes (full GHC bootstrap)
- **Stage 3**: ~30-45 minutes (WASM cross-compiler)
- **Total**: ~1.5-2 hours on linux-0

## About This Implementation

This build uses:
- **Makefile-based build system** (NOT Hadrian - Hadrian is wrong!)
- **100% Nix** (NO ghcup) - provides GHC bootstrap, build tools, and LLVM
- **wasi-sdk 24.0** - official WASM/WASI SDK from WebAssembly/wasi-sdk
- **Alternative __PIC__ fix** - removes guards from `rts/wasm/Wasm.S`
- **Native adjustors** - uses native code instead of libffi (libffi is Emscripten-only)

Our changes implement proper on-demand GlobalRegs compilation as an
alternative to the patch in https://github.com/stable-haskell/ghc/issues/134.

### WASM Toolchain: wasi-sdk 24.0

The shell.nix automatically downloads and sets up wasi-sdk 24.0 which provides:
- `clang` 18.1.8 targeting wasm32-unknown-wasi
- Integrated `wasi-libc` (WASI C library with sysroot)
- LLVM binutils (`llvm-ar`, `llvm-ranlib`, `wasm-ld`)

**Why not nixpkgs pkgsCross.wasi32?**
- nixpkgs LLD 19.1.7 has a bug rejecting valid WebAssembly binary modules
- See error: "archive member '*.c.obj' is neither ET_REL nor LLVM bitcode"
- wasi-sdk 24.0 has a working linker and is the upstream recommended toolchain

The shell.nix creates wrapper scripts (not symlinks) for all WASM tools to ensure
LD_LIBRARY_PATH propagates through Cabal subprocesses to find libLLVM.so.18.1-wasi-sdk.

### libffi Incompatibility with WASI

**Important:** libffi's WebAssembly support is Emscripten-only, NOT compatible with WASI.

The RTS is patched to exclude wasm32 from the libffi-clib dependency (see rts.cabal
lines 596 and 794). The build uses native adjustors instead via the
`--disable-libffi-adjustors` flag passed to ghc-toolchain-bin.

See: https://github.com/libffi/libffi/blob/master/src/wasm/ffi.c#L33
(requires emscripten/emscripten.h, not available in WASI)

## Troubleshooting

### "nix-shell: command not found"

Install Nix:
```bash
curl -L https://nixos.org/nix/install | sh
```

### "make: *** No rule to make target 'stage3-wasm32-unknown-wasi'"

Make sure you're on the `stable-ghc-9.14` branch with the latest Makefile.

### Build fails in stage2

Make sure you're inside `nix-shell` which provides all necessary tools.

### LLVM/Clang version issues

The shell.nix uses LLVM 18. If you need a different version, edit shell.nix.

## Interactive Development

For development work:

```bash
# Enter the nix shell
nix-shell

# Now you have all tools available
ghc --version
cabal --version
llvm-config --version

# Build stages manually
make CABAL=_build/stage0/bin/cabal stage2
make CABAL=_build/stage0/bin/cabal stage3-wasm32-unknown-wasi
```

## References

- Build instructions: https://github.com/stable-haskell/ghc/issues/134
- Stable Haskell GHC: https://github.com/stable-haskell/ghc
73 changes: 73 additions & 0 deletions build-wasm-make.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Build WASM cross-compiler using Makefile (NOT Hadrian!)
# Based on https://github.com/stable-haskell/ghc/issues/134
#
# This script uses NIX flake for ALL dependencies (NO ghcup!)
# 1. Nix provides GHC bootstrap compiler and tools
# 2. Nix provides LLVM/Clang with WASM support (pinned via ghc-wasm-meta)
# 3. Builds stage2 (bootstrap native compiler)
# 4. Builds stage3-wasm32-unknown-wasi (cross-compiler + WASM libraries)
#
# The Makefile default CABAL=_build/stage0/bin/cabal is used (no override needed):
# stage0 cabal is built automatically from the repo's pinned Cabal source.

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

info() { echo -e "${BLUE}ℹ${NC} $*"; }
success() { echo -e "${GREEN}✓${NC} $*"; }
error() { echo -e "${RED}✗${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }

# Check we're in the right place
if [ ! -f "Makefile" ]; then
error "Not in GHC source directory (Makefile not found)"
exit 1
fi

info "Building WASM cross-compiler via Makefile"
info "Using Nix flake for all dependencies (NO ghcup)"
info ""

# Check for nix
if ! command -v nix >/dev/null 2>&1; then
error "nix not found. Please install Nix first."
exit 1
fi

# Build stage2: native bootstrap compiler
# Uses Makefile default CABAL=_build/stage0/bin/cabal (stage0 built automatically)
info "Step 1/2: Building stage2 (bootstrap compiler)"
info "This builds GHC itself using the bootstrap compiler..."
info "Expected time: 45-60 minutes"
info ""

nix develop . -c make stage2

echo ""
success "Stage2 build complete!"
echo ""

# Build stage3-wasm32-unknown-wasi: cross-compiler + WASM libraries
info "Step 2/2: Building stage3-wasm32-unknown-wasi (WASM cross-compiler)"
info "This builds the WASM cross-compiler using stage2..."
info "Expected time: 30-45 minutes"
info ""

nix develop . -c make stage3-wasm32-unknown-wasi

echo ""
success "WASM cross-compiler build complete!"
echo ""
echo "Cross-compiler location:"
echo " _build/stage3/bin/wasm32-unknown-wasi-ghc"
echo ""
echo "To test:"
echo " printf 'main = putStrLn \"Hello WASM\"\n' > hello.hs"
echo " nix develop . -c _build/stage3/bin/wasm32-unknown-wasi-ghc hello.hs -o hello.wasm"
67 changes: 67 additions & 0 deletions build-wasm-on-linux0.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Helper script to build WASM cross-compiler on linux-0
# Syncs the repo (source only, no _build/), then runs the Makefile-based build.
#
# Usage: ./build-wasm-on-linux0.sh
#
# Prerequisites on linux-0:
# 1. Nix must be installed with flake support
#
# Note: This script handles the case where the local repo is a git worktree
# by re-initializing a fresh git repo on the remote after syncing (nix develop
# requires the directory to be a proper git repository to evaluate flake.nix).

set -euo pipefail

REMOTE_HOST="${REMOTE_HOST:-x86_64-linux-0.lan}"
REMOTE_USER="${REMOTE_USER:-${USER}}"
REMOTE_DIR="${REMOTE_DIR:-/tmp/ghc-wasm-build}"
LOCAL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Colors
BLUE='\033[0;34m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

info() { echo -e "${BLUE}ℹ${NC} $*"; }
success() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }

info "Syncing GHC source to ${REMOTE_HOST}:${REMOTE_DIR}..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p ${REMOTE_DIR}"

# Sync the source (excluding build artifacts and git metadata).
# We exclude .git because when building from a git worktree, .git is a FILE
# pointing to the main repo on the local machine — unusable on the remote.
rsync -avz --delete \
--exclude='_build/' \
--exclude='.git' \
--exclude='.git/' \
--exclude='cabal-cache/' \
--exclude='.nix-wasm-bin/' \
--exclude='*.o' \
--exclude='*.hi' \
"${LOCAL_DIR}/" \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"

# Re-initialize git on the remote.
# nix develop requires the directory to be a valid git repo to evaluate flake.nix
# (it uses builtins.fetchGit / git+file:// URL for the local flake).
# After rsync (which excludes .git), we create a fresh repo with all files staged.
info "Re-initializing git repository on remote (needed for nix develop)..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cd ${REMOTE_DIR} && git init -q && git add -A && git commit -q -m 'build: local snapshot' 2>/dev/null || true"

success "Sync complete"

info "Starting WASM cross-compiler build on linux-0..."
info "Using Makefile-based build (stage2 → stage3-wasm32-unknown-wasi)"
warn "This will take 1-2 hours depending on the machine"
echo ""

# Run the build interactively so progress is visible
ssh -t "${REMOTE_USER}@${REMOTE_HOST}" "cd ${REMOTE_DIR} && ./build-wasm-make.sh"

success "Build completed!"
info "The WASM cross-compiler is available at:"
echo " ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/_build/stage3/bin/wasm32-unknown-wasi-ghc"
Loading
Loading