Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.

head.hackage CI driver

This is the application which drives GHC's head.hackage continuous integration infrastructure, namely head-hackage-ci. The general goals here are three-fold:

  1. test that the patches in the head.hackage repository build
  2. use these patches to smoke-test GHC pre-releases and nightly builds
  3. provide the patches as a Hackage repository (https://ghc.gitlab.haskell.org/head-hackage) for consumption by end-users

We accomplish this via the head-hackage-ci executable. This executable has two modes:

  • test-patches is the primary driver of the CI testing process
  • make-constraints is a utility for producing cabal-install constraints to ensure that only patched versions of packages are used in install plans

Naturally, many Haskell packages have dependencies on native libraries. head-hackage-ci supports two ways of providing these libraries:

  • from the host system: Here we just rely on the host system to provide native libraries; it is up to the user to ensure that the necessary packages are installed.

  • from nixpkgs: Here we use nix and the nixpkgs package set to provide native libraries. These dependencies are defined in ci/build-deps.nix. This mode is

Test procedure

The testing part of the CI process (goals (1) and (2) above) uses head-hackage-ci's test-patches mode and some shell scripts (namely ci/config.sh and ci/build-repo.sh) (and in the case of a Nix-based build, ci/build-deps.nix). The below is all orchestrated by run-ci.sh:

  1. Call ci/config.sh to determine the configuration of the run. This does the following:

    1. Identify the version of the compiler being tested (provided by the user via the GHC environment variable)

    2. Use the compiler version to find the set of packages that we expected to be broken.

    3. Build a set of command-line arguments destined for head-hackage-ci from the broken-package set above and a set of "extra" packages defined in config.sh

  2. If we are using nixpkgs to get native libraries: compute a cabal.project fragment from the dependency information in ci/build-deps.nix (this logic lives in ci/default.nix).

  3. Call head-hackage-ci test-patches with the computed arguments. This does the following:

    1. Determine the set of packages to test (determined by the contents of the patches/ directory and additional packages provided via the --extra-package flag)

    2. Build a local Hackage repository of patched packages using the build-repo.sh script (which itself depends upon hackage-repo-overlay-tool and hackage-overlay-tool)

    3. Build a cabal.project file containing configuration (e.g. the location of the local repository, the location of the compiler, where native dependencies are found, etc.)

    4. Call cabal v2-update to inform cabal of the patched package repository

    5. For each package to test:

      1. Create a new working directory
      2. Copy the previously constructed cabal.project into our working directory
      3. Construct a dummy Cabal package depending only on the package under test
      4. Call cabal new-build to build the package
      5. Examine the state of plan.json and the cabal logs directory to work out the outcome of the build
    6. Write a JSON report (of type Types.RunResult ()) to result.json

    7. Examine the failed units and determine whether there were any unexpected failures.

Build plans and empty patches

When testing a package, the CI driver will construct a build plan that favors versions of Hackage libraries with head.hackage patches over versions of the same library that lack patches. For example, if CI tests a library that depends on library foo that has two Hackage releases, 0.1 and 0.2, then if foo-0.1 has a patch but foo-0.2 does not, then the driver will include foo-0.1 in the build plan even though foo-0.2 has a more recent version number. This is done to reduce the likelihood of subsequent Hackage releases of foo breaking the CI due to API changes.

Sometimes, this approach can work against you. Suppose that another library bar also depends on foo. Moreover, bar requires the use of foo-0.2 and excludes foo-0.1 in its version bounds. Because foo-0.1 has a patch but foo-0.2 does not, however, the CI driver will insist on using foo-0.1 when constructing build plans, which means that it will fail to find a valid build plan for bar!

The simplest way to fix this sort of problem is to add a patch for foo-0.2. If there are patches for both foo-0.1 and foo-0.2 present, then the CI driver will admit build plans with either version of foo. In the event that foo-0.2 already compiles with all supported versions of GHC, you can simply add an empty patch by running touch patches/foo-0.2.patch.