Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 13 additions & 28 deletions doc/cabal-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -499,38 +499,23 @@ flag.
cabal freeze
^^^^^^^^^^^^

If a package is built in several different environments, such as a
development environment, a staging environment and a production
environment, it may be necessary or desirable to ensure that the same
dependency versions are selected in each environment. This can be done
with the ``freeze`` command:

``cabal freeze`` writes out a **freeze file** which records all of

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this section used to introduce the phrase "freeze file" for discussing such files regardless of their particular file name. I've found this vocabulary helpful when discussing the management of such files. I would suggest keeping it around.

the versions and flags that are picked by the solver under the
current index and flags. Default name of this file is
``cabal.project.freeze`` but in combination with a
``--project-file=my.project`` flag (see :ref:`project-file
<cmdoption-project-file>`)
the name will be ``my.project.freeze``.
A freeze file has the same syntax as ``cabal.project`` and looks
something like this:

.. highlight:: cabal
.. code-block:: console

::
$ cabal freeze

constraints: HTTP ==4000.3.3,
HTTP +warp-tests -warn-as-error -network23 +network-uri -mtl1 -conduit10,
QuickCheck ==2.9.1,
QuickCheck +templatehaskell,
-- etc...
generates ``cabal.project.freeze`` file, which describes the exact dependency
tree as it was resolved at that moment by Cabal. This means it captures an
exact version of every dependency, including dependencies of dependencies,
recursively all the way.

Since ``cabal`` reads ``cabal.project.freeze`` when present, and takes into
consideration the version constraints in it, this means that by producing
``cabal.project.freeze`` you are guaranteed that every future ``cabal`` call
will use the exact same set of dependencies, regardless of any updates (even
patches) that might get published for these dependencies in the meantime.
Therefore, we have effectively "frozen" the dependencies in place.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also mention that while cabal.project.freeze restricts specified dependency versions, it does NOT prevent from including future dependencies unless reject-unconstrained-dependencies=all is specified


For end-user executables, it is recommended that you distribute the
``cabal.project.freeze`` file in your source repository so that all
users see a consistent set of dependencies. For libraries, this is not
recommended: users often need to build against different versions of
libraries than what you developed against.
``cabal.project.freeze`` is intended to be committed to the version control.
Comment on lines -529 to +518

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see a lot more discussion about this recommendation. Firstly, as noted elsewhere, a cabal.project.freeze file pins a particular version of base, and hence GHC. What if my CI tests against multiple versions of GHC?

Also, why did this recommendation change as part of this PR? What was the rationale behind the previous recommendation (check it in for executables and not for libraries), and what has changed since it was written?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously the ecosystems and tools are quite different, but for reference, Cargo does unconditionally recommend checking in Cargo.lock files (similar to how this now recommends checking in cabal.project.freeze files). However, they have a much more thorough discussion of the considerations and consequences of doing so.


.. _cabal-gen-bounds:

Expand Down
2 changes: 1 addition & 1 deletion doc/cabal-project-description-file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ file with ``profiling: True``.

The full configuration of a project is determined by combining the
following sources (later entries override earlier ones, except for appendable
options):
options, like dependency version constraints):

1. :ref:`The user-wide global configuration <config-file-discovery>` (default: ``~/.config/cabal/config``)

Expand Down
256 changes: 256 additions & 0 deletions doc/how-to-freeze-versions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
How to control versions
=======================

There are various ways and places to limit what versions the solver can pick for
dependencies.

Version ranges
Within a package description, version ranges for dependencies can be; tight
or loose or missing altogether.

Version constraints
Within a project, version constraints for dependencies limit the versions
that the solver can pick.

Curated version sets
A project can import curated sets of packages and versions that are known to
work together, such as those Stackage provides for Cabal's use.

Capped repository versions
Adding ``index-state`` to a project limits versions coming from Hackage to
include only those that were available at the cutoff time.

Frozen versions
Pins the versions picked by the solver for all dependencies. This is a way
to preserve a set of versions found by the solver, a solver-curated set, if
you will.

Version exceptions
Allow newer or older dependencies.

Version ranges
--------------

A dependency can have a version range specified in the package description.
Published packages are expected to attach version ranges to dependencies that
conform to a set of rules that ``cabal check`` command will check for and report
on:

- that lower bounds are inclusive
- that upper bounds are exclusive
- that upper bounds don't have trailing zeros
- that upper bounds are not missing
- that upper bounds are not missing for ``base``

For large projects with many packages, it would be a lot of work to keep all
package dependency version ranges up to date. Ways of overcoming this are:

- use a bash script to replace or update version ranges in all package descriptions
- use a package generator like ``hpack-dhall-cabal`` to import sets of dependencies with version ranges
- use a ``cabal.project`` file to specify version constraints for some or all dependencies
- use a curated set of packages and versions and import these into the project

Version constraints
-------------------

Version constraints can be applied to the project or command line.

.. Note::

While they do not directly restrain dependency versions, flag constraints
can alter the set of dependencies of a package.

.. Warning::

Version constraints are additive. If I add a constraint it doesn't remove or
replace prior constraints on versions. Constraints don't have override
semantics.

Curated version sets
--------------------

Stackage provides curated sets of packages and versions that are known to work
together and are updated regularly. The latest resolver is the nightly and this
typically lags a bit behind the latest available GHC version. The LTS resolvers
will each follow a specific GHC version and are updated less frequently.

Capped repository versions
--------------------------

.. _freeze-versions:

Frozen versions
---------------

Pinning adds a version equality constraint for each package in the set of
project dependencies, explicit and transitive. The ``cabal freeze`` command
saves these to a file named the same as the whole of the project file name but
with a extra ``.freeze`` extension, so the freeze file for ``cabal.project`` is
``cabal.project.freeze``. Effectively a ``.freeze`` file is an implicit project
import, same as the ``.local`` file for projects.

.. Warning::

The order of imports of ``.local`` and ``.freeze`` files is important. The
``.local`` file is imported last, after the ``.freeze`` file, giving the
user a final say in the setting of any fields that have override semantics.

.. Warning::

A project or package that may build with a variety of compiler versions will
be locked to a particular compiler version by freezing. For instance, if
``cabal init`` is used to create an executable package,
``unconstrained-base-exe.cabal``, after removing version constraints on base
it can be frozen with various compiler versions that will each bring in
their set of boot libraries as dependencies.

.. code-block:: cabal

executable unconstrained-base-exe
build-depends: base >0

.. code-block:: diff

$ cabal freeze --with-compiler=ghc-8.10.7
$ mv cabal.project.freeze ghc-8.10.7.freeze

$ cabal freeze --with-compiler=ghc-9.10.1
$ mv cabal.project.freeze ghc-9.10.1.freeze

$ cabal freeze --with-compiler=ghc-9.12.1
$ mv cabal.project.freeze ghc-9.12.1.freeze

$ diff ghc-8.10.7.freeze ghc-9.12.1.freeze --unified
-constraints: any.base ==4.14.3.0,
- any.ghc-prim ==0.6.1,
- any.integer-gmp ==1.0.3.0,
- any.rts ==1.0.1
+constraints: any.base ==4.21.0.0,
+ any.ghc-bignum ==1.3,
+ any.ghc-internal ==9.1201.0,
+ any.ghc-prim ==0.13.0,
+ any.rts ==1.0.2

$ diff ghc-9.10.1.freeze ghc-9.12.1.freeze --unified
-constraints: any.base ==4.20.0.0,
+constraints: any.base ==4.21.0.0,
any.ghc-bignum ==1.3,
- any.ghc-internal ==9.1001.0,
- any.ghc-prim ==0.11.0,
+ any.ghc-internal ==9.1201.0,
+ any.ghc-prim ==0.13.0,
any.rts ==1.0.2

Do you need to freeze?
^^^^^^^^^^^^^^^^^^^^^^

Why would you want to freeze? Don't we want to get minor updates of our
dependencies, or at least patches, as soon as we can? Well, although they
shouldn't, it is possible that any kind of update introduces new bugs,
performance issues, or some other kind of unexpected behaviour. This is where
``cabal.project.freeze`` comes in, as it ensures that dependencies don't
unexpectedly change. You can still update your dependencies, but you have to do
it on purpose, by modifying or by deleting and regenerating
``cabal.project.freeze`` file, and in the meantime you are guaranteed no
surprises will happen.

This consistency can be valuable as it ensures that all teammates, deployments,
and continuous integration are installing the exactly same dependencies. So if
you are running and testing the code on your local machine, you are guaranteed
that your teammate and your continuous integration will be running the exact
same code, and that at the end that exact same code will get deployed.

A ``.freeze`` file can be good to have when developing for yourself or within a
private team. If anyone using it can somehow have different inputs to
the solver then the ``.freeze`` file can be troublesome. It can prevent the
solver from finding a different version of a dependency that would satisfy a
different architecture or a different compiler version and boot libraries.

.. Warning::

If publishing a package to Hackage, no matter what kind of component it
contains, don't include a ``.freeze`` file, don't add it to any field of the
package description that would have ``cabal sdist`` include it in the
``.tar.gz``. In general, don't include anything in the package description
that relates to the project environment, like ``cabal.project`` or
``cabal.project.local``.

Freezing workflows
^^^^^^^^^^^^^^^^^^

.. Warning::
For each of these workflows, you may have to first delete the
``index-state`` line from ``cabal.project`` (and from
``cabal.project.freeze`` if it exists) and then run ``cabal update`` to
ensure that cabal will have newer versions to re-resolve the dependencies
with. Alternatively, you can run ``cabal update
--ignore-project``.

For the versions of all dependencies at once:

- To check solver-chosen versions - cold snap

With this workflow we freeze, inspect then discard ``cabal.project.freeze``,
always leaving the solver free to choose other versions. This is great way
to see what versions of dependencies are currently being used.

- To update versions of all dependencies - thaw and freeze

If you changed the version ranges of any of the dependencies in any of your
project's package descriptions, in any ``.cabal`` file, then delete the
``cabal.project.freeze`` file if it already exists and run ``cabal freeze``
to generate fresh version of ``cabal.project.freeze``. The steps of this
workflow are delete and freeze. The solver is let free to choose versions of
all dependencies but once it has decided, those versions are immediately
pinned.

This is "thaw and freeze" workflow is the simplest way to work with a

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is "thaw and freeze" workflow is the simplest way to work with a
This "thaw and freeze" workflow is the simplest way to work with a

``cabal.project.freeze`` file committed to source control.

For the version of a single dependency:

- To pin the version of a new dependency

The steps of this workflow are add the new dependency and freeze. The solver
is free to chose its version for the new dependency while retaining the

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
is free to chose its version for the new dependency while retaining the
is free to choose a version for the new dependency while retaining the

already pinned versions for the rest of the dependencies.

- To update the version of one dependency

If you want to a pick up a different version of a single dependency, you can
delete its constraint from ``cabal.project.freeze`` and then run ``cabal
freeze`` again. The steps of this workflow are delete one line and freeze.
It gives the solver the chance to choose another version for the unpinned
dependency.

Ensuring everything is frozen
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. Note::

If the ``.freeze`` file already has version equality constraints for every
package that is a dependency of the project, then the solver will not be
able to find a different version for any of them, the ``.freeze`` file
cannot change and, at that point when every dependency is frozen, ``cabal
freeze`` becomes an idempotent operation.

Adding a dependency to one of the packages in a project without freezing harder
Copy link

@langston-barrett langston-barrett Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line doesn't make sense unless you read the previous section to know what "freeze harder" means. Some readers might land on this section by following a link to it directly. Perhaps be more explicit?

Suggested change
Adding a dependency to one of the packages in a project without freezing harder
Adding a dependency to one of the packages in a project without running ``cabal freeze`` again

or

Suggested change
Adding a dependency to one of the packages in a project without freezing harder
Adding a dependency to one of the packages in a project without regenerating the ``cabal.project.freeze`` file using one of the methods described above

(Perhaps with a link to the above section)

leaves the newly added dependency susceptible to getting updated unexpectedly
when the solver can find a different version for it. Running ``cabal freeze``
will show this vulnerability to a human or an automated check that notices a new
version equality constraint in the ``.freeze`` file, a constraint for a package
that wasn't in the ``.freeze`` file before.
Comment on lines +238 to +242
Copy link

@langston-barrett langston-barrett Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest rephrasing this paragraph along these lines

When adding a new dependency to a project that utilizes a freeze file, it is often desirable to regenerate the freeze file using one of the methods described above. This ensures that the freeze file contains a constraint for the new dependency.


To automate this check, make it a part of continuous integration or make a
pre-commit hook for it. A simple check for this might be to compare the md5sum
of the ``.freeze`` file before and after running ``cabal freeze``. If the
checksums are the same, then the ``.freeze`` file didn't change, and all
versions are frozen.

.. code-block:: bash

[[ -f cabal.project.freeze ]] || exit 1
OLD_FREEZE_SUM=$(md5sum cabal.project.freeze)
cabal freeze || exit 1
NEW_FREEZE_SUM=$(md5sum cabal.project.freeze)
exit [[ "$NEW_FREEZE_SUM" == "$OLD_FREEZE_SUM" ]]
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Welcome to the Cabal User Guide

how-to-package-haskell-code
how-to-source-packages
how-to-freeze-versions
how-to-enable-profiling
how-to-build-like-nix
how-to-run-in-windows
Expand Down
Loading