I believe the meaning of the word “install” has also changed over time.
Before we get to that we need to introduce another “cabal”. The proposal defines packages and how to build them but not exactly how to distribute them. This is where Hackage and the cabal
command line tool come in. To avoid confusion, I always refer to it as cabal-install from the name of its package.
Originally, cabal-install used to mostly wrap the Setup.hs
interface but added new powers to its install command:
- Running
cabal install pkg-name
would automatically fetch, build and install pkg-name from Hackage.
- When called in a package directory,
cabal install
would automatically fetch the required dependencies from Hackage, install them into the user package-db (the default is different from Setup.hs
) and then build and install the package in the current directory.
Satisfying dependencies was tricky so cabal-install introduced a constraint solver to find a set of compatible packages to use. The solver also consults the package database and prefers to reuse already installed packages (the dependency solving happens at the package level) when allowed by the constraints. This seems a strange default today, but I assume it was to avoid recompiling and/or to avoid “changing” the packagedb when not strictly necessary.
I think these were the days of the “cabal hell”.
The imperative way to manipulate the packagedb would lead to disasters like this:
- Install
pkg-b
that depends on pkg-a
.
- Many days later, you install
pkg-c
that depends on a newer version of pkg-a
(without realising that this breaks pkg-b
).
- Next time you try to use
pkg-b
, you realise it does not work anymore and, in a rush to get things done, you reinstall it (without realising this will break pkg-c
).
Moreover, using Haskell for application development was coming into the picture and cabal-install could not provide any way to assist with reproducibility since its dependency solving would rely on a ever-changing Hackage index. It would also not have any support for building multi-package projects, or control precisely how packages are built (cabal install
would go all automatic and cabal build
would not do any dependency resolution).
This is where stack came into play, bringing in a maintained set of compatible packages and project-level reproduciblity.
To catch up solve these issues, cabal-install introduced a new set of commands (the v2-
commands, the previous behaviour prefixed with v1-
) that would perform “nix-style” builds:
- (Almost) any building always happen in a project context defined in a
cabal.project
file (in its absence, cabal-install will use a trivial one), with no shared state between projects.
- All dependencies are isolated through the use of hashes like nix does. The id of an entry in the packagedb will include the hash of its build configuration, which will includes the hashes of its dependencies and so on.
- Given the mechanism above, already built dependencies can safely shared between projects when their hash is equal. This is implemented through a special package database called “the store”.
- A field in
cabal.project
can be used to fix the state of Hackage’s index (index-state:
), so the set of dependencies you get today is the same as the set of dependencies you will get next month.
Nowadays, v2 commands are the default and you need to use the v1-
prefix to get the previous behaviour.
The transition between v1 and v2 commands took a long time and, to many, it is not over at all. The change has introduced subtle changes in behaviour that v1-
users would not expect. I belive most of them are related to the meaning of “install”.
Here are few examples:
Agda
The focus on reproducible project-based builds means that if you do cabal install
inside Agda
's source tree; cabal-install will
- make a source distribution
- unpack it in a temporary directory
- build it and install it from there.
This is to guaratee the result is the same as if the code would come from Hackage; but has the side effect of recompiling everything from scratch every time.
Installing libraries
How do we install a library? I believe the best conceptual model is to think that cabal v2 commands do not install anything, ever. Indeed cabal-install does not mutate the global or user package database. If you do cabal install free
, cabal-install will warn you
The command “cabal install [TARGETS]” doesn’t expose libraries.
While, if you do cabal install --lib free
, you get told
Warning: The libraries were installed by creating a global GHC environment file at:
/home/andrea/.ghc/x86_64-linux-9.8.2/environments/default
An environment file is not a packagedb! but a file that GHC can use as a replacement for a sequence of package related options.
λ cat /home/andrea/.ghc/x86_64-linux-9.8.2/environments/default
clear-package-db
global-package-db
package-db /home/andrea/.local/state/cabal/store/ghc-9.8.2-2c96/package.db
package-id base-4.19.1.0-cbb2
package-id free-5.2-fba9dd97ec45ac275c816eab9abe287a13a9b6edbd30126b68b69dd9a88f8af9
This shows what cabal-install actually did: it “installed” free into its own private packagedb and then wrote down some instructions for GHC to find it. While it seem the same this avoid the “reinstall” problems of v1 commands, while still sharing dependencies and avoiding recompilation.
(TBH I am not sure how install --lib
behaves, does it read the environment file to make a plan?)
Note that, despite being present in the store packagedb, projects will not see the “installed” library because their build environment is always defined by their respective cabal.project files. If the project includes a package which has free as a dependency and, after dependency solving, the free package hash in the build plan matches the one above; only then that package will see that unit from the store.
I better stop writing now but ask aways if something is not clear. Either I can answer or I can find where to look at.