Introduction
Cabal is an acronym of Common Architecture for Building
Applications and Libraries. Central to this architecture is cabal
, the
executable (exe) build tool from the cabal-install package.
This architecture defines a common interface for package authors and distributors to easily build their applications in a portable way. It is part of a larger infrastructure for distributing, organizing, and cataloging Haskell libraries and programs. Specifically, it describes what a package is, how these interact with the language, and what Haskell implementations (compilers) must to do to support packages. It also specifies some infrastructure (code) that makes it easy for tool authors to build and distribute conforming packages.
Here's an elided dump of cabal --help
showing the fundamental commands:
$ cabal --help
Command line interface to the Haskell Cabal infrastructure.
Commands:
[initialization and download]
init Create a new cabal package.
[project building and installing]
build Compile targets within the project.
clean Clean the package store and remove temporary files.
[running and testing]
repl Open an interactive session for the given component.
run Run an executable.
test Run test-suites.
[sanity checks and shipping]
sdist Generate a source distribution file (.tar.gz).
The architecture says nothing about more global issues such as how authors decide where in the module name space their library should live; how users can find a package they want; how orphan packages find new owners; and so on.
Packages
When developing anything non-trivial in Haskell, anything that larger than a
single .hs
file, we pack modules (.hs
files) into packages.
- Module - The unit of compilation in Haskell is the module. A module is a collection of related functions, types, and type classes. Modules are the primary unit of organization in Haskell programs.
- Package - The unit of distribution in Haskell is the package. A package is a collection of one or more modules, plus some metadata about the package (e.g., its name, version, and dependencies). Package components include libraries, executables, test suites and benchmarks.
Package source code can be published to Hackage and vetted by Stackage.
Hackage - A central repository of Haskell packages. Hackage is a website that hosts packages and provides tools for searching, browsing, and downloading packages.
Stackage - For each resolver Stackage pairs a GHC compiler version with a groups of packages from Hackage that build together. This group is pins each included package to an exact version.
Stackage resolvers come in three sets;
nightly-yyyy-mm-dd
for the latest set,lts-mm.nn
for stable packages andghc
for the very limited set of packages released with GHC. Eachlts-mm.*
series imposes some rules on included packages, such as not allowing breaking version bumps.
Demo
Having installed GHC
and cabal
with GHCup on Ubuntu Linux, let's
find cabal
;
$ type cabal
cabal is ~/.ghcup/bin/cabal
$ readlink ~/.ghcup/bin/cabal
cabal-3.10.3.0
$ type cabal-3.10.3.0
cabal-3.10.3.0 is ~/.ghcup/bin/cabal-3.10.3.0
We can use cabal
to download its own package, cabal-install
, from Hackage.
A package is its description, in a .cabal
file, and all of the source referred
to by that description. The package description file name must be the same as
the name of the package. An sdist
is a .tar.gz
archive of a package.
$ VER=3.10.3.0 cabal get cabal-install-{$VER}
Downloading cabal-install-3.10.3.0
Downloaded cabal-install-3.10.3.0
Unpacking to cabal-install-3.10.3.0/
$ VER=3.10.3.0 cat cabal-install-{$VER}/cabal-install.cabal
Cabal-Version: 2.2
Name: cabal-install
Version: 3.10.3.0
...
executable cabal
...
We can install cabal
it again1, showing that cabal
can build and install
local packages from source and when doing so downloads dependencies from
Hackage, in this case cabal-install-solver
and hackage-security
.
We can (and oftentimes must) use targets to specify some or all
of the components of a package. Many cabal
build tool commands require a
target and in fact, cabal build
will fail if not given a target.
- The
all
target includes every component. - The
all:ctype
target includes every component of a certain component type (ctype
), such aslibs
,exes
andtests
. cabal-install:exe:cabal
is a fully qualified name forcabal
, as anexe
component of thecabal-install
package.
$ cd cabal-install-3.10.3.0/
$ cabal install cabal-install:exe:cabal --overwrite-policy=always
Wrote tarball sdist to
/.../cabal-install-3.10.3.0/dist-newstyle/sdist/cabal-install-3.10.3.0.tar.gz
Resolving dependencies...
Build profile: -w ghc-9.8.2 -O1
In order, the following will be built (use -v for more details):
- cabal-install-solver-3.10.3.0 (lib) (requires download & build)
- hackage-security-0.6.2.6 (lib) (requires build)
- cabal-install-3.10.3.0 (lib) (requires build)
- cabal-install-3.10.3.0 (exe:cabal) (requires build)
Downloading cabal-install-solver-3.10.3.0
Starting hackage-security-0.6.2.6 (lib)
Downloaded cabal-install-solver-3.10.3.0
Starting cabal-install-solver-3.10.3.0 (lib)
Building hackage-security-0.6.2.6 (lib)
Building cabal-install-solver-3.10.3.0 (lib)
Installing hackage-security-0.6.2.6 (lib)
Completed hackage-security-0.6.2.6 (lib)
Installing cabal-install-solver-3.10.3.0 (lib)
Completed cabal-install-solver-3.10.3.0 (lib)
Starting cabal-install-3.10.3.0 (lib)
Building cabal-install-3.10.3.0 (lib)
Installing cabal-install-3.10.3.0 (lib)
Completed cabal-install-3.10.3.0 (lib)
Starting cabal-install-3.10.3.0 (exe:cabal)
Building cabal-install-3.10.3.0 (exe:cabal)
Installing cabal-install-3.10.3.0 (exe:cabal)
Completed cabal-install-3.10.3.0 (exe:cabal)
Symlinking 'cabal' to '~/.cabal/bin/cabal'
There is currently no command to show the available targets, but cabal targets
has been proposed for this purpose with cabal#9744.
Projects
Even larger, projects are a collections of packages. These allow us to develop a set of related packages, to develop a product and to depend on unpublished packages that we can get from source code repositories or other means.
- Source Code Repositories - It is possible to depend on packages that are in a source code repository.
- Vendored Packages - When source code for a package is copied locally and used if it was a local package.
Building with Stackage
There are two main build tools in the Haskell ecosystem, Cabal and Stack with
exe names cabal
and stack
. The main difference between them is how they deal
with dependencies in their projects. Stack works with a Stackage resolver and
any dependency that is not included in the resolver must be pinned to an exact
version as an extra dependency. Cabal can work this way too but has a built-in
dependency solver that will pick versions of dependencies that are not pinned,
if it can.
Stack's project is a .yaml
file and Cabal's is a .project
file (by
convention). Each tools' command allows specifying alternate projects with
options, shown below specifying the default project names.
--stack-yaml=stack.yaml
--project-file=cabal.project
Both tools will pick up the project implicitly if it uses the default project file name.
Everything in a Stack project must be in the one file. That's a limitation of
YAML
. The .project
file format has no formal specification but uses the same
parser as the .cabal
format that does, reusing some of the same fields as well
as having some fields specific to a project. The import
field can be used to
import another project file configuration fragment and this is how Cabal can use
Stackage resolvers.
import: https://www.stackage.org/nightly-2024-06-13/cabal.config
$ curl -fsSL https://www.stackage.org/nightly-2024-06-13/cabal.config
...
with-compiler: ghc-9.8.2
constraints: abstract-deque ==0.3,
abstract-deque-tests ==0.3,
abstract-par ==0.3.3,
...
zlib-clib ==1.3.1,
zot ==0.0.3,
zstd ==0.1.3.0
While importing directly from Stackage may work, it can lead to unsolveable
version constraints. The way around this is to download the cabal.config
from
Stackage for the resolver you want to use and then to comment out any
conflicting version constraints.
While Stack can work with Stackage ghc-x.y.z
resolvers, no such resolver is
provided in cabal.config
(cabal project) format.
$ curl -fsSL https://www.stackage.org/ghc-9.8.2/cabal.config
curl: (22) The requested URL returned error: 404
Building without Stackage
A Cabal project can be set up without importing constraints from Stackage. If
you encounter constraint solver failures then picking a subset of a Stackage
resolvers' version constraints may help resolve the problem quicker than
fiddling with version choices and allow-newer
exeptions until the constraint
solver is happy.
Extra Dependencies
Extra dependencies are a Stack concept that doesn't really exist in Cabal.
These are exact versions of packages that a project depends on that are not
included with the resolver in use (snapshot
is a synonym of resolver
). Here
are Stack's own extra dependencies:
snapshot: lts-22.21 # GHC 9.6.5
extra-deps:
# lts-22.21 provides pantry-0.9.3.2.
- pantry-0.10.0@sha256:6f99ee8d7cfeeb0e2513638618acf80c72d018e7f10120048fa120a409b9dcd2,7864
# lts-22.21 provides tar-0.5.1.1, which does not support Unicode filenames:
- tar-0.6.2.0@sha256:619828cae098a7b6deeb0316e12f55011101d88f756787ed024ceedb81cf1eba,4576
If you're committing the stack.yaml.lock
file then it is enough to leave off
the hash as the .lock
file contains that detail. This way we can see that
Stack's extra-deps
are exact version equality constraints.
extra-deps:
- pantry-0.10.0
- tar-0.6.2.0
The same exact version equality constraints in a cabal.project
would be:
constraints:
pantry ==0.10.0
, tar ==0.6.2.0
Stack needs these versions to be explicitly declared but Cabal will use its dependency solver to fill in the gaps.
Whether using Stack or Cabal, the versions of dependencies needs to fit within
the version ranges for dependencies as specified in the build-depends
field of
the package description. There is a way of relaxing the version constraints with
--allow-newer
but this should only be used as a temporary workaround in order
to get a project to build. If there's a version range bound problem with a
dependency then you may fork it and fix the problem there (and hopefully
upstreaming the change) or you can ask the maintainer for a Hackage revision of
the package to relax an upper bound.
Source Dependencies
From this snippet of a cabal.project
for
unison/cabal.project, we can see that it is possible to
pull in dependencies from a source code repository (in addition to some version
constraints) with source-repository-package
stanzas.
import: ./project-stackage/lts-20.26.config
source-repository-package
type: git
location: https://github.com/unisonweb/configurator
tag: e47e9e9fe1f576f8c835183b9def52d73c01327a
source-repository-package
type: git
location: https://github.com/unisonweb/haskeline
tag: 9275eea7982dabbf47be2ba078ced669ae7ef3d5
The Stack equivalent is very similar:
resolver: lts-20.26
extra-deps:
- git: https://github.com/unisonweb/configurator
commit: e47e9e9fe1f576f8c835183b9def52d73c01327a
- git: https://github.com/unisonweb/haskeline
commit: 9275eea7982dabbf47be2ba078ced669ae7ef3d5
Package (and Description) Generators
Starting with a blank slate, cabal init
can be used to generate a skeleton
package. This command will interactively walk you through package creation,
collecting required inputs and asking you to choose from the available options.
$ mkdir flash-new-package
$ cd flash-new-package/
$ cabal init
What does the package build:
1) Library
* 2) Executable
3) Library and Executable
4) Test suite
...
Add informative comments to each field in the cabal file. (y/n)? [default: y]
[Log] Using cabal specification: 3.0
[Log] Creating fresh file LICENSE...
[Log] Creating fresh file CHANGELOG.md...
[Log] Creating fresh directory ./app...
[Log] Creating fresh file app/Main.hs...
[Log] Creating fresh file flash-new-package.cabal...
[Warning] No synopsis given. You should edit the .cabal file and add one.
[Info] You may want to edit the .cabal file and add a Description field.
$ tree .
.
├── app
│ └── Main.hs
├── CHANGELOG.md
├── flash-new-package.cabal
└── LICENSE
Stack bundles hpack. This is a package description file generator that
generates a .cabal
file from a package.yaml
. This is similar to the .cabal
description except that it used different names for some fields (different
phraseology) and can infer some fields (detecting modules from disk) and expand
on terser values. The hpack
command can be used standalone and there is a
similar dhall-hpack-cabal
command that takes its import from a package.dhall
file. Ultimately Stack deals with the .cabal
file whether it is generated or
not, the same way Cabal does, as the package description.
We recommend using GHCup to install
cabal
so it is best not to also install it yourself by other means unless you intend to hack on Cabal.↩︎