Fork me on GitHub

Haste and Cabal files

When creating a Haskell project using Haste, it’s not all that uncommon to want that project – or at least parts of it – to be buildable with vanilla GHC as well. Considering that Haste.App, which is shipped with the standard Haste distribution, more or less requires this functionality to work at all, this is quite a reasonable requirement.

Since Cabal (or stack if you want to be all cool and up to date and stuff) is the build system for Haskell projects, you probably have a .cabal file for your project:

name:                myapp
version:             0.1.0.0
author:              Jane Doe
maintainer:          janedoe@example.com
build-type:          Simple
cabal-version:       >=1.10

executable myapp
  main-is:           Main.hs
  build-depends:
    base      >= 4.8 && < 4.9,
    haste-lib >= 0.5 && < 0.6
  default-language:  Haskell2010

Your application builds just fine when using haste-cabal, but when you try to build it using vanilla cabal, you get the following message:

Resolving dependencies...
Configuring myapp-0.1.0.0...
cabal: At least the following dependencies are missing:
haste-lib ==0.5.*

That’s not good! What really went wrong here, and how can we fix it?

A stupid design (non-)decision

Once upon a time, we were in quite a rush to get Haste.App out the door in time for a paper deadline. In our haste, we did not spend too much time thinking about the package structure of its libraries. And so it came to pass that the server implementation of Haste.App ended up together with the compiler itself in the haste-compiler package. Meanwhile, the client implementation of Haste.App found itself in the haste-lib package, to avoid forcing users to install the haste-compiler package twice, with its dependencies on GHC and a multitude of other packages with highly questionable relevance to web development.

At the time, this was not much of an issue: the paper was accepted, the programming model was well received, and everything was rainbows and unicorns. Unfortunately, one glaringly obvious and exceptionally annoying implication of this design decision (or rather, non-decision) became clear as soon as people started actually using Haste.App: Cabal will become very cross indeed when it fails to find haste-lib on the server and refuse to compile the server part of Haste.Apps!

The workaround: impl(haste)

Fortunately however, the issue is quite easy to work around. Cabal lets you use conditionals over a number of properties – compile time flags, build platform, compiler used, etc. – in order to tailor your build to the present situation. When building a package with Haste, the Cabal expression impl(haste) evaluates to true, which, needless to say, it does not when building with vanilla GHC.

We can use this to tailor the dependencies of our package to the compiler being used:

name:                myapp
version:             0.1.0.0
author:              Jane Doe
maintainer:          janedoe@example.com
build-type:          Simple
cabal-version:       >=1.10

executable myapp
  main-is:           Main.hs
  build-depends:
    base      >= 4.8 && < 4.9
  if impl(haste)
    build-depends: haste-lib >= 0.5 && < 0.6
  else
    build-depends: haste-compiler >= 0.5 && < 0.6
  default-language:  Haskell2010

Note the new if impl(haste) ... stanza. If we’re being built with Haste, we still want to depend on haste-lib, while we want to depend on haste-compiler instead if we’re being compiled by GHC. Any dependencies that are shared by the client and the server go into the first, unconditional build-depends section.

Similarly, it’s also possible to add branch on the particular version of Haste being used. By replacing if impl(haste) ... with if impl(haste > 0.4) ... for instance, we can different sets of dependencies, compiler flags, etc. depending on which version of Haste is in use. While not relevant to this particular workaround, different sets of dependencies or compiler flags may not work equally well for all versions of Haste (or GHC, or any other Haskell compiler for which Cabal implements this functionality for that matter); having this in the back of your head may come in handy one day.

…and that just about sums it up. An error caused by a silly oversight two years ago, still going strong! This just goes to show that you should always think (at least!) twice about the structure of your libraries. If you’re unlucky, you may well be stuck with some embarrassing mistakes for quite some time.