Introduction to Cabal

As described at the Cabal homepage,
Cabal is a system for building and packaging Haskell libraries and programs. It defines a common interface for package authors and distributors to easily build their applications in a portable way. Cabal is part of a larger infrastructure for distributing, organizing, and cataloging Haskell libraries and programs."
In this video we'll explore the basics of Cabal, and how you can use it to package libraries, build executables, run automated tests, and more. We'll also have a look at the family of "new-" commands.

Show Notes

Before we begin, I should clarify that the name “Cabal” is overloaded. There is a library called “Cabal”, which implements the underlying functionality used in the command line tool called “cabal-install”.

The cabal-install tool is bundled with the Haskell Platform, and is available in many package managers. To follow along with this video, make sure you have a recent version of cabal-install on your machine (version 2 or above.)

$ cabal --version
cabal-install version 2.0.0.1

When using the term “Cabal” throughout this video, I won’t use the clear distinction between the library and the command line tool, but rather refer to them as a whole.

Initializing a Package

We begin with an empty project directory called introduction-to-cabal, and in it we create a new directory greeter.

$ mkdir greeter
$ cd greeter

To create a new package we’ll use Cabal’s init command. It will ask us a bunch of questions about the project it should generate, and we’ll mostly use the default values:

Package name greeter
Package version 0.1.0.0
License PublicDomain
Author name Oskar Wickström
Maintainer email
Project homepage URL
Project synopsis Greetings with love
Category
What does the package build Library
Source directory src
Base language Haskell2010
Add informative comments n

The output shows that cabal init has guessed dependencies for our package and generated three files.

The Package Cabal File

Let’s open up greeter.cabal. We see the package name, the version, license, and author information. We’re using the Simple build type, which will cover the needs of this tutorial, and many other projects. Using Custom build types you can hook in to various build phases and customize the build. We won’t use Custom builds in this video.

The last part is a library stanza, describing the greeter library. Its only dependency is base. The hs-source-dirs property is set to src, and the default-language to Haskell2010.

library
  -- exposed-modules:
  -- other-modules:
  -- other-extensions:
  build-depends:       base >= 4.11 && <4.12
  hs-source-dirs:      src
  default-language:    Haskell2010

Next, let’s create and expose a Haskell module in our library!

Exposing a Module

Our module will be called Greeter, and to make it visible to users of this library, we add it to the list of exposed modules.

library
  exposed-modules:      Greeter
  -- other-modules:
  -- other-extensions:
  build-depends:        base >= 4.11 && <4.12
  hs-source-dirs:       src
  default-language:     Haskell2010

In the src directory, we create a new file Greeter.hs for our module. The file name needs to match the module name. We define a function greet from String to String.

module Greeter where

greet :: String -> String
greet who =
  "Hello, " <> who <> "!"

Let’s build the project and try it out!

$ cabal build
$ cabal repl
*Greeter> greet "Alice"
"Hello, Alice!"
*Greeter> greet "Bob"
"Hello, Bob!"

Press Ctrl+D to exit the GHCi REPL.

Adding Dependencies

Our library currently depends only on base. In a larger project, it’s likely that some dependencies will be acquired from Hackage. Say we want to automatically title-case our greeting, so that a name passed in lowercase will result in a title-friendly greeting.

In greeter.cabal we add a build-depends entry for the titlecase package.

library
  exposed-modules:      Greeter
  -- other-modules:
  -- other-extensions:
  build-depends:        base >= 4.11 && <4.12
                      , titlecase
  hs-source-dirs:       src
  default-language:     Haskell2010

In src/Greeter.hs, we import the Data.Text.Titlecase module and use the titlecase function when constructing the greeting.

module Greeter where

import           Data.Text.Titlecase

greet :: String -> String
greet who =
  titlecase $
  "Hello, " <> who <> "!"

Again, we run cabal build to build our package.

$ cabal build
...
cabal: Encountered missing dependencies:
titlecase -any

Oh, we’re missing the titlecase dependency. It is not available in the Cabal package store on our machine. We could run cabal install --only-dependencies to get it, but that is not a very good idea. As the traditional Cabal installation of packages can clash with other projects, we might end up breaking other projects by reinstalling dependencies within our version ranges.

There is no isolation between projects using Cabal on a single machine. A solution to this problem is Cabal sandboxes, wherein each project has a fully isolated package store and share nothing with other packages. Unfortunately, this might result in packages being rebuilt, even if the exact same versions have been built previously on your machine. Also, they will be duplicated in your filesystem and take up more space.

The “New-” Family of Commands

To support both isolation and caching of packages, the family of “new-” commands, also called Nix-style local builds, have been introduced in recent versions of Cabal. The naming scheme where commands begin with “new-” is temporary, and will be changed once the Nix-style commands become the default.

To build our package using Nix-style local builds, we invoke the new-build command.

$ cabal new-build

Not only does the new-build command build our project with isolated and cached dependencies, it also install any required dependencies before building. No need for cabal install --only-dependencies!

Analogous to the old repl command, we can invoke new-repl.

$ cabal new-repl
*Greeter> greet "mike"
"Hello, Mike!"
*Greeter> greet "simon peyton jones"
"Hello, Simon Peyton Jones!"

Building an Executable

We want our cool greeting function wrapped up in an executable program that we can send to all our friends. We create a new directory exe, and within it a new file called Main.hs. The main action will convert all command line arguments to greetings, and print them on separate lines. To use getArgs we import the System.Environment module.

module Main where

import         Greeter
import         System.Environment

main = mapM_ (putStrLn . greet) =<< getArgs

In greeter.cabal, we add an executable stanza, for the executable named greet. We depend on base, but use a less restrictive upper bound, and on our own library greeter. The source directory for the executable is exe, and the default language is again Haskell2010. Finally, the main-is property is set to Main.hs.

executable greet
  build-depends:        base >= 4.11 && <5
                      , greeter
  hs-source-dirs:       exe
  default-language:     Haskell2010
  main-is:              Main.hs

Let’s build it!

$ cabal new-build

OK, but where’s our executable? The command output does print where it linked the executable, but there is currently no programmatic way of obtaining that path. If we want to run the command from inside the project, though, we can use the new-run command.

$ cabal new-run greet alice
Up to date
Hello, Alice!
$ cabal new-run greet 'simon peyton jones'
Up to date
Hello, Simon Peyton Jones!
$ cabal new-run greet alice bob carol mike joe
Up to date
Hello, Alice!
Hello, Bob!
Hello, Carol!
Hello, Mike!
Hello, Joe!

Other Modules

Let’s say we want to have a module named Hello in our library that isn’t exposed to users of the library. The Hello module will export a function hello that we consider an implementation detail of the library.

module Hello where

hello :: String -> String
hello s = "hello, " <> s <> "!"

In Greeter.hs, we import the Hello module and compose titlecase with hello to define the greet function.

module Greeter where

import           Data.Text.Titlecase
import           Hello

greet :: String -> String
greet = titlecase . hello

Finally, we add the Hello module to other-modules, and verify that the greet executable works as before.

$ cabal new-build
$ cabal new-run greet alice bob carol mike joe
Up to date
Hello, Alice!
Hello, Bob!
Hello, Carol!
Hello, Mike!
Hello, Joe!

Had we imported the Hello module in the executable, in the Main module, we’d get an error.

$ cabal new-run greet alice bob carol mike joe
exe/Main.hs:4:1: error:
    Could not find module ‘Hello’
    it is a hidden module in the package ‘greeter-0.1.0.0’
    Use -v to see a list of the files searched for.
  |
4 | import           Hello
  | ^^^^^^^^^^^^^^^^^^^^^^

Multi-Package Projects

Recent versions of Cabal support cabal.project files, which can be used to configure projects consisting of multiple packages. Configuration options can be provided at the global project level, or for specific packages, which can be either your own local packages or the packages you depend on.

Let’s say we want write a web application that greets people, using our Greeter module. We create a new directory, at the project top level, called greeter-web, and run cabal init in there.

$ cd ..
$ mkdir greeter-web
$ cd greeter-web
$ cabal init

We use the same defaults and values as before, except it will generate only an executable.

In greeter-web/src/Main.hs we see the generated Main module. We won’t build a web application in this tutorial, only import the Greeter module and print a TODO message.

module Main where

import         Greeter

main :: IO ()
main = putStrLn "TODO: Build a greeter web app."

To use Greeter in greeter-web, we need to add the dependency on the greeter package.

executable greeter-web
  main-is:              Main.hs
  -- other-modules:
  -- other-extensions:
  build-depends:        base >= 4.11 && <4.12
                      , greeter
  hs-source-dirs:       src
  default-language:     Haskell2010

To specify the project with multiple packages, we create a new file cabal.project in the project root directory. It specifies the two packages greeter and greeter-web.

packages: greeter, greeter-web

Finally, we can build all packages in our project using the all target.

$ cd ..
$ cabal new-build all

We can also run our package executables using new-run.

$ cabal new-run greeter-web
Up to date
TODO: Build a greeter web app.

Testing Packages

To be more confident that the greeter package works as intended, we want to add automated tests. In greeter/greeter.cabal, we add a test-suite stanza named greeter-tests. We use the type called exitcode-stdio-1.0, meaning that a successful test run returns the zero exit code, and a failed test run returns a non-zero exit code. The test suite source files are put in the test directory.

test-suite greeter-tests
  type:                 exitcode-stdio-1.0
  build-depends:        base >= 4.11 && <5
                      , greeter
  hs-source-dirs:       test
  default-language:     Haskell2010
  main-is:              Test.hs

We create the test directory, and in that directory a new file called Test.hs. Typically we’d depend on a testing framework, like HUnit or Tasty, but to keep the scope of this video down we’ll write a simplified test program – we’ll verify that greet "alice" returns a properly formatted string.

module Main where

import           Greeter

main :: IO ()
main =
  case greet "alice" of
    "Hello, Alice!" -> return ()
    s -> fail ("Unexpected greeting: " <> s)

In the project root directory, we can run all test suites using new-test and the all target.

$ cabal new-test all
...
1 of 1 test suites (1 of 1 test cases) passed.

Summary

There are many more features of Cabal that we have not covered in this video. Check out cabal.readthedocs.io/en/latest/ for the latest documentation. If you are interested in the “new-” family of commands, see the section called Nix-style local builds.

Thanks for watching!