Map Cabal Files To Nix Without Information Loss

The Problem

Our implementation of cabal2nix loses important information, because its mapping from Cabal to Nix is too simplistic. Cabal supports conditional values that depend on

  • the OS and architecture on which the build takes place,
  • the compiler version,
  • the respective versions of the build dependencies, and
  • Cabal flags.

But we don’t capture those conditional settings in the generate Nix file. Instead, we resolve those conditions with a set of hard-coded assumptions that work well for Linux/x86_64 using GHC 7.10.2, but other build environments often fail to compile the expression because some essential information is missing.

The Situation Today

Consider the Cabal file for conduit-1.2.5. Among other things, it says:

  Build-depends:       base              >= 4.3   && < 5
                     , resourcet         >= 1.1   && < 1.2
                     , exceptions        >= 0.6
                     , lifted-base       >= 0.1
                     , transformers-base >= 0.4.1 && < 0.5
                     , transformers      >= 0.2.2 && < 0.5
                     , mtl
                     , mmorph
  if !impl(ghc>=7.9)
    build-depends:   void                >= 0.5.5

The library depends on the module Data.Void, which GHC’s base library contains starting with version 7.9.x, but older versions of GHC don’t have that module, so the build needs an additional 3rd-party library, void, to provide that code. The Nix build expression for conduit, however, does not capture that information. cabal2nix omits void from the list of dependencies entirely:

libraryHaskellDepends = [
  base exceptions lifted-base mmorph mtl resourcet transformers

The conversion function used by cabal2nix doesn’t translate the conditional from Cabal into Nix. Instead, the function resolves all conditionals, assuming that

  • the build will take place with the same architecture, OS, and compiler version as were used to compile cabal2nix,
  • all Haskell dependencies are available in all versions, and
  • all Cabal flags have their default value unless specified otherwise in a hard-coded list.

For the majority of packages, that heuristic produces build expressions that work fine, but In case of conduit the result won’t compile with compilers prior to GHC 7.10.x, and thus a manually configured override is necessary to fix the build.

The lack of support for conditionals is particularly annoying when it comes to packages that make extensive use of Cabal flags, like git-annex. There are numerous flags that determine the feature set supported by the build, i.e. whether to include the “assistant” or not. Now, Stackage package sets like LTS Haskell do contain git-annex, but they don’t contain all the dependencies that are necessary to build the assistant. Consequently, git-annex ought to compile with -f-assistant within a context of Stackage. At the same time, we want to provide the assistant feature to Nixpkgs users — and we can, because we have all of Hackage, not just Stackage!

So what we do is this: our generated build expression for git-annex by default includes the assistant, and that is the build gitAndTools.git-annex refers to. Then we define variants of that build without the assistant, and these are the ones haskell.packages.lts-x_y.git-annex refers to. Now, we define that variant by setting the Cabal flag -f-assistant, but that override does not change the fact that all those dependencies needed only to build the assistant are still listed as build inputs in the Nix expression for git-annex! Cabal won’t use these dependencies during the build, but Nix will create them and include them into the build environment nonetheless, which defeats the purpose of having that flag in the first place.

Possible Improvements

Nix expressions must capture the complete logic that the underlying Cabal file specifies. In case of the conduit example, the generated code should say:

libraryHaskellDepends = [
  base exceptions lifted-base mmorph mtl resourcet transformers
] ++ lib.optional (lib.versionOlder ghc.version "7.9") void;

Furthermore, Cabal flags should be mapped to boolean function arguments that enable/disable the underlying feature and thereby modify how the Nix expression evaluates, so that no extraneous build inputs occur.