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.
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”.
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 18.104.22.168
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
$ 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:
|Author name||Oskar Wickström|
|Project homepage URL|
|Project synopsis||Greetings with love|
|What does the package build||Library|
|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
hs-source-dirs property is set to
src, and the
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
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
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.
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.
greeter.cabal we add a
build-depends entry for the
library exposed-modules: Greeter -- other-modules: -- other-extensions: build-depends: base >= 4.11 && <4.12 , titlecase hs-source-dirs: src default-language: Haskell2010
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
$ 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
$ 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 action will convert all command line arguments to greetings, and print them on separate lines. To use
getArgs we import the
module Main where import Greeter import System.Environment main = mapM_ (putStrLn . greet) =<< getArgs
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
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
$ 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!
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 <> "!"
Greeter.hs, we import the
Hello module and compose
hello to define the
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 | ^^^^^^^^^^^^^^^^^^^^^^
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.
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."
greeter-web, we need to add the dependency on the
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
packages: greeter, greeter-web
Finally, we can build all packages in our project using the
$ cd .. $ cabal new-build all
We can also run our package executables using
$ cabal new-run greeter-web Up to date TODO: Build a greeter web app.
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
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-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
$ cabal new-test all ... 1 of 1 test suites (1 of 1 test cases) passed.
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!