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

Recommended way to use cabal freeze for an executable? #8047

Open
4 tasks
Martinsos opened this issue Mar 14, 2022 · 12 comments · May be fixed by #8053
Open
4 tasks

Recommended way to use cabal freeze for an executable? #8047

Martinsos opened this issue Mar 14, 2022 · 12 comments · May be fixed by #8053

Comments

@Martinsos
Copy link
Collaborator

Martinsos commented Mar 14, 2022

Having used Javascript's NPM in the past, I like it that cabal has freeze option, to create a freeze file, which from what I understand, is similar to package.lock.json in npm world.

Since I have a Haskell executable that has multiple team members working on it + gets built in the CI, I would like to use freeze file to make build reproducible -> meaning that when same git commit is executed on CI or on machine of developer A or machine of developer B, they are all guaranteed to get the same result. This is how package.lock.json is used also. So it doesn't really matter which dependency bounds are specified in myproject.cabal, nor does it matter if new patches were released for the packages we use in the project -> all that matters is the freeze file.

From what I understood, good workflow for this would be:

  • When developing locally, when you want to in any way update dependencies used in the project, you should run cabal freeze once you are done. So for example, you will modify the version boundaries for some package in myproject.cabal, or you will add a new dependency, or remove an existing one, or do cabal update and want to get patches -> whatever you do, at the end you should do cabal freeze, so that these changes are reflected in the freeze file.

One thing I wasn't sure about though is: does cabal actually use only cabal.project.freeze when cabal.project.freeze exists, does that happen by default? Testing it out locally, and googling about it, I developed a notion that it doesn't use exclusively freeze file, or maybe doesn't use it at all, at least not by default. But if that is so, what is the purpose of freeze file?
When I say testing locally, I mean that I generate freeze file with cabal freeze, run cabal build, it says it is up to date, but then if I update some dependency bound in myproject.cabal and run cabal build again, it will rebuild! While I would expect it to ignore the change, due to using freeze file.

Another question: is there a way to run cabal freeze automatically on any change in dependencies, the same way package.lock.json is generated automatically?

Also, https://cabal.readthedocs.io/en/3.6/cabal-package.html?highlight=freeze#freezing-dependency-versions
 -> it says output is cabal.config, but that is not correct is it? Isn't output cabal.project.freeze ?

EDIT:
TODO (for the PR I would love to create at the end of this):

  • Mention in docs that although cabal build will do a bit of work if you modify .cabal, it will not produce a new build, it will still be the same old build (due to the freeze file). It just looks like it is doing smth new, because cabal file got touched.
  • Fix the mention of cabal.config being output of freeze -> it should say it returns cabal.project.freeze.
  • Make it clear, somewhere in docs, that dependency versions from freeze file are combined with other files, which effectively results in dependency versions from freeze file are the ones that dictate what will be installed, resulting in reproducibility. Emphasize that this way freeze file becomes a part of project configuration and is by default used in all cabal operations -> there is nothing we need to do to use it, same as there is nothing we need to do to use cabal.project.
  • Describe the standard workflow for dealing with freeze files, when developing an executable (when to run cabal freeze, when to delete freeze file, should it be committed, ...). Consider discussing executable vs library use cases (how it is not that important for a library) -> not sure if that is too opinionated though, but I have seen that opinion reiterated on multiple blog posts.
@Martinsos Martinsos changed the title Recommended way to use cabal freeze for an executable Recommended way to use cabal freeze for an executable? Mar 14, 2022
@Mikolaj
Copy link
Member

Mikolaj commented Mar 14, 2022

@philderbeast
Copy link
Collaborator

philderbeast commented Mar 14, 2022

When moving from stack to cabal, I grabbed the constraints from stackage and stuffed those into cabal.project as constraints. Running freeze repeats the == constraints from the cabal.project but only for the packages actually used. It also shows the flags it chose.

In this screenshot, cabal.project is on the left and cabal.project.freeze is on the right.
Screen Shot 2022-03-14 at 7 21 52 PM

The docs say that with freeze files "all users see a consistent set of dependencies". Does that imply a reproducible build if using the same compiler version? The cabal v2-freeze section of the docs doesn't mention the interaction with other commands such as cabal outdated or cabal build. The cabal outdated section "Listing outdated dependency version bounds" mentions its interaction with freeze files while the cabal v2-build section doesn't.

@Martinsos
Copy link
Collaborator Author

Some information is also present in https://cabal.readthedocs.io/en/latest/cabal-project.html#cabal-project-reference -> it says that cabal.project.freeze is applied after cabal.project (therefore overriding it, at least the options that are not appendable), but before cabal.project.local.

So I would assume from this that cabal.project.freeze is used everywhere where cabal.project is used, which should certainly also mean cabal build.

But maybe trick is in this "appendable" part? I don't know hm. But for some reason cabal build cares about changes in version bounds in .cabal even when freeze file is there. Maybe it just uses all of them? First takes boundaries from .cabal, then adds info from cabal.project.freeze to those, which means at the end it really matters what is written in freeze file?

I just tried following: I had

template-haskell      ^>= 2.16.0

in my .cabal file.
I run cabal build and cabal freeze.
Running cabal build after that just returns Up to date.

So then I modified .cabal to have more relaxed upper bound (latest version of template-haskell is 2.18):

template-haskell      >= 2.16.0 && < 2.19

When I ran cabal build after this, I didn't get Up to date, which I expected, instead I got

Resolving dependencies...
Build profile: -w ghc-8.10.7 -O1
In order, the following will be built (use -v for more details):
 - waspc-0.4.0.0 (lib) (configuration changed)
 - waspc-0.4.0.0 (exe:wasp-cli) (configuration changed)
Configuring library for waspc-0.4.0.0..
Preprocessing library for waspc-0.4.0.0..
Building library for waspc-0.4.0.0..
Configuring executable 'wasp-cli' for waspc-0.4.0.0..
Preprocessing executable 'wasp-cli' for waspc-0.4.0.0..
Building executable 'wasp-cli' for waspc-0.4.0.0..

Which I guess is not so bad -> it did some work, but it didn't really install any new dependencies, or change any dependencies at all, it just recompiled the targets in the package. Probably it does that because it sees that cabal file changed but isn't aware what changed.

So then I modified .cabal file to contain:

template-haskell      >= 2.17.0 && < 2.19

and this resulted in failure

Resolving dependencies...
cabal: Could not resolve dependencies:
[__0] trying: waspc-0.4.0.0 (user goal)
[__1] next goal: template-haskell (dependency of waspc)
[__1] rejecting: template-haskell-2.16.0.0/installed-2.16.0.0 (conflict: waspc
=> template-haskell>=2.17.0 && <2.19)
[__1] rejecting: template-haskell-2.18.0.0, template-haskell-2.17.0.0
(constraint from project config
/home/martin/git/wasp-lang/wasp/waspc/cabal.project.freeze requires
==2.16.0.0)
[__1] rejecting: template-haskell-2.16.0.0 (constraint from non-upgradeable
package requires installed instance)
[__1] rejecting: template-haskell-2.15.0.0, template-haskell-2.14.0.0,
template-haskell-2.13.0.0, template-haskell-2.12.0.0,
template-haskell-2.11.1.0, template-haskell-2.11.0.0,
template-haskell-2.10.0.0, template-haskell-2.9.0.0, template-haskell-2.8.0.0,
template-haskell-2.7.0.0, template-haskell-2.6.0.0, template-haskell-2.5.0.0,
template-haskell-2.4.0.1, template-haskell-2.4.0.0, template-haskell-2.3.0.1,
template-haskell-2.3.0.0, template-haskell-2.2.0.0 (constraint from project
config /home/martin/git/wasp-lang/wasp/waspc/cabal.project.freeze requires
==2.16.0.0)
[__1] fail (backjumping, conflict set: template-haskell, waspc)
After searching the rest of the dependency tree exhaustively, these were the
goals I've had most trouble fulfilling: template-haskell, waspc

As seen in the error message, it says that freeze file demanded template haskell to be of version 2.16.0.0 exactly, and that is why the resolution failed. So this is great actually, it seems from this that freeze file is used!
From what I saw, it seems that freeze file gets combined with the .cabal file and other config files, and all the constraints are taken into account. However, as freeze file is the most specific one, it will effectively be the one to dictate the final version, and therefore the build is indeed reproducible.
It is also a nice thing that version bounds from .cabal are not ignored -> if we change them to not encompass the version specified in freeze file, cabal will fail. And that is good, because it warns us that we have some inconsistency between what we claim we want and what we pinned down as a working version.

So I guess I answered my own question, but I conclude that freeze file is automatically for all/most cabal commands, and effectively results in reproducible builds. And yes, the workflow I described sounds to be a reasonable workflow.

And no, there is no way to run cabal freeze automatically (but that doesn't sound like a bi problem, at least now for now? I will know better when we use it more).

And yes, it seems the docs are wrong at that place where they mention cabal.config, that is an old thing.

Does this make sense all together?

@Martinsos
Copy link
Collaborator Author

I can imagine one scenario that causes build to not be reproducible any more, and that is adding a new dependency to .cabal but forgetting to run cabal freeze.

In that case I guess that, since .cabal and freeze file are combined together, that new dependency will just retain the version bounds from .cabal and that is it, therefore leaving it un-pinned.

I am not sure what is a solution to this, if there is a way to ensure that people run cabal freeze.

There is one more thing I just realized: should I be deleting existing freeze file before running cabal freeze in attempt to update it? Does cabal freeze use existing freeze file to determine the versions? I just tested this with example above, and it seems that it indeed does -> cabal freeze uses existing freeze file. If that is so, it makes sense to first delete the freeze file, and only then run cabal freeze. I wonder what is the purpose of cabal freeze reading freeze file, and if instead it would be better if it ignored it?

@fgaz
Copy link
Member

fgaz commented Mar 15, 2022

When I say testing locally, I mean that I generate freeze file with cabal freeze, run cabal build, it says it is up to date, but then if I update some dependency bound in myproject.cabal and run cabal build again, it will rebuild! While I would expect it to ignore the change, due to using freeze file.

The rebuild happens because modifying the cabal file invalidates the local/project cache, but due to the freeze file the same reproducible build plan will be constructed, and the previously built dependencies will be reused

@Martinsos
Copy link
Collaborator Author

When I say testing locally, I mean that I generate freeze file with cabal freeze, run cabal build, it says it is up to date, but then if I update some dependency bound in myproject.cabal and run cabal build again, it will rebuild! While I would expect it to ignore the change, due to using freeze file.

The rebuild happens because modifying the cabal file invalidates the local/project cache, but due to the freeze file the same reproducible build plan will be constructed, and the previously built dependencies will be reused

Thanks, that is what I also concluded at the end!

@Martinsos
Copy link
Collaborator Author

Just to be clear, I still have questions outstanding:

  1. What is the recommended workflow for using freeze files for a project with executable? Is it what I described above?
  2. Should I delete freeze file before I run cabal freeze again?
  3. Is there a way to automatically generate freeze file on changes in .cabal? If not, is that on purpose or is it missing feature?

I am guessing for each of these what the answer might be, and I described those above, but it would be great to hear from somebody experienced what are the actual answers.

@jneira
Copy link
Member

jneira commented Mar 16, 2022

Is there a way to automatically generate freeze file on changes in .cabal? If not, is that on purpose or is it missing feature?

There exists something similar in npm for example? dont you have to run some npm command (or a file watcher that run the command) to update its lock file? or you are thinking in make cabal build generates the freeze file on .cabal changes (npm does that afair)?

@Martinsos
Copy link
Collaborator Author

Martinsos commented Mar 16, 2022

Is there a way to automatically generate freeze file on changes in .cabal? If not, is that on purpose or is it missing feature?

There exists something similar in npm for example? dont you have to run some npm command (or a file watcher that run the command) to update its lock file? or you are thinking in make cabal build generates the freeze file on .cabal changes (npm does that afair)?

That is where I got the idea: in npm, package.lock.json is updated automatically.
More details here: https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json .

package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json.

This means that if you do smth like npm install somedep, package.lock.json is regenerated.
However, even if you manually modify package.json by adding new dependency (or smth else) and then run npm install, this will also update package.lock.json, because it will modify node_modules. So detecting changes in node_modues is really what ensures that package.lock.json is updated every time that dependencies were changed.

So yes, it would come down to cabal freeze file most likely being updated on cabal build, it seems. Not sure if there is any other cabal command that "puts dependencies into action".

@jneira
Copy link
Member

jneira commented Mar 16, 2022

thanks, the npm example is really useful

yeah, there would be other involved cabal command, like install f.e.

however I think it would be hard make it the default, as quite cabal users don't use freeze files in local development by default

@Martinsos
Copy link
Collaborator Author

Martinsos commented Mar 16, 2022

thanks, the npm example is really useful

yeah, there would be other involved cabal command, like install f.e.

however I think it would be hard make it the default, as quite cabal users don't use freeze files in local development by default

It could be a flag that if turned on automatically regenerates freeze file. Library devs wouldn't use it probalby, while application devs would use it.

I don't yet know how important this automatic feature is though. It sounds nice, and I know in npm I don't worry about it at all, I just do my stuff and commit package.lock.json when it changes, that is it. So that is nice. With cabal, I have to remember to run cabal freeze after I do some changes to deps. If I don't do that, my project will be using stuff from the last freeze. That is not terrible. Actually, writing this, I think there is case where automatic freeze is valuable: what if I add new dependency to .cabal? Then old freeze + this new dependency will be used. And if I forget to run cabal freeze after this, I will have that dependency not-frozen. It could stay not-frozen for quite some time if nobody remembers to run cabal freeze. And likely nobody would notice. While in npm, package.lock.json would get generated automatically for me and that couldn't happen.

@Martinsos
Copy link
Collaborator Author

Regarding ensuring that freeze file is up to date: what I just did in our project was add this check in the CI that checks if freeze file is up to date, so I will share it here in case it is interesting for the discussion.

If freeze file is not up to date, CI fails. So although there is no automatic updating of freeze file by cabal, this ensures that it is indeed up to date. It is a stronger/safer mechanism than automatic updating by cabal, but on the other hand most people probably won't think of doing it hm.

PR: https://github.com/wasp-lang/wasp/pull/508/files

Crucial code (Github Actions):

      - name: Check if cabal freeze file is up to date
        if: matrix.os == 'ubuntu-latest'
        run: |
          # We do the check by running `cabal freeze` and checking if cabal.project.freeze
          # changed. If it didn't, it is up to date, otherwise it is not.
          # If there is no cabal.project.freeze, or `cabal freeze` command failed, that is also red flag.
          [[ -f cabal.project.freeze ]] || exit 1
          OLD_FREEZE_SUM=$(md5sum cabal.project.freeze)
          cabal freeze || exit 1
          NEW_FREEZE_SUM=$(md5sum cabal.project.freeze)
          [[ "$NEW_FREEZE_SUM" == "$OLD_FREEZE_SUM" ]]

One thing I believe I learned from this, while thinking about regenerating freeze file, is that in most cases, the best thing to do is to first delete freeze file and then run cabal freeze. This might result in multiple dependencies changing, but is also guaranteed to not cause any unneeccesary complications. While on the other hand, if you run cabal freeze without deleting the freeze file first (for example in case you added a new dependency), there is a chance dependency resolution will fail due to all the versions that are pinned down by current freeze file.

So if cabal was to implement automatic regeneration of freeze file, it would almost certainly have to consist of first deleting existing freeze file and only then generating a new one (or using some other way to generate a new one while ignoring the existing one). I believe npm uses the same approach when regenerating package.lock.json -> it ignores existing one while doing it, and doesn't just add to it, but regenerates it whole, as if it never existed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants