Request for comment: `cabal freeze` doesn't produce a lock file

sorry, yes. i meant the latest version allowed by any constraints in the Gemfile. e.g.

gem 'nokogiri'
gem 'rails', '5.0.0'
gem 'rack',  '>=1.0'
gem 'thin',  '~>1.1'

as i mentioned:

The conservative flag allows bundle update --conservative GEM to update the version of GEM, but prevents Bundler from updating the versions of any of the gems that GEM depends on, similar to changing a gem’s version number in the Gemfile and then running bundle install.
Bundler: The best way to manage a Ruby application's gems

i guess because this seems like the most obvious thing in the world to me. we want to be able to update a dependency without updating all dependencies.

because version constraints are (generally) an anti-pattern. they imply that there’s some reason we can’t/shouldn’t update a dependency beyond that version. only use a version constraint if you really do know there’s something prohibiting you from upgrading to that version (and ideally have tests that would break if someone updated it, so that the constraint is just there to guard against people wasting time when you do your weekly/monthly dependency upgrades via dependabot or whatever).

amortizing risk. i try to enforce a culture of TDD in companies i work for, but often test coverage is woefully inadequate, so we want to upgrade one dependency at a time, in its own PR—or at least one commit per dependency upgrade, because an upgrade often requires a lot of changes in a lot of different places. if that happens for multiple dependencies, i sure as heck don’t want a mega-commit that bundles a ton of risk in a single PR/deploy, and also makes for challenging PR review. small incremental changes are part and parcel of modern agile development.

they also allow for the absolute blessing that is git bisect should we ship a regression.

i confess, this is all so standard from using package managers like npm, yarn, gomod, bundler, etc. that i’m truly surprised by this line of inquiry. i’ve worked for a LOT of companies in my career, including being eng #11 at zendesk in 2010, and this is all just so standard and desired by developers.

ok this explains the entirety of the confusion. in the haskell world, using version constraints as set via the package version policy is not an anti-pattern but standard practice because of the ecosystemwide beneficial effects. Unlike the ruby or javascript world we are strongly typed, and so breakages between versions happen both A) more frequently (whenever the types mismatch, not just when there’s a runtime error as a result of this) and B) more predictably (because we can set policies around types changing as a way of guarding version bounds). Further, updating dependencies (within bounds) is therefore also much more risk free. API compatibility issues induced by version upgrades are discovered at compile time even in the absence of a test suite. (Of course, behavioral changes are not discovered this way, etc – this is a trend not an absolute). Therefore in the haskell world, it is standard practice to set PVP bounds, and automatically always choose to pick up minor updates. Further, it is standard practice to freely update those bounds with the understanding that for the most part, the induced errors will all be found at compile time and can be understood and fixed mainly by following the types.

https://pvp.haskell.org/faq/

5 Likes

that it’s an anti-pattern is orthogonal to the language you’re using. it is valuable to be able to specify two different things:

  1. here are acceptable/valid ranges of package versions.
  2. here are the exact versions we’re using to get a reproducible build.

indeed, if what you just said was true, that would be tantamount to saying we expect either:

  1. the cabal file to be a (manually maintained) lockfile, or
  2. we would just accept not having reproducible builds, which is untenable for applications that people actually depend on to work.

okay just, wow. i cannot emphasize strongly enough how completely irrelevant strong typing is here.

first, you don’t just let business-critical applications run on arbitrary versions of packages that could be incompatible! most obviously because there could be BUGS (or even intentional changes in behavior) that don’t cause compilation issues.

secondarily, even if the bug was caught by the compiler, the developer would still have to spend time finding the the exact version the app worked with in development. hopefully it wouldn’t be too hard to find because it would be a relatively recent upgrade (otherwise we’d have caught this in a failed CI build/deploy or another developer on the team would experience it when upgrading). but my god, would that be annoying.

this is why rust has Cargo.toml and Cargo.lock, and go/gomod has go.mod and go.sum, and every tech company i have ever seen in my entire career takes it for granted that you use lock files. this is just utterly essential functionality, irrespective of using a typed language.

  1. again, that doesn’t guard against bugs, just intentional behavior changes adhering to semver.
  2. then you’re just using the main package declaration file as a manual lock file instead of letting software automatically handle the lockfile for you.
2 Likes

For libraries, I struggle to see how specifying version constraints is an anti-pattern. The alternative, locking all dependencies to exact versions, requires allowing multiple versions of transitive dependencies in the same build, which is unsound in the general case and is the source of all sorts of bugs in the JavaScript world.

For executables, I agree that locking is important, and that using bounds in addition is usually not worth the trouble. I suspect they’d be used less if lock/freeze files had better UX, including for updating one version at a time.

8 Likes

Every Haskell company I’ve worked at locks its dependencies as well. Mostly with Nix. Works great.

The thing is - caba-installl and Haskell isn’t just for pRoDuCtIoN sOfTwArE so forcing those aesthetics and values on the ecosystem isn’t a given.

There are definitely advantages to just setting some loose PVP bounds and not always using a lock file. Especially - as others have said - for library development. And in Haskell, almost all development can be library development (industry is interestingly less skilled at this tho LOL)

I agree the freeze file workflow could use some love so that users can more ergonomically lock their deps without third party tools. But tbh…you almost always have to lock some C dependency in a Haskell project anyways…so that begets Nix which obviates these “problems.”

5 Likes

I think i expressed myself poorly. Of course for production software at a specific company one uses a freeze or lock. (Not so for either open-source software or libraries). But when doing so, one usually updates the freeze all at once rather than library by library. And this is where types matter – because most issues are caught by the type system, then the conjunction of type checking, plus a test suite is much more powerful than either alone. As such, the specific rare thing is seeking to update a single library at a time. This is even moreso because the compiler has a relatively rapid release cycle, and often companies time library-upgrade sweeps to match the general ecosystem-upgrade that accompanies a new compiler version.

We want cabal files and freeze files both – of course. That’s why we have both! The point is the way people use and interact with them is very much a product of the specific expectations and norms that arise from being an ecosystem with deep dependency trees in a directly compiled language (without an intermediate bytecode product) with strong typing guarantees and relatively frequent compiler releases.

@utopia for what it’s worth The Haskell Tool Stack comes from the “Always use a lock file” lineage of software development, and might fit your mental model better.

3 Likes

awesome.

i mean, the problem i’m highlighting absolutely sucks, even if you’re not a money making business. sure, it’s not like you’re going to lose thousands of dollars, but it’s still super annoying and totally avoidable.

of course library development is different, because the executable that ultimately uses the library is where you set the lock. that’s the one exception i’m aware of.

i have no idea what this means. the language you use has nothing to do with whether you’re creating an application vs a library.

if nix fixes all this, then i guess the answer to my complaint is “switch from cabal to nix”. ideally cabal would handle all of this, since the underlying libraries all use cabal and having two tools sucks. right?

3 Likes

thanks. i was told that stack is on the decline and not really the thing to invest in. who knows if that’s true. i’m a newb.

I think it’s better to say that cabal-install is on the rise, and is a lot more fun to use than it was a few years ago. For production code bases, I feel like Stack is still used everywhere Nix isn’t, but that’s a biased opinion.

Oh and I guess I should mention, all uses of Nix for Haskell (that aren’t haskell.nix) are using Stack(age). It’s where Nixpkgs gets its locked package sets. :slight_smile:

6 Likes

not for libraries, but for executables regardless of whether they are open source. being open source doesn’t mean you’re okay with bugs.

that can be a very problematic approach, for reasons i’ve described in some detail.

i’ve explained why this is a highly suboptimal “solution”.

in any case, i think we’ve exhausted the topic. i appreciate your willingness to explain your position.

5 Likes

To convince more companies to use Haskell and grow its user base basic things like locking should just work for reproducibility; at least for the part that the programming ecosystem has under control, i.e. Haskell packages. You won’t convince many companies to adopt Nix (yet), especially not at the same time with adopting Haskell. They’re sold to Docker for now to manage everything else like c libs…

cabal freeze seems like an attempt at lock files before the programming community found a good structure as used in Javascript or Rust. Workarounds like downloading/pinning some Hackage index or manually post-processing the freeze file feel just like, well, workarounds. If a freeze file looks like a lock file, it should behave conveniently, predictably like lock file or clearly state what it’s for (and either way we should improve locking with hashes etc, ideally with some widespread parseable format like json).

Regarding Nix: The lack of a modern locking mechanism clearly shows in the cumbersome Haskell Nix support: the official Nixpkgs tools don’'t even look at your Cabal versions or freeze files, because they don’t contain enough information for reproducibility. You need a large separate out-of-tree project like haskell.nix that requires import-from-derivation to work around the lock shortcomings, and oftentimes it doesn’t work out-of-the-box. In Rust for example, with its easily parseable json lock file Nix packaging is so much easier, because Nix can build upon it.

Another important but expected tool that has to work around locking shortcomings is the widespread version-bumping tool Renovate. It seems to use workarounds like “git trailers” where it would just bump the lock file in other languages.

So freeze files definitely need some improvements.

12 Likes

I have not been explaining my personal position. I have been describing standard practices adopted throughout the haskell ecosystem, and explaining why those have been adopted. If you want to critique what exists, you should understand it and the underlying motivations.

In particular, whether or not you want better tooling for freeze files (patches welcome!) version constraints are not an “anti-pattern” but fundamental to the ecosystem, and serve an important purpose for dependency resolution which lets one arrive at lockfiles or stackage-like version “universes” to begin with.

i’ve explained why that’s incorrect. either you use version constraints with absolute precision (i.e. just making them a manual lockfile), or you’ve got a lockfile, so version constraints don’t accomplish anything (because you have to manually/intentionally update any package version, so adding a second step to it—that you have to change the constraint—doesn’t effectively change the process).

this is just logic. it doesn’t matter what language you’re using.

excellent post!

making this post at least 20 characters.

I am not engaging in an argument here. I am describing the reality which is that version constraints are pervasive and used pervasively throughout haskell, and coupled to tooling in very specific ways. Without them, the ecosystem as it exists falls apart.

Whether version constraints are an anti-pattern or not (i think they’re not, at least for libraries) is better debated in a separate thread.

The ground work for a real lock file is outlined out in Ability to promote freeze files to lock files · Issue #10784 · haskell/cabal · GitHub “I believe the critical point here is to separate further the planning and the building phase, allowing cabal-install to build an existing plan rather than re-executing the solver.”

5 Likes

The original Topic is about Cabal (the tool) but as the discussion has widened to include Stack, I would mention:

  • Cabal (the tool) and Stack are alternatives but not mutually exclusive ones. Stack is built on top of Cabal (the library);
  • today (there has been a journey), Stack’s focus is on (a) reproducible build plans (b) based on sets of package versions (and their Cabal flags) that are known (by testing) to work well together and with a specified version of GHC (i.e. the Stackage project) (see Why Stack? or Stack in conclusion) and (c) ease of use;
  • Stack allows package versions (including their Hackage revisions, for packages from Hackage) to be specified unambiguously (see package location);
  • Stack uses lock files (see here) to make specifying reproducible builds less tedious;
  • Stack can be configured to integrate with Nix (the purely functional package manager), with Nix handling the non-Haskell dependencies needed by Haskell packages (see here); and
  • on Unix-like operating systems, Stack also has support for automatically performing builds inside a Docker container (see here).
2 Likes

fwiw, the in-tree support for haskell packages is quite good and very flexible. haskell-flake wraps that API to provide better UX via flake.parts, which is handy. I’ve started preferring this approach for my own projects as I find haskell.nix unwieldy.

5 Likes

+1 to all this mpilgrem (and chreekat) but why do you say “today (there has been a journey)” ? I understood that reproducibility has been stack’s raison d’être from the start.

1 Like