haskell/cabal

Inconsistent 'freeze' behaviour

mietek opened this issue · 17 comments

A cabal freeze done before installing dependencies gives a different result than a cabal freeze done after installing dependencies.

The difference is that a globally installed package is included in the first result, and is not included in the second result.

To reproduce, start with either ghc-7.6.3 or ghc-7.8.2, and cabal-install-1.20.0.1 or cabal-install-1.20.0.2:

# mkdir /tmp/snap-test

# cd /tmp/snap-test

# cabal sandbox init
Writing a default package environment file to
/tmp/snap-test/cabal.sandbox.config
Creating a new sandbox at /tmp/snap-test/.cabal-sandbox

# cabal update
Downloading the latest package list from hackage.haskell.org
Note: there is a new version of cabal-install available.
To upgrade, run: cabal install cabal-install

# cat > snap-test.cabal << EOF
name:           snap-test
version:        0.13.2.5
build-type:     Simple
cabal-version:  >=1.2

executable snap-test
  build-depends: base, snap ==0.13.2.5
EOF

# cabal freeze
Resolving dependencies...

# mv cabal.config cabal.config.before

# cabal install --dependencies-only
Resolving dependencies...
Notice: installing into a sandbox located at /tmp/snap-test/.cabal-sandbox
...
Installed snap-0.13.2.5

# cabal freeze
Resolving dependencies...

# mv cabal.config cabal.config.after

The result with ghc-7.6.3:

# diff -u cabal.config.before cabal.config.after
--- cabal.config.before 2014-05-25 22:36:41.792754624 +0000
+++ cabal.config.after  2014-05-25 22:41:57.294000214 +0000
@@ -56,7 +56,6 @@
              nats ==0.2,
              network ==2.5.0.0,
              old-locale ==1.0.0.5,
-             old-time ==1.1.0.1,
              parallel ==3.2.0.4,
              parsec ==3.1.5,
              prelude-extras ==0.4,

# ghc-pkg list --global | grep old-time
    old-time-1.1.0.1

The result with ghc-7.8.2:

# diff -u cabal.config.before cabal.config.after
--- cabal.config.before 2014-05-25 22:36:42.601316886 +0000
+++ cabal.config.after  2014-05-25 22:44:32.652330297 +0000
@@ -56,7 +56,6 @@
              nats ==0.2,
              network ==2.5.0.0,
              old-locale ==1.0.0.6,
-             old-time ==1.1.0.2,
              parallel ==3.2.0.4,
              parsec ==3.1.5,
              prelude-extras ==0.4,

# ghc-pkg list --global | grep old-time
    old-time-1.1.0.2

The only part of snap that has a dependency on old-time is the snap executable stanza. The difference in behaviour is caused by cabal ignoring the build-depends of the snap executable stanza after having installed it. This can be observed by following your test case and then running:

$ cabal sandbox hc-pkg -- unregister snap 

$ cabal freeze
Resolving dependencies...

$ mv cabal.config cabal.config.unregistered

$ diff -u cabal.config.before cabal.config.unregistered

(Just to be clear, the diff command provides no output.)

Running freeze with the -v3 flag and looking at the goals provides additional evidence that this is the case.

If my understanding is correct, this is related to #779 and will be fixed when that is. @tibbe does my understanding here and of #779 sound reasonable.

@benarmston I'm a bit unfamiliar with this part of cabal. Are you saying that once cabal installs an executable it will omit its deps in the future? I thought that ghc-pkg, and thus cabal, doesn't track installed executables and thus it's a bit confusing if cabal somehow knows that executables are installed!

A more general point: do we want freeze to take currently installed packages (e.g. the sandbox package DB) when computing the install plan to freeze? On one hand that seems useful; you can test the setup before freezing it. On the other hand it might lead to issues like this, were freeze works differently depending on the state of the sandbox package DB (which is supposed to be an implementation detail).

@benarmston: As I understand, #779 is the issue where listing executable-only packages in build-depends causes Cabal to install the packages, and to fail to recognise the packages have been installed. Note snap is not an executable-only package.

@tibbe: Consider a working application which does not constrain the version numbers of its dependencies in its *.cabal file, and has no cabal.config. Without taking installed packages into account, how else to get to the desired state of having all known-good dependencies explicitly declared? Note down the entire dependency tree manually?

I think a cabal freeze which never takes installed packages into account would be borderline useless. The only use I see would be noting down the most recently updated versions of dependencies.

Same test as above, but with snap-0.13.2.6 instead of 0.13.2.5, is now showing an additional unexpected difference:

--- 79a3c87/cabal.config
+++ f7e3527/cabal.config
@@ -56,7 +56,6 @@
              nats ==0.2,
              network ==2.5.0.0,
              old-locale ==1.0.0.6,
-             old-time ==1.1.0.2,
              parallel ==3.2.0.4,
              parsec ==3.1.5,
              prelude-extras ==0.4,
@@ -88,7 +87,7 @@
              time ==1.4.2,
              transformers ==0.3.0.0,
              transformers-base ==0.4.2,
-             transformers-compat ==0.3.3.3,
+             transformers-compat ==0.3.3.4,
              unix ==2.7.0.1,
              unix-compat ==0.4.1.1,
              unordered-containers ==0.2.4.0,

Paging @ekmett and @kosmikus.

@tibbe that's more or less what I'm telling you. When cabal installs a package, say foo, it registers the package's library (if any) with ghc-pkg. But the information which is registered doesn't contain any details about foo's executables. When cabal's resolver selects the installed version of foo, it queries ghc-pkg to find out what build-depends foo was compiled against. That list contains the build-depends for foo's library but not any build-depends for any of foo's executables.

In the test program provided by @mietek the only dependency on old-time is from the snap package's executable snap. When the snap package has been registered with ghc-pkg and the installed version of snap is selected, any build-depends in the snap package's executables are not available.

My first reply on this ticket unregistered the snap package with ghc-pkg, thereby preventing cabal's resolver from selecting the installed version of snap. This caused cabal's resolver to consider the source package for snap and in doing so was able to determine the build-depends on all executables in the snap package.

@mietek I think that this issue and #779 are somewhat related. However, when I referenced #779 I had not understood that the proposed solution was to prevent adding non-lib packages to build-depends. Given that proposal my reference was probably more noise than signal.

Whilst trying to better understand what was happening I reduced @mietek's example to a much smaller case. It is on github at https://github.com/benarmston/cabal-issue-1896/. That repo contains a test.sh script which runs through the steps provided in @mietek's test case but against a much smaller set of dependencies. There are three tags of interest in the repo: cabal-1.7.0, cabal-1.7.1, and no-lib.

Tag cabal-1.7.1 exhibits the bug described in here. It has a cabal-version constraint of >=1.7.1.

Tag cabal-1.7.0 changes that constraint to >=1.7.0 and no longer exhibits the bug.

Tag no-lib uses a cabal-version constraint of >=1.7.1 but removes the library stanza. It does not exhibit the bug.

The reason for no-lib not exhibiting the bug is that the lack of a library section prevents the foo package from being registered with ghc-pkg, preventing cabal's resolver from using the installed version. Which inturn means that the source version is always considered and therefore the build-depends of the executable stanzas are always available to the resolver.

The changes between the tags can be seen at benarmston/cabal-issue-1896@cabal-1.7.0...cabal-1.7.1 and benarmston/cabal-issue-1896@cabal-1.7.1...no-lib

Whilst my understanding of the cause of this bug may be correct. I'm really not sure what the solution to it might be.

DISCLAIMER: My understanding of the process I've described above has been gained by trial and error whilst trying to understand this ticket. It hasn't been gained through an understanding of the resolver code. There could well be subtleties that I'm missing.

As an aside, to be clear on why I think this ticket and #779 are somewhat related.

For #779: cabal installs the non-lib package and doesn't register that it has been installed (because there is not a library to register with ghc-pkg). As the package has not been registered, its build-depends are not registered.

For this ticket: cabal installs the lib+exe package and registers the lib with ghc-pkg. The exe is not registered and in particular its build-depends are not registered.

I had wrongly assumed that the solution to #779 would be to register the non-lib package somewhere. If that had been the case, we could have used that registry to include any executables within lib+exe packages including the build-depends. In which case #779 and this ticket would have had the same solution.

But my assumption was wrong.

@tibbe if freeze doesn't take installed packages into account could it not select different flags for some packages than were selected when installing them (particularly if the user provided certain flags when installing). If it can do that, could this not result in freezing an install plan which won't actually build in the current sandbox? Or would warn about dangerous reinstalls?

If not taking the installed packages into account is the preferred method, I think we'd have to be careful not to present unhelpful (and perhaps scarry) messages to users.

Here's the usage pattern I'm imagining:

  1. Run cabal freeze. We now get an initial cabal.config file, based on the version constraints of the current package, its dependencies and so on.
  2. To make sure that this is actually a got set of packages to freeze, we build everything and run our tests (e.g. cabal install --only-dep --enable-tests && cabal test).

What if a user now runs cabal freeze again? First question: what does the user expect to happen at this point? We've already nailed down all versions in cabal.config and if we feed those constraints to the solver it ought to come up with exactly the same versions in its output and nothing will change. If something does change, something is wrong.

@tibbe: I agree.

The other possible usage pattern is — once the user has his app's dependencies explicitly declared in a cabal.config, what is the easiest way for the user to update the dependencies and ensure the app continues to work? What tooling can cabal provide to enable making this process automatic?

To bump your dependencies I'd expect the user to either

  • just bump them manually in the cabal.config file (if they know which version they want) or
  • remove a few lines from the file and then re-run freeze (which might pick newer) versions if available.

It's also possible to remove cabal.config (which should be tracked in source control) altogether, re-run freeze, and then use a diff tool to pick some changes and revert some. Note that the user needs to re-run build after this to make sure the manually edited changes actually make sense (i.e. form a valid install plan).

When the snap package has been registered with ghc-pkg and the installed version of snap is selected, any build-depends in the snap package's executables are not available.

Could the resolver always look at the source version of a package in order to determine dependencies, even if an installed version is selected?

Paging @dcoutts.

Just was pinged by this thread again, assuming because of a recent comment. I'm somewhat late to the party, but I just wanted to note the bit about:

-             transformers-compat ==0.3.3.3,
+             transformers-compat ==0.3.3.4,

note: transformers-compat ships 3 versions 0.3.3.2, 0.3.3.3, and 0.3.3.4, which all are basically the same package with different flags set up to work around the backtracker for cabal. Not sure if that is useful data, but it may explain why you get weird behavior around that package compared to others.

gbaz commented

it would be good to check if new-freeze has this issue, and if not, we can eventually obsolete this

I believe new-freeze generates foo == x.y.z || foo == u.v.w constraints instead, see #4832 and #3502. Optimistically closing.