I’m relatively new to PureScript, and as such, I have only ever managed my packages with spago. I’m curious about what the pain points were with bower that led the community to shift to a package set approach?
Bower as a registry is not maintained anymore (it’s been on borrowed time for a while), so it has a bad reputation from the JS community. The built-in solver was also really terribly naive, so you could get into really weird situations if you tried to install new things without first nuking your
bower_components. Other than that, I think it was alright.
PureScript with Spago and Haskell with Stack are the only languages I’ve ever worked with that use a “package set,” instead of typical package management that you’d get with e.g., npm or yarn. The idea is that the community curates a list of packages with version numbers that are all known to work together. You typically just target a public package set rather than having a lock file that lists whatever versions of each dependency you happen to end up using (that might or might not be compatible).
Back when PureScript used Bower, I sometimes found myself in “dependency hell” where I couldn’t find the proper versions of everything in order to get my project compiling and working. Sometimes I even had to back off to an earlier release of the compiler and downgrade a bunch of packages to make it work. All that went away once I switched to spago. Now upgrading to newer versions of packages/compilers is quite painless!
Once you get used to spago, you’ll wish every other ecosystem you work with used package sets
I think we need to draw a distinction between the general approaches of package sets versus version bounds and solving, and bower versus spago specifically.
On the subject of package sets versus version bounds and solving, I think both approaches are important, and I think ideally it should be possible to do both. Package sets are great, but often you’ll need a few specific versions of certain packages which differ from the versions you have in the package set, or alternatively you’ll want packages which aren’t in the set at all. In that case, you still want to make sure everything works together; if you don’t have accurate version bounds information or the ability to solve based on bounds, you basically just have to guess and hope for the best. Stackage (Haskell’s version of package sets) makes heavy use of version bounds information when assembling package sets, as well as for automatically producing lists of which downstream packages are likely to need updating when an upstream package publishes a new version.
The problem with bower was not that the approach of bounds and solving is fundamentally worse than package sets (it isn’t; a large chunk of the Haskell community uses the solving approach via the
cabal build tool, and most other programming language communities use solving). The problem was just that bower was not very good at it. It had a slightly strange behaviour where it would treat the existing contents of your
bower_components directory as preferences when installing, so if you tried to update your dependencies and then rerun
bower install, you’d often see conflicts which could be fixed by removing that directory (as @natefaubion alluded to).
More problematically, bower tended to give up quite easily when searching for an install plan, which meant that it was common to see conflicts, and in particular it was common to see installations which previously did work stop working when upstream packages published new versions. For example, imagine I have dependencies:
- package A at
>= 1.0.0 <3.0.0
- package B at
in which case bower might select, say,
- package A at
- package B at
and everything is fine.
Now suppose B publishes a new version
1.2.0, which adds a dependency on A with the version bounds
>= 1.0.0 <2.0.0. This means that version
2.1.0 of A, which we previously selected, is no longer an option if we are using the newer version of B. What might happen now is that the solver might first look at my package’s direct dependencies and choose A at
2.1.0 and B at
1.2.0. Then, it will try to ensure that A’s and B’s dependencies are each satisfied, and it will see that B declares an upper bound on A which rules out the version we have selected. Preferably, what would happen at this point is that it would backtrack, and try to choose a different version of A which satisfies both B’s and our package’s declared bounds. However, bower tended to just give up and report an error.
So I’m gathering that there was nothing inherently wrong with versioning approach, it was just bower that didn’t perform all that well, and the only reason we use a package set approach now as opposed to a “better bower” is that the person who stepped up and made bower’s replacement just happened to prefer the package set approach. Is that a correct evaluation?
I can’t comment on exactly why the package set approach was chosen, since I haven’t really been involved with either
psc-package (its predecessor), but I’d personally agree with that summary. It’s too subjective for me to be able to say whether it’s “correct” or not, though. I do think we’d probably still be using bower if it had become better at solving, added lockfile support, and hadn’t been deprecated.
Package sets are probably simpler from an implementation perspective, which is bonus; writing a solver is certainly a bit daunting. Also, when you’re writing an app, you probably do want to pin your dependencies down quite tightly so that everyone is using the same versions of everything; that just happens naturally with package sets, whereas if you’re using solving then you have to implement an additional lockfile feature. Also, the ecosystem wasn’t big enough then (and probably still isn’t now) that the lack of a solver for when you do want to go off the beaten path would present too much of a problem.
I think basing PureScript package management/build tools on package sets was a fairly natural choice partially because we had had a bit of a packaging renaissance in the Haskell community just a couple of years earlier: package sets really took off in the Haskell world around 2014-15, and lots of people were very happy with them. In particular, the release of
stack in 2015, a build tool based on package sets, was a big deal. Up until then everyone had been using
cabal, which is based on solving, and at the time had a pretty poor UX. As I recall,
stack had pretty much become the default option by the time psc-package was created in 2017. (Since then,
cabal has become significantly better and in a few respects I think its UX now beats
I would agree with this statement. I only pushed hard for
spago because I disliked / got frustrated with
bower. If that frustration with
bower hadn’t happened, there wouldn’t be as strong of a reason to go with
spago. That being said,
spago does make it easier to maintain documentation because I don’t have to worry as much about whether package A compiles with package B or not.
I feel like I am the person mentioned in this quote, so I’ll offer some historical perspective on why I believe the above is not entirely accurate: Spago was started to solve my own build problems (in particular, our monorepo build at work got horribly messy with bash scripts and so on), and was meant to be an improvement on
psc-package, which was the package-set tool at the time.
We were not using Bower because of the solver issues people mentioned above, but other than that I don’t think there was much wrong with Bower (well, I guess also the deprecation notice). People were happy with it and so on, at least until they started shutting down their registry.
So I’ll point out that there’s no “bower replacement” out there to this day, and that no one was ever trying to replace that. (Or at least, I was not. I did want to sunset
psc-package because of the confusion beginners would encounter with that, but today we have much better docs so people don’t seem to stumble on that problem that often anymore)
I think package sets work well, but constraint solving is also needed (as Harry explained above and in many other occasions), so I’d like to see a package manager with a solver at some point. And indeed we are building the new registry with both package sets and solvers in mind, and I’ll likely try to reuse Cabal’s solver in Spago at some point, since that works very well these days and certainly I’m not looking forward to writing a constraint solver from scratch
This is relatively unrelated, so I hope it’s not too out of place. You mentioned cabal getting better in recent years.
I’ve only tried stack. If cabal is good enough nowdays, is it worth using stack at all?
I think the reasons to use stack over cabal are:
- it installs GHC for you on (at least) windows, mac, and Linux
- it pins everything by default, which is what you want for basically anything that isn’t a library (although it’s worth noting that pinning everything with cabal is as simple as running
cabal freezeand checking in the file it generates)
- package sets get used by lots of people so you’re less likely to run into bugs where some specific version of some library interacts poorly with some specific version of some other library I guess? This one is admittedly a bit flimsy