Haskell in WSL: GHCup, Cabal Projects, and Reproducible Builds
Set up Haskell in WSL with GHCup-managed tools, explicit Cabal project files, dependency plans, native libraries, and clean Linux build checks.
Haskell development in WSL can align a workstation with Linux CI and make compiler, package, and native-library boundaries visible. GHC, Cabal, and the Haskell package ecosystem are installed inside the Linux distribution; a Windows compiler and package database are separate. Build products and package caches should not be shared between the two operating systems. A clean project build is a better signal than a successful editor session whose environment may have been assembled over years.
The reproducible unit is larger than a source tree. It includes the GHC version, Cabal version, package index and constraints, compiler flags, native system dependencies, and environment settings. Cabal can describe and resolve a Haskell package graph, but it does not lock the WSL distribution or every external library.
Manage compiler versions deliberately
GHCup is the recommended Haskell toolchain installer for many platforms and can manage GHC, Cabal, HLS, and related tools. Follow its current guide for the WSL distribution and architecture. Record the selected versions and paths before changing an existing toolchain. Multiple GHC versions can coexist, so ghc --version alone is not enough if a build script resolves another executable through PATH.
Keep the checkout, Cabal store, and build directory on the Linux filesystem. Microsoft’s WSL guidance recommends Linux storage for Linux command-line work. A Windows-mounted path may be required by a specific editor or data workflow, but it adds permissions, case, and file-watcher differences. Do not use a /mnt/c build directory as evidence that a Linux CI checkout will behave identically.
Check what the current shell resolves:
command -v ghc
ghc --numeric-version
command -v cabal
cabal --numeric-version
ghc --info | head -20
If the project specifies a GHC version, set it through the toolchain workflow used by the repository and verify that cabal build sees that compiler. Haskell Language Server should also be compatible with the selected GHC and project. An editor’s type-checking output is not a substitute for the command-line build used in CI.
Define the Cabal project boundary
A Cabal package has a .cabal file describing components, modules, dependencies, and build options. A cabal.project file can describe one or more packages and project-wide settings. Start by reading both. Do not add a new package or change dependency bounds simply to make a local resolver choose a convenient version. Track deliberate source and lock metadata according to the repository’s policy.
Typical checks from the project root are:
cabal update
cabal build all
cabal test all
cabal check
These commands have distinct roles. Updating the package index changes local solver knowledge; it is not the same as changing the project’s declared dependencies. Building all components catches compilation failures, while tests exercise only what the package defines. cabal check reports package metadata and distribution concerns; it does not prove runtime correctness. Use the exact repository CI commands as the acceptance baseline.
Cabal can write a freeze file with selected package constraints. Review it when the project needs a reproducible solver plan, and regenerate it deliberately after dependency changes. A frozen plan still depends on the compiler and platform-specific packages. If the project uses a committed cabal.project.freeze, do not silently replace it with a workstation-generated version from a newer compiler or package index.
Keep user-wide Cabal settings separate from project settings. A global configuration can choose repositories, build jobs, or default options that affect every project in the distribution. When results differ between two developers, inspect the active config path, environment variables, project file, compiler selection, and package index timestamp before editing dependency bounds.
Native libraries and foreign interfaces
Haskell packages may depend on C libraries, system headers, or tools such as pkg-config. Those inputs need Linux development packages in WSL. A dependency that links successfully on Windows may fail under GHC on Linux because the library names, ABI, headers, or search paths differ. Read the package’s installation documentation and inspect the compiler’s exact error before changing Cabal flags.
For packages with C sources or bindings, retain the build log and note the compiler and linker versions. Avoid globally setting CFLAGS, LDFLAGS, or library search paths to fix one package unless the project documents them. Environment-wide overrides can accidentally link against an unintended library and make a later clean build fail. Keep application-specific build flags in the project’s Cabal configuration or supported package settings.
If a package uses Template Haskell, generated code, or custom setup scripts, test from a clean build directory. Incremental output can mask a missing declared input. Cabal’s build cache is useful, but it is not proof that every component can be reconstructed from source on a fresh runner. Preserve the failing component and command before cleaning generated files.
Test scope, profiling, and build artifacts
Run the narrowest failing target during development, then execute the repository’s complete build and test suite before accepting changes. Record compiler optimization settings because debug, test, and release profiles can behave differently. Benchmark results should include GHC version, flags, CPU constraints, and whether the process is cold or warmed; WSL CPU topology and host scheduling can affect local timings.
Keep dist-newstyle and downloaded package caches out of source control. Before removing them, capture the package plan and error evidence. A clean build in a separate checkout can distinguish stale generated output from a source-level issue without destroying the current working state. If a project uses HLS, run the CLI build independently so editor-specific caches or plugins do not determine success.
When producing a distribution tarball, run the package checks and inspect which files are included. Verify license, source files, generated modules, and any data files the application expects at runtime. The package can build correctly in WSL and still omit a runtime asset from its source archive. Do not equate cabal build with deployment packaging validation.
Interactive development without changing the build contract
GHCi is valuable for exploring modules and evaluating expressions, but it should use the same GHC and project dependencies as the compiled test path. Launch it from the project root with the repository’s supported Cabal command, inspect the loaded modules, and avoid adding ad hoc package exposure that is absent from the project file. A REPL session can retain state or definitions that do not exist in a clean executable build, so always rerun the relevant test from a fresh process before treating an interactive result as a fix.
Compiler warnings are useful evidence. Review warning policy and flags before suppressing a warning globally; a warning may indicate an incomplete pattern, deprecated API, or type default that becomes a defect later. If the code is expected to support several GHC releases, run a compatibility matrix in CI rather than changing the local compiler repeatedly until one build passes. Capture the selected compiler and Cabal plan with each matrix result.
Separate optimization and profiling builds from ordinary correctness tests. Optimization can expose strictness or timing assumptions, and profiling may require additional compiler settings and libraries. Do not compare an optimized local binary with an unoptimized CI run and attribute the behavior to WSL. For performance reports, preserve the source revision, compiler options, workload, and whether compilation or execution time was measured.
Troubleshoot and accept the environment
If GHC is missing or unexpected, inspect GHCup’s selected version and shell startup configuration. If Cabal cannot solve dependencies, record the package index, GHC version, project file, freeze file, and constraints. If a native link fails, identify the missing Linux development library and confirm the path with the distribution package tools. If an editor reports errors but CLI tests pass, compare the HLS version and project root.
Accept the WSL Haskell environment when compiler and Cabal versions are recorded, the project uses a deliberate dependency plan, a clean Linux-side build succeeds, tests and package checks complete, and required native libraries are documented. Keep the Windows toolchain and build outputs separate from Linux artifacts.
Haskell in WSL is a dependable Linux development path when the toolchain and package plan are explicit. It does not guarantee that every package supports a given GHC release or that local compilation validates a release artifact on every deployment platform.
Related:
- Bazel in WSL: Hermetic Builds, Output Bases, and Linux Paths
- Rust in WSL: rustup Toolchains, Linux Linkers, and Target Artifacts
Sources: