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

RFC: cabal init by default pins the ghc version via with-compiler #5661

Open
m-renaud opened this issue Nov 4, 2018 · 17 comments
Open

RFC: cabal init by default pins the ghc version via with-compiler #5661

m-renaud opened this issue Nov 4, 2018 · 17 comments

Comments

@m-renaud
Copy link
Collaborator

m-renaud commented Nov 4, 2018

Overview

I recently documented all the steps you need to go through to get a working project from scratch using cabal-install, and one of the pain points I found was around specifying and pinning the compiler version.

Currently, running cabal init uses the ghc found on PATH to initialize the project, but does not pin the compiler version. As a consequence, its possible for changes in the global environment to break the build for a project (the dreaded non-reproducible build problem).

There's actually nothing preventing cabal from offering by default "reproducible builds", by pinning the ghc version and using v2/new-build. In my opinion, upgrading to a new compiler for a project should be an explicit action, not implicit based on global environment configuration.

I propose that project initialization via cabal pins the ghc version by creating cabal.project.local cabal.project containing a with-compiler directive.

Motivating Story

Happy Haskeller Holly is working on a project, everything builds and works great! She finishes up the project and some time later starts work on another project, noticing that there's a shiny new ghc version (woohoo!) that she wants to start using. So, she upgrades her compiler, starts working on a new project and everything is great. Later, she has a great idea for how to improve her first project, so she goes back to it, and surprise! It doesn't build anymore, and Holly become a sad Haskeller.

Who's developer experience does this improve?

Almost everyone, and especially beginners/casual users. Haskell tools have a reputation of being difficult to use (and in the past, this was very true) but as a community we've gotten much better at making things accessible to everyone, from beginners to experts. There are many points in the build tool design space, and a beginner for one reason or another may end up using any one of them. It is my opinion that whatever one they use they should have simple, consistent experience for getting from a fresh system to a working project, and that project should continue to work in the future.

"But what about the power users?"

One possible counter-argument to this is folks who have a complicated/non-standard workflow, and this would be an extra step they may need to undo/revert. My personal opinion is that the workflow that works the smoothest for newcomers should be the default, if you're experienced enough to set up a complicated workflow, then it's easy enough for you to work around it; beginners don't have the luxury of experience and familiarity :)

Amendment: In addition, this new default behaviour can be disabled in ~/.cabal/config.

What does this not address

This proposal does not address the case when you don't have the pinned ghc version installed. If you have a pinned ghc version it will not attempt to download it for you. An idea for how this problem could be solved is through an integration with ghcup, but that's another discussion.

Design

Prior Art

It is currently possible to pin the compiler version for a project via configure cabal v2-configure -w ghc-8.4.4. This is already what folks do, but the with-compiler option is buried in the docs and not mentioned in the Cabal Configuration Overview section or in the Quickstart despite it avoiding a lot of confusion and build errors for beginners (or even folks who haven't used Haskell in a while).

#5658 (pending at time of writing) adds support for passing --with-compiler (-w) to cabal init which will pin the version of the compiler when the project is initialized, correction: this PR does not do any pinnning or write a project file.

Changes

  • cabal init by default writes a cabal.project.local cabal.project containing a with-compiler directive, taken from the ghc on the path (details below)
  • cabal init output contains info about what ghc was selected (the full path) as well as where the configuration is written. This helps inform users that this option exists, what it does, and where to change it.
  • Documentation is added to the cabal website, user docs, etc. explaining what this is, and step-by-step instructions for how to upgrade to a new ghc version.
  • Amendment: The new default behaviour can be disabled by a change to ~/.cabal/config.

Selecting ghc path

We can instead find out the exact version of ghc that's currently on the PATH via ghc --version and then attempt to find the specific binary (usually also on the path and then symlinked to ghc). If we are unable to find it we could fallback to not pinning the version and printing a message.

There are two options for pinning: either specify the full path to the executable, or specify just the executable name (ghc-ver).

Amendment: The latter is preferred as different ghc installation mechanisms place the binary in different locations.

Amendments

2018-11-05 8:12PST

  • Write with-compiler to cabal.project instead of cabal.project.local
  • Correction, Add -w/--with-compiler flag to cabal init #5658 does not produce a project file.
  • New default behaviour can be disabled in ~/.cabal/config
  • Clarified that only specifying the name of the binary is preferred over the full path.
@ElvishJerricco
Copy link

FWIW, any pinning you'd want would go in cabal.project, not cabal.project.local, because the former is the only one you're supposed to check in to source control.

My two cents: This and freeze files by default are an idea I'd appreciate for products, but not for infrastructure. i.e. Any libraries or build tools that someone wants to push to Hackage should be as relaxed and unpinned as reasonably possible. But when someone starts an end-use product, and not some library or infrastructure to be used by other projects, they invariably want a pinned down environment. The problem is that drawing the line between these two use cases is hard, and it's hard to know which should be catered to.

@m-renaud
Copy link
Collaborator Author

m-renaud commented Nov 5, 2018

any pinning you'd want would go in cabal.project, not cabal.project.local, because the former is the only one you're supposed to check in to source control.

TIL, SGTM :)

My two cents ... it's hard to know which should be catered to.

Here's my take on it: people who are developing Haskell libraries and infrastructure are (almost) by definition more familiar with the ecosystem and the tools, and they know they don't want a pinned version because they likely test with numerous versions of GHC anyways. Conversely, those who are building a product just "want things to work", and may not be as familiar with the ecosystem and how changing a global binary can cause builds to fail in projects. This latter group includes all beginners, hobbyist users of Haskell, and folks who build products using Haskell; I would wager quite a bit that this group is orders of magnitude larger than the former (or would be if Haskell became mainstream) :)

I guess my opinion can be summarized as: "cater to the beginner, make the advanced user do a little extra work". If the beginner needs to know about specific configuration options just to make their toy program build then I would say we're doing something wrong :)

@erikd
Copy link
Member

erikd commented Nov 5, 2018

If cabal is going to get training wheels, there should be a way for advanced users to permanently remove those training wheels.

I'm even fine with training wheels being the default, but they need to be optional.

@m-renaud
Copy link
Collaborator Author

m-renaud commented Nov 5, 2018

there should be a way for advanced users to permanently remove those training wheels

Agreed, this is probably something that could go in the user's global cabal configuration file. I'm sure one of the cabal developers would have a recommendation for how to accomplish this nicely.

@phadej
Copy link
Collaborator

phadej commented Nov 5, 2018

IMO writing absolute ghc path to cabal.project is bad practice. HVR PPA, ghcup, and possibly users' own preference will have the same compiler in different places.

Also cabal init already writes:

build-depends:       base ^>=4.11.1.0

which "pin-points" the GHC close enough.

cabal init could "run" (or simulate) cabal new-configure -w $COMP, producing some local settings (that would result in cabal.project.local).


I also don't want cabal init to do different things whether you say cabal init -w ghc-8.4.4 or cabal init -w /opt/ghc/8.4.4/bin/ghc (i.e. write cabal.project in one, but cabal.project.local file in the other case). That will be confusing.


And finally, how many beginners or casual users do have different versions of GHC on their machine, and actively use more than one? stack users do (it's hidden from them!). But cabal is explicit about the compiler, yet users should not care in general given their GHC is recent enough. For example now, whether you have GHC-8.2.2 or GHC-8.4.4 will hardly matter for beginner or casual use.


That said, generating cabal.project skeleton in cabal init is quite different thing, and that I'm in favour, 👍.

@m-renaud
Copy link
Collaborator Author

m-renaud commented Nov 5, 2018

Thank you everyone for the feedback so far, I really appreciate it!

@ElvishJerricco

Amended proposal to propose cabal.project be created instead of cabal.project.local

@erikd

Added to the proposal in the "power users" section that the new default behaviour can be changed via ~/.cabal.config

@phadej

Clarified that although absolute path is an option, we should instead just write the binary name for the reasons you mentioned.
I'll reply to the other parts of your comment in a follow up.

See the new Amendments section at the bottom for edit history.

@Mistuke
Copy link
Collaborator

Mistuke commented Nov 6, 2018

I actually have the opposite though, pinning to a ghc version should be an explicit thing not an implicit one. Especially if the pinned information is checked into version control.

This becomes a hassle when you're working with a team on something but individual developers have different versions of ghc. This shouldn't matter for the majority of code out there. Also if 8.4.1 works there's absolutely no reason 8.4.2, 8.4.3 or 8.4.4 shouldn't work.

So I think pinning of base as it does now is the much more sensible option.

Requiring ghc also needlessly locks out other haskell Compilers.

@m-renaud
Copy link
Collaborator Author

m-renaud commented Nov 6, 2018

@phadej

I also don't want cabal init to do different things whether you say

Completely agree, @ElvishJerricco's suggestion to write cabal.project instead of cabal.project.local was great (and actually more what I had in mind when I wrote the proposal, but wasn't aware of the intended semantics of .local vs non-local). This way, cabal init and cabal init -w ghc-ver would both result in a cabal.project file being written with a with-compiler directive.

how many beginners or casual users do have different versions of GHC on their machine, and actively use more than one?

This isn't the exact issue I'm thinking of, its the situation where someone tries out haskell, and then a couple months later come back and try to build their project again (maybe after a system update, since folks install ghc from their package manager sometimes) and it fails to build. The old version of ghc would still be on the path so if we pinned it, it would still work.

Also cabal init already writes: build-depends: base ^>=4.11.1.0

Sure, but the error message you get if your base bounds are wrong doesn't point you at all to what's wrong. For example, if everything else stays the same in my project but I upgrade my ghc which depends on a base version outside my bounds I get the following error:

cabal: Could not resolve dependencies:
[__0] trying: myproject-0.1.0.0 (user goal)
[__1] next goal: base (dependency of myproject)
[__1] rejecting: base-4.11.1.0/installed-4.1... (conflict: myproject =>
base>=4.9 && <4.11)
[__1] rejecting: base-4.12.0.0, base-4.11.1.0, base-4.11.0.0, base-4.10.1.0,
<omitted>
non-upgradeable package requires installed instance)
[__1] fail (backjumping, conflict set: base, myproject)
After searching the rest of the dependency tree exhaustively, these were the
goals I've had most trouble fulfilling: base, myproject

I recognize this error as a ghc version incompatible with my stated bounds, but we should not expect beginners to know that. Maybe this could be solved with better error messages from cabal install somehow recognizing that this error is likely caused by a GHC upgrade and recommending appropriate action, but it would be best if it just never happened. @hvr for ideas on possibly improving the failed dependency solver messages?

@m-renaud
Copy link
Collaborator Author

m-renaud commented Nov 7, 2018

@Mistuke

Thanks for the feedback :) Those are some good points you raise, let me see if I can provide my perspective on them.

pinning to a ghc version should be an explicit thing not an implicit one.

I think this goes back to the comment that @ElvishJerricco made earlier in the thread that this depends on the type of project (library vs. executable):

"Any libraries or build tools that someone wants to push to Hackage should be as relaxed and unpinned as reasonably possible. But when someone starts an end-use product, and not some library or infrastructure to be used by other projects, they invariably want a pinned down environment."

This makes me think that maybe we should maybe pin the ghc for cabal init --is-executable but not for --is-library, I'm curious what other folks think of this. This also has some implications for what the default behaviour for cabal init should be if unspecified (library vs. executable), imho this should be --is-binary, but that's another discussion that I don't want to have here.

when you're working with a team on something but individual developers have different versions of ghc

That's a good point, although I think having several people agree on a compiler version is a pretty standard procedure when there's shared development on a project, and a good idea in general since there may be small discrepancies between versions anyways. And going back to the previous point, if you're working on a library it makes sense to support the widest range of compilers and dependency version bounds as possible, so the idea of only pinning for a binary package makes more sense.

So I think pinning of base as it does now is the much more sensible option.

I'm think I'm going to have to disagree with this point, pinning base is an indirect way of pinning the compiler, by saying base >=4.11 && < 4.12 you are implicitly saying you require ghc-8.4.* since that's the base version that particular version requires (caveat: this is obviously only considering the case where you want to use ghc instead of another haskell compiler, but I speak to that case below and this point stands). I think its better to say exactly what you want, which is a specific compiler version. See my previous comment for what happens (spoiler: bad error message) when you indirectly specify what you want.

Requiring ghc also needlessly locks out other haskell Compilers.

I think this is a red herring, cabal already defaults to ghc as the compiler when initializing your project, so you need to perform additional configuration to get it to use a non-ghc compiler to build your project anyways. This proposal doesn't in any way prohibit you from choosing a different compiler afterwards if you want to use something other than ghc (and in fact with cabal init -w support that writes a cabal.project file you could specify your non-ghc compiler from the beginning). The reason this proposal is for pinning the "ghc" version is because that's the default Haskell compiler.

Let me know if I've misunderstood any of your points.

@m-renaud
Copy link
Collaborator Author

So, what's the next step to take here? The majority of responses seem to be in favour, although I admit the number of responses wasn't huge.

The point that @ElvishJerricco brought up about wanting to pin the version for products but not libraries was a great observation, I think we can address this by only pinnning for executables and not for libraries which are likely for publishing to hackage. How do folks feel about that?

Lastly, if we decide that this is a desirable change, how can I help move this along? I haven't made any changes to cabal-install before but if someone is able to give me a bit of an intro and some code pointers I can look into making this change.

Thanks!

@ElvishJerricco
Copy link

Something kind of like Stack's templates would probably be my preference for this. That way it's not a matter of defaults, but rather of just choosing the template you want. Though I do like cabal-install's Q&A style init, so whatever templating system used would need some way to do that.

@ElvishJerricco
Copy link

Or, for a super minimal change, there could just be a new question in that Q&A style init. Do you want to pin all the things? [n]:

@linearray
Copy link
Member

So what happens when the pinned version is not installed anymore after a system upgrade? Do we at least try to build with the current one?

Otherwise I think this makes for a much worse experience than we have now, because you have to go through all the project files and remove the with-compiler directives.

@m-renaud
Copy link
Collaborator Author

@ElvishJerricco I would love to have something like Stack's templates for cabal-install. If I remember correctly they're going through a redesign and it's possible cabal could use the same underlying templates? I'm not sure though, I'll find out and follow up.

@linearray Thanks for bringing that up, I can imagine a few ways of addressing this:

  1. (bad) simply fail compilation
  2. (better) error saying that ghc-x.y.z is pinned in project.cabal but cannot be found
  3. (best) same as 2) but find which version is not on the PATH and provide instructions (or a prompt) to update the with-compiler directive

That being said, if you do a system upgrade you need to go through and update version bounds anyways, so it's still not a co-op. Also, in all cases I know of, doing a system upgrade leaves the old ghc (with version attached) on the PATH, so your project should continue building until you decide to update your project.

Also, out of scope here but it works be great if there was a cabal upgrade-ghc <new-ghc-ver> (or something like that) which would automatically adjust known version bounds and update with-compiler directives (after asking of course).

@m-renaud
Copy link
Collaborator Author

@ElvishJerricco re. new question: that could work, but a beginner would have no idea what that means, which kinda negates the problem I'm trying to solve :)

@linearray
Copy link
Member

linearray commented Nov 19, 2018

That being said, if you do a system upgrade you need to go through and update version bounds anyways, so it's still not a co-op. Also, in all cases I know of, doing a system upgrade leaves the old ghc (with version attached) on the PATH, so your project should continue building until you decide to update your project.

Homebrew will leave the old version lying around until you decide to do a brew cleanup. Debian/Ubuntu on the other hand will just overwrite GHC with the new version and the old one is gone.
FWIW: With Homebrew it feels like bad form to me to pin an old compiler in /usr/local/Cellar, because brew cleanup has the ring to it that it doesn't break stuff. This is just my view and not official.

  1. (best) same as 2) but find which version is not on the PATH and provide instructions (or a prompt) to update the with-compiler directive

I think we have to be careful not to create a problem where there was none before w.r.t. upgrades, especially now when they are so frequent. As stated above cabal init will set base version bounds such that using the next GHC major version will fail with the error message you mentioned. If with-compiler just gives you a nicer error message at the cost of introducing yet another thing you have to fix before your package compiles again, I don't think it buys much.

A (cheap) suggestion: Perhaps it would be better to not refuse compilation with the new ghc you find on PATH and just print a warning that this is not the ghc used for init and you may have to relax base version bounds and update your code. This field is somewhat related: #4894

A more comprehensive solution should at least combine updating with-compiler with updating base version bounds in my view.

@m-renaud
Copy link
Collaborator Author

m-renaud commented Nov 21, 2018

@linearray thanks for the thoughtful comments 👍

Homebrew will leave the old version lying around until you decide to do a brew cleanup

Ah yes, forgot about that, its been a while. I wonder if that's the most common way of installing ghc on Mac, I don't use a Mac so I'm not really sure. I took a look at the https://taylor.fausak.me/2018/11/18/2018-state-of-haskell-survey-results and it looked like the majority of MacOS developers used either Stack or Nix to get their GHC version (I just scanned the CSV though, would be useful to get actual numbers). Also, ghcup now supports MacOS and this has support for installing multiple versions side-by-side. I guess what I'm wondering is if this would be an issue in practice.

FWIW: With Homebrew it feels like bad form to me to pin an old compiler in /usr/local/Cellar

So, the idea isn't to pin a particular path, just the compiler with the version number (such as with-compiler: ghc-8.4.4), then it doesn't matter where it lives on the $PATH.

Debian/Ubuntu on the other hand will just overwrite GHC with the new version and the old one is gone.

That's true of apt, but AFAIK the recommended way to install the GHC toolchain on Linux nowadays (especially on these distros) is either ghcup or hvr's PPA, both of which add versioned ghc binaries to the $PATH. Also, the aptitude repo for ghc is updated so infrequently for Ubuntu/Debian that this would surface very infrequently.

I think we have to be careful not to create a problem where there was none before w.r.t. upgrades

Agreed, but given the points above I don't think this will happen in practice.

A (cheap) suggestion: Perhaps it would be better to not refuse compilation with the new ghc you find on PATH

I'm not sure how the cabal developers would feel about this, it kinda defeats the purpose of with-compiler if you ignore it. On the other hand, you could have it search the path for a ghc version, and if it doesn't match the with-compiler directive it could print a nice error, something like:

Could not find ghc-8.4.4 pinned in cabal.project (line 24):
    with-compiler: ghc-8.4.4

The ghc binary on the $PATH is version 8.6.2 (/usr/bin/ghc), also available as /usr/bin/ghc-8.6.2
Please either add ghc-8.4.4 to the path, or update cabal.project to set "with-compiler: ghc-8.6.2"

A more comprehensive solution should at least combine updating with-compiler with updating base version bounds in my view.

I completely agree. There should be a tool where you point it to a project directory and say what the new version of ghc you'd like to use would be and it would update your with-compiler directive and adjust version bounds, but that's definitely out of scope here :)

@emilypi emilypi added the type: RFC Requests for Comment label Sep 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants