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-ddfor the latest set,lts-mm.nnfor stable packages andghcfor 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
alltarget includes every component. - The
all:ctypetarget includes every component of a certain component type (ctype), such aslibs,exesandtests. cabal-install:exe:cabalis a fully qualified name forcabal, as anexecomponent of thecabal-installpackage.
$ 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,4576If 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.0The 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: 9275eea7982dabbf47be2ba078ced669ae7ef3d5Package (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
cabalso it is best not to also install it yourself by other means unless you intend to hack on Cabal.↩︎
