Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to freeze #9984

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
43 changes: 14 additions & 29 deletions doc/cabal-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -558,38 +558,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
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.

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.

cabal gen-bounds
^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -826,7 +811,7 @@ data-files from the store.
.. _adding-libraries:

Adding libraries to GHC package environments
""""""""""""""""""""""""""""""""""""""""""""
********************************************

It is also possible to "install" libraries using the ``--lib`` flag. For
example, this command will build the latest Cabal library and install it:
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
166 changes: 166 additions & 0 deletions doc/how-to-freeze-versions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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 ranges
--------------

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

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.

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 satisify a
different architecture or a different compiler version and boot libraries.

.. Warning::

If publishing a package to Hackage, not 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``.

Freeze
If the ``cabal.project.freeze`` file doesn't exist, generating one is a
great way to see what versions of dependencies are currently being used even
if you choose to discard the ``.freeze`` file after inspecting it.

Thaw, 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``.

Freeze, Freeze (Freezing Harder)
You might in some cases want to skip deletion of ``cabal.project.freeze``,
but keep in mind that in that case ``cabal freeze`` will use existing
``cabal.project.freeze`` when resolving dependencies, therefore not updating
any existing dependencies, only adding new ones.

Partial Thaw, Freeze
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.

.. Note::

If not sure, pick the "thaw, freeze" workflow, as it is the safest, the
simplest and the most common. Finally, you will always want to commit the
changed ``cabal.project.freeze`` to version control.

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
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.

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-analyze-haskell-code-performance
how-to-build-like-nix
how-to-run-in-windows
Expand Down
Loading