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!