Universal build and CI testing for Haskell packages
Shell
93
569 commits
updated May 25, 2026
The goal of packcheck is to build a Haskell package in a single command. Any platform, any package, any machine configuration; you simply run one command and it will build your package, run tests, benchmarks, and doctests, generate documentation, check the package for upload to Hackage, and verify that everything builds correctly from a distribution.
Packcheck automates the discovery and installation of required tools as much as possible. You do not need to worry about installing GHC, cabal, stack, docspec, or hlint; it is all handled for you. Just run one command and get your job done. It works in local development workflows, in Nix environments, as well as in CI.
Packcheck never changes your configuration or environment, not even temporarily. It finds the requested compiler version wherever it exists in your PATH or in standard locations and uses it to compile. If not found, it can install one using ghcup, but it never changes your ghcup default compiler. Even when installing ghcup, it performs the minimal download, just the binary, and does not modify your shell configuration.
Easiest way to try it out is to download the packcheck.sh script and run
it from within your Haskell package repo locally.
curl -sLO https://raw.githubusercontent.com/composewell/packcheck/master/packcheck.sh
bash packcheck.sh cabal
One of Packcheck's core strengths is parity. You can run the exact same tests locally or in the CI.
The following github.yml uses the default GHC available on the runner
(typically the latest stable version).
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
curl -sLO https://raw.githubusercontent.com/composewell/packcheck/master/packcheck.sh
bash packcheck.sh cabal
Tip: Replace
masterwith a specific commit hash for production environments to ensure build reproducibility.
To verify your package against multiple GHC versions, use a strategy matrix:
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
ghc: ["9.14.1", "9.12.4", "9.10.3"]
steps:
- uses: actions/checkout@v4
- run: |
curl -sLO https://raw.githubusercontent.com/composewell/packcheck/master/packcheck.sh
bash packcheck.sh cabal GHCVER=${{ matrix.ghc }}
Packcheck supports Split Caching which makes CI as fast as your local testing. Packcheck can be invoked to install dependencies and exit, then we save the cache and resume. After failures CI can resume from building your package instead of installing dependencies, making it fast and amenable to iterative development.
See the github CI template | for an example use.
Packcheck provides out-of-the-box configuration files for all major OS and CI platforms. These templates include optimized, multi-stage caching to ensure your CI runs are fast and efficient. The templates are designed to work immediately for most packages. For fine-grained control, simply uncomment the labeled lines in the config files to toggle benchmarks, Haddock generation, or coverage reports.
| CI System | Platforms | Configuration Template |
|---|---|---|
| GitHub Actions | Linux, macOS, Windows | .github/workflows/packcheck.yml |
| Cirrus CI | FreeBSD | .cirrus.yml |
| Appveyor | Windows | appveyor.yml |
| CircleCI | Linux | .circleci/config.yml |
Build Tools: Cabal, Stack
GHC Provisioning: System PATH, ghcup, Stack-managed GHC
ghcup (with auto-install), or Stack.hlint
via simple environment variables.cabal-docspec. Toggle it on with a single
variable and optionally provide a custom binary URL for specific
environments.Packcheck is driven by environment variables, making it highly scriptable for both local use and CI.
# Test using a specific GHC version via ghcup
./packcheck.sh cabal GHCUP_VERSION=0.1.50.0 GHCVER=9.14.1
# Test using Stack with a specific resolver
./packcheck.sh stack RESOLVER=lts-21
# Run hlint on specific directories
./packcheck.sh hlint HLINT_TARGETS="src test"
# Run a cabal build using a stack-installed GHC
stack exec ./packcheck.sh cabal RESOLVER=lts-21
To verify the documentation snippets in your source files, enable the
docspec flag.
# Run all docspec snippets in the source
./packcheck.sh cabal \
ENABLE_DOCSPEC="y" \
DOCSPEC_URL="https://github.com/phadej/cabal-extras/releases/download/cabal-docspec-0.0.0.20250606/cabal-docspec-0.0.0.20250606-x86_64-linux.xz" \
DOCSPEC_OPTIONS="--timeout 60"
Tip: Instead of passing the options on the command line, you can also export them in your shell environment and just run
packcheck.sh cabalafter that.
CHECK_ENV=y is enabled, packcheck
ignores all environment variables and relies solely on command-line
inputs; it can also detect and report mistyped variable names. We
recommend using this mode by default, especially when troubleshooting
“ghost” issues caused by unintended environment variables or typos
(e.g., GHCVWR instead of GHCVER).GHCVER is a prefix (e.g., 9.12), Packcheck
finds the first matching binary in your PATH. Set GHCVER=head to
specifically target ghc-head.stack exec ./packcheck.sh cabal RESOLVER=lts-21HLINT_OPTIONS and
CABAL_BUILD_OPTIONS to pass raw flags directly to the underlying
tools.SKIP_POST_DEP and SKIP_PRE_DEP in
your CI YAML to manually manage where caching starts and stops.Most CI scripts are fragile collections of shell commands.
packcheck.sh is a high-level, universal "super-build" script written
in bash to ensure it works everywhere without dependencies. It
wraps the complexity of cross-platform Haskell tooling into a single,
consistent interface. Whether you are a beginner learning Haskell
metadata or a maintainer managing complex libraries, Packcheck ensures
your builds are reproducible, transparent, and fast.
Note: Any parameter can be passed either as a command-line argument or an environment variable. Command-line arguments are ideal for interactive local use, while environment variables are preferred for CI configurations.
# Show all available commands and options
./packcheck.sh --help
--------------------------------------------------
Usage
--------------------------------------------------
packcheck.sh COMMAND [PARAMETER=VALUE ...]
For example:
packcheck.sh cabal GHCVER=9.8.1
packcheck.sh stack RESOLVER=lts GHC_OPTIONS="-O0 -Werror"
packcheck.sh hlint
Ask questions: https://app.gitter.im/#/room/#composewell_streamly:gitter.im
Report issues: https://github.com/composewell/packcheck/issues/new
Control parameters can either be passed on command line or exported
as environment variables. Parameters marked DESTRUCTIVE may modify
your global user config or state.
Boolean parameters can be specified as
y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON for an affirmative value and as
n|N|no|No|NO|false|False|FALSE|off|Off|OFF or empty for a negative value.
--------------------------------------------------
Commands and flags
--------------------------------------------------
cabal : build using cabal
stack : build using stack
hlint : run hlint
clean : remove the .packcheck directory
cleanall : remove .packcheck, .stack-work directories
help | --help | -h : show this help message
--version : show packcheck version
--------------------------------------------------
Selecting tool versions
--------------------------------------------------
GHCUP_VERSION : [a.b.c.d] or 'latest' to install at $HOME/.ghcup/bin/ghcup (for versions see https://downloads.haskell.org/~ghcup)
GHCVER : [a.b.c | head] GHC version prefix (may not be enforced when using stack)
CABALVER : [a.b.c.d] Cabal version (prefix) to use
STACKVER : [a.b.c.d] Stack version (prefix) to use
STACK_UPGRADE : [y] DESTRUCTIVE! Upgrades stack to latest version
RESOLVER : Stack resolver to use for stack builds or cabal builds using stack
HLINT_VERSION : hlint version to install at $HOME/.local/bin/hlint (see [https://github.com/ndmitchell/hlint/releases](https://github.com/ndmitchell/hlint/releases))
DOCSPEC_URL : cabal-docspec release URL to install at $HOME/.local/bin/cabal-docspec (see [https://github.com/phadej/cabal-extras/releases/](https://github.com/phadej/cabal-extras/releases/))
--------------------------------------------------
Where to find the required tools
--------------------------------------------------
PATH : [path] Set PATH explicitly for predictable builds
TOOLS_DIR : [dir] Find ghc|cabal by version as in TOOLS_DIR/ghc/<version>/bin
--------------------------------------------------
Specifying common tool options
--------------------------------------------------
GHCUP_GHC_OPTIONS : Used as in "ghcup install ghc <GHCUP_GHC_OPTIONS> <version>"
GHC_OPTIONS : Specify GHC options to use
SDIST_OPTIONS : Arguments to stack/cabal sdist command
--------------------------------------------------
Specifying what to build
--------------------------------------------------
DISABLE_BENCH : [y] Do not build benchmarks, default is to build but not run
DISABLE_TEST : [y] Do not run tests, default is to run tests
DISABLE_DOCS : [y] Do not build haddocks, default is to build docs
ENABLE_DOCSPEC : [y] Run cabal-docspec after the cabal build
DISABLE_SDIST_BUILD : [y] Do not build from source distribution
DISABLE_SDIST_PROJECT_CHECK: [y] Ignore project file and continue
DISABLE_SDIST_GIT_CHECK : [y] Do not compare source distribution with git repo
DISABLE_DIST_CHECKS : [y] Do not perform source distribution checks
--------------------------------------------------
Skipping parts of build (for split caching)
--------------------------------------------------
BUILD_DEPS_ONLY : [y] Build dependencies and exit
BUILD_PACKAGE_ONLY : [y] Skip tools/deps; build local package only
--------------------------------------------------
cabal options
--------------------------------------------------
CABAL_REINIT_CONFIG : [y] DESTRUCTIVE! Remove old config to avoid incompatibility issues
CABAL_PROJECT : Alternative cabal project file, path relative to project root
CABAL_BUILD_OPTIONS : ADDITIONAL cabal build options to append to defaults
CABAL_BUILD_TARGETS : cabal build targets, default is 'all'
CABAL_HADDOCK_TARGETS : cabal haddock targets, default is '.'
CABAL_DISABLE_DEPS : [y] Do not install deps, no cabal update, useful for nix
CABAL_TEST_OPTIONS : ADDITIONAL cabal test options to append to defaults
CABAL_CHECK_RELAX : [y] Do not return failure if 'cabal check' fails on the package.
CABAL_HACKAGE_MIRROR : DESTRUCTIVE! Specify an alternative mirror, modifies the cabal config file.
CABAL_INDEX_TTL : Do not attempt cabal update until it is stale by this many hours.
HADDOCK_OPTIONS : ADDITIONAL haddock build options to append to defaults
--------------------------------------------------
stack options
--------------------------------------------------
STACK_YAML : Alternative stack config file path relative to project root
STACK_OPTIONS : ADDITIONAL stack global options (e.g. -v) to append
STACK_BUILD_OPTIONS : ADDITIONAL stack build command options to append
--------------------------------------------------
hlint options
--------------------------------------------------
HLINT_OPTIONS : hlint arguments e.g.'--datadir=.'
HLINT_TARGETS : target directories to run hlint on e.g. 'src test'
--------------------------------------------------
Coverage options
--------------------------------------------------
COVERAGE : [y] Just generate coverage information
--------------------------------------------------
Diagnostics options
--------------------------------------------------
CHECK_ENV : [y] Treat unknown env variables as error, used with env -i
BASE_TIME : System time to be used as base for timeline reporting
SDist git comparison: By default, the source distribution build
fails if the source distribution (.tar.gz) does not match the contents
of your git repository. Files not in the distribution can be listed
in .packcheck.ignore. This ensures you only release what you have
committed. You can bypass this with DISABLE_SDIST_GIT_CHECK=y.
Source distribution build is isolated for each package. In a multi-package repo, a source distribution build can be performed for only one repo at a time. When a package is built from a source distribution, it does not have visibility into changes in the other packages in the same repo. Dependencies in a source distribution build are resolved from remote sources (git or Hackage), based on how they are specified in the cabal file or cabal.project file, rather than from the current repo.
Destructive Options: Parameters marked DESTRUCTIVE! (like
STACK_UPGRADE or CABAL_REINIT_CONFIG) are safe for CI but should
be used cautiously on local machines as they modify global user
configurations.
Shell
98.4%
Universal build and CI testing for Haskell packages
Shell
93
569 commits
updated May 25, 2026
The goal of packcheck is to build a Haskell package in a single command. Any platform, any package, any machine configuration; you simply run one command and it will build your package, run tests, benchmarks, and doctests, generate documentation, check the package for upload to Hackage, and verify that everything builds correctly from a distribution.
Packcheck automates the discovery and installation of required tools as much as possible. You do not need to worry about installing GHC, cabal, stack, docspec, or hlint; it is all handled for you. Just run one command and get your job done. It works in local development workflows, in Nix environments, as well as in CI.
Packcheck never changes your configuration or environment, not even temporarily. It finds the requested compiler version wherever it exists in your PATH or in standard locations and uses it to compile. If not found, it can install one using ghcup, but it never changes your ghcup default compiler. Even when installing ghcup, it performs the minimal download, just the binary, and does not modify your shell configuration.
Easiest way to try it out is to download the packcheck.sh script and run
it from within your Haskell package repo locally.
curl -sLO https://raw.githubusercontent.com/composewell/packcheck/master/packcheck.sh
bash packcheck.sh cabal
One of Packcheck's core strengths is parity. You can run the exact same tests locally or in the CI.
The following github.yml uses the default GHC available on the runner
(typically the latest stable version).
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
curl -sLO https://raw.githubusercontent.com/composewell/packcheck/master/packcheck.sh
bash packcheck.sh cabal
Tip: Replace
masterwith a specific commit hash for production environments to ensure build reproducibility.
To verify your package against multiple GHC versions, use a strategy matrix:
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
ghc: ["9.14.1", "9.12.4", "9.10.3"]
steps:
- uses: actions/checkout@v4
- run: |
curl -sLO https://raw.githubusercontent.com/composewell/packcheck/master/packcheck.sh
bash packcheck.sh cabal GHCVER=${{ matrix.ghc }}
Packcheck supports Split Caching which makes CI as fast as your local testing. Packcheck can be invoked to install dependencies and exit, then we save the cache and resume. After failures CI can resume from building your package instead of installing dependencies, making it fast and amenable to iterative development.
See the github CI template | for an example use.
Packcheck provides out-of-the-box configuration files for all major OS and CI platforms. These templates include optimized, multi-stage caching to ensure your CI runs are fast and efficient. The templates are designed to work immediately for most packages. For fine-grained control, simply uncomment the labeled lines in the config files to toggle benchmarks, Haddock generation, or coverage reports.
| CI System | Platforms | Configuration Template |
|---|---|---|
| GitHub Actions | Linux, macOS, Windows | .github/workflows/packcheck.yml |
| Cirrus CI | FreeBSD | .cirrus.yml |
| Appveyor | Windows | appveyor.yml |
| CircleCI | Linux | .circleci/config.yml |
Build Tools: Cabal, Stack
GHC Provisioning: System PATH, ghcup, Stack-managed GHC
ghcup (with auto-install), or Stack.hlint
via simple environment variables.cabal-docspec. Toggle it on with a single
variable and optionally provide a custom binary URL for specific
environments.Packcheck is driven by environment variables, making it highly scriptable for both local use and CI.
# Test using a specific GHC version via ghcup
./packcheck.sh cabal GHCUP_VERSION=0.1.50.0 GHCVER=9.14.1
# Test using Stack with a specific resolver
./packcheck.sh stack RESOLVER=lts-21
# Run hlint on specific directories
./packcheck.sh hlint HLINT_TARGETS="src test"
# Run a cabal build using a stack-installed GHC
stack exec ./packcheck.sh cabal RESOLVER=lts-21
To verify the documentation snippets in your source files, enable the
docspec flag.
# Run all docspec snippets in the source
./packcheck.sh cabal \
ENABLE_DOCSPEC="y" \
DOCSPEC_URL="https://github.com/phadej/cabal-extras/releases/download/cabal-docspec-0.0.0.20250606/cabal-docspec-0.0.0.20250606-x86_64-linux.xz" \
DOCSPEC_OPTIONS="--timeout 60"
Tip: Instead of passing the options on the command line, you can also export them in your shell environment and just run
packcheck.sh cabalafter that.
CHECK_ENV=y is enabled, packcheck
ignores all environment variables and relies solely on command-line
inputs; it can also detect and report mistyped variable names. We
recommend using this mode by default, especially when troubleshooting
“ghost” issues caused by unintended environment variables or typos
(e.g., GHCVWR instead of GHCVER).GHCVER is a prefix (e.g., 9.12), Packcheck
finds the first matching binary in your PATH. Set GHCVER=head to
specifically target ghc-head.stack exec ./packcheck.sh cabal RESOLVER=lts-21HLINT_OPTIONS and
CABAL_BUILD_OPTIONS to pass raw flags directly to the underlying
tools.SKIP_POST_DEP and SKIP_PRE_DEP in
your CI YAML to manually manage where caching starts and stops.Most CI scripts are fragile collections of shell commands.
packcheck.sh is a high-level, universal "super-build" script written
in bash to ensure it works everywhere without dependencies. It
wraps the complexity of cross-platform Haskell tooling into a single,
consistent interface. Whether you are a beginner learning Haskell
metadata or a maintainer managing complex libraries, Packcheck ensures
your builds are reproducible, transparent, and fast.
Note: Any parameter can be passed either as a command-line argument or an environment variable. Command-line arguments are ideal for interactive local use, while environment variables are preferred for CI configurations.
# Show all available commands and options
./packcheck.sh --help
--------------------------------------------------
Usage
--------------------------------------------------
packcheck.sh COMMAND [PARAMETER=VALUE ...]
For example:
packcheck.sh cabal GHCVER=9.8.1
packcheck.sh stack RESOLVER=lts GHC_OPTIONS="-O0 -Werror"
packcheck.sh hlint
Ask questions: https://app.gitter.im/#/room/#composewell_streamly:gitter.im
Report issues: https://github.com/composewell/packcheck/issues/new
Control parameters can either be passed on command line or exported
as environment variables. Parameters marked DESTRUCTIVE may modify
your global user config or state.
Boolean parameters can be specified as
y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON for an affirmative value and as
n|N|no|No|NO|false|False|FALSE|off|Off|OFF or empty for a negative value.
--------------------------------------------------
Commands and flags
--------------------------------------------------
cabal : build using cabal
stack : build using stack
hlint : run hlint
clean : remove the .packcheck directory
cleanall : remove .packcheck, .stack-work directories
help | --help | -h : show this help message
--version : show packcheck version
--------------------------------------------------
Selecting tool versions
--------------------------------------------------
GHCUP_VERSION : [a.b.c.d] or 'latest' to install at $HOME/.ghcup/bin/ghcup (for versions see https://downloads.haskell.org/~ghcup)
GHCVER : [a.b.c | head] GHC version prefix (may not be enforced when using stack)
CABALVER : [a.b.c.d] Cabal version (prefix) to use
STACKVER : [a.b.c.d] Stack version (prefix) to use
STACK_UPGRADE : [y] DESTRUCTIVE! Upgrades stack to latest version
RESOLVER : Stack resolver to use for stack builds or cabal builds using stack
HLINT_VERSION : hlint version to install at $HOME/.local/bin/hlint (see [https://github.com/ndmitchell/hlint/releases](https://github.com/ndmitchell/hlint/releases))
DOCSPEC_URL : cabal-docspec release URL to install at $HOME/.local/bin/cabal-docspec (see [https://github.com/phadej/cabal-extras/releases/](https://github.com/phadej/cabal-extras/releases/))
--------------------------------------------------
Where to find the required tools
--------------------------------------------------
PATH : [path] Set PATH explicitly for predictable builds
TOOLS_DIR : [dir] Find ghc|cabal by version as in TOOLS_DIR/ghc/<version>/bin
--------------------------------------------------
Specifying common tool options
--------------------------------------------------
GHCUP_GHC_OPTIONS : Used as in "ghcup install ghc <GHCUP_GHC_OPTIONS> <version>"
GHC_OPTIONS : Specify GHC options to use
SDIST_OPTIONS : Arguments to stack/cabal sdist command
--------------------------------------------------
Specifying what to build
--------------------------------------------------
DISABLE_BENCH : [y] Do not build benchmarks, default is to build but not run
DISABLE_TEST : [y] Do not run tests, default is to run tests
DISABLE_DOCS : [y] Do not build haddocks, default is to build docs
ENABLE_DOCSPEC : [y] Run cabal-docspec after the cabal build
DISABLE_SDIST_BUILD : [y] Do not build from source distribution
DISABLE_SDIST_PROJECT_CHECK: [y] Ignore project file and continue
DISABLE_SDIST_GIT_CHECK : [y] Do not compare source distribution with git repo
DISABLE_DIST_CHECKS : [y] Do not perform source distribution checks
--------------------------------------------------
Skipping parts of build (for split caching)
--------------------------------------------------
BUILD_DEPS_ONLY : [y] Build dependencies and exit
BUILD_PACKAGE_ONLY : [y] Skip tools/deps; build local package only
--------------------------------------------------
cabal options
--------------------------------------------------
CABAL_REINIT_CONFIG : [y] DESTRUCTIVE! Remove old config to avoid incompatibility issues
CABAL_PROJECT : Alternative cabal project file, path relative to project root
CABAL_BUILD_OPTIONS : ADDITIONAL cabal build options to append to defaults
CABAL_BUILD_TARGETS : cabal build targets, default is 'all'
CABAL_HADDOCK_TARGETS : cabal haddock targets, default is '.'
CABAL_DISABLE_DEPS : [y] Do not install deps, no cabal update, useful for nix
CABAL_TEST_OPTIONS : ADDITIONAL cabal test options to append to defaults
CABAL_CHECK_RELAX : [y] Do not return failure if 'cabal check' fails on the package.
CABAL_HACKAGE_MIRROR : DESTRUCTIVE! Specify an alternative mirror, modifies the cabal config file.
CABAL_INDEX_TTL : Do not attempt cabal update until it is stale by this many hours.
HADDOCK_OPTIONS : ADDITIONAL haddock build options to append to defaults
--------------------------------------------------
stack options
--------------------------------------------------
STACK_YAML : Alternative stack config file path relative to project root
STACK_OPTIONS : ADDITIONAL stack global options (e.g. -v) to append
STACK_BUILD_OPTIONS : ADDITIONAL stack build command options to append
--------------------------------------------------
hlint options
--------------------------------------------------
HLINT_OPTIONS : hlint arguments e.g.'--datadir=.'
HLINT_TARGETS : target directories to run hlint on e.g. 'src test'
--------------------------------------------------
Coverage options
--------------------------------------------------
COVERAGE : [y] Just generate coverage information
--------------------------------------------------
Diagnostics options
--------------------------------------------------
CHECK_ENV : [y] Treat unknown env variables as error, used with env -i
BASE_TIME : System time to be used as base for timeline reporting
SDist git comparison: By default, the source distribution build
fails if the source distribution (.tar.gz) does not match the contents
of your git repository. Files not in the distribution can be listed
in .packcheck.ignore. This ensures you only release what you have
committed. You can bypass this with DISABLE_SDIST_GIT_CHECK=y.
Source distribution build is isolated for each package. In a multi-package repo, a source distribution build can be performed for only one repo at a time. When a package is built from a source distribution, it does not have visibility into changes in the other packages in the same repo. Dependencies in a source distribution build are resolved from remote sources (git or Hackage), based on how they are specified in the cabal file or cabal.project file, rather than from the current repo.
Destructive Options: Parameters marked DESTRUCTIVE! (like
STACK_UPGRADE or CABAL_REINIT_CONFIG) are safe for CI but should
be used cautiously on local machines as they modify global user
configurations.
Shell
98.4%