It's maddening to hear people say things like, "Oh if everyone just used semantic versioning this wouldn't be a problem". Of course this cannot work. _Think about it_. There are innumerable ways two pieces of code can be incompatible. ... If you call these things "breaking" changes, you will constantly be increasing the major version.
One of the things that prompted the OP was this breakage in Python's cryptography package [1] (OP actually opened this issue) due to the introduction of a Rust dependency in a 0.0.x release. The dependency change didn't change the public API at all, but did still cause plenty of issues downstream. It's a great question on the topic of semver to think about how to handle major dependency changes that aren't API changes. Personally, I would have preferred a new major release, but that's exactly your point syllogism — it's a matter of opinion.
As a sidenote, Alex Gaynor, one of the cryptography package maintainers is on a memory-safe language crusade. Interesting to see how that crusade runs into conflict with the anti-static linking crusade that distro packagers are on. I find both goals admirable from a security perspective. This stuff is hard.
The problem is that in the real world despite promises of semantic versioning, breaking often occurs which makes it impossible for two packages to depend on the same version of another library though in theory they should.
What I was trying to illustrate is, that the notion of versioning is broken in and off itself. By the lessons of the article, pretty much every API change is a breaking change, so you would constantly need to increment the major version, if you take SemVer seriously, meaning minor versions don't exist, de facto. And if you now imagine that you'd need to touch your code every time one of your dependencies increments their minor version (I mean. Likely you can't, because you have no notion of how often that happens, because currently all your tools just ignore those changes), lest people can't build your software because the packaging tool needs to assume an incompatibility that needs manual resolving, you will see how useless this makes versioning.
Now, there are two ways out of this mess: One is to ignore it and just assume that, in practice, some things will are more likely to break consumers than others and just apply a reasonable case-by-case judgement. It's what's happening right now in probably ~every language for ~every tool out there. I think it's reasonable, but I personally dislike it, because for one, humans eff up all the time, so relying on them having a good notion of what breaks and applying it consistently and timely leads to pain. This whole article is born out of the idea, that these things should be codified and then automatically applied, I shouldn't even have to need to know what version my package currently is, IMHO.
The other way out is much more complicated: Transition to a notion of breakage not by versioning APIs, but by defining it in terms of pairs of packages. This also has a bunch of definite and obvious deficiencies (for example that you don't have access to all the code that imports you. Or the combinatoric explosion).
Currently, my personal hope is that this can be solved by supporting gradual code repair (see, e.g. https://github.com/golang/go/issues/18130 for what this means and how this is currently progressing) and then add good tooling (I'm working on something, but I have limited time and brain space). We'll see :)
With pip for instance, it often happens that a transitive dependency gets updated inadvertently breaking your code. This follows from the assumption that all packages follow semantic versioning perfectly and keep backward compatibility where they should. This is not the case in practice and experience has shown it is unrealistic to have that assumption. A better way is to rely on exact versions of packages (up to a single bit) and not on semantic versioning.
For anyone in the Haskell world, it's obvious that semantic versioning[1] doesn't work. Some non-trivial amount of that is likely due to how horribly broken Cabal is, a problem the community remains in staunch denial of. But for the rest, one problem is that because Haskell more thoroughly specifies types, individual functions can remain the same in behavior but change in type because a bug-fix, say, fixes a record type used somewhere. The external interface might be largely the same, or even identical, but the binary output is non-backwards compatible. Dynamic libraries and strong, descriptive type systems aren't very compatible. The result is that a lot of bug fixes become minor version bumps.
These minor version bumps scare library developers, especially people that produce libraries that depend on libraries that depend on... and so on. So packages fail to build too often because of constraints. So the irony of this thorough process for package building is that humans have mucked it up and made it break builds. In almost every case in which semantic versions blocked some update from occurring and caused my work to stop, it was because I needed to bump the constraints on some third-party package. It's a boring process that I've only become too accustomed to:
1. try to build with updated libA 1.1.0-foobar
2. libB depended on libA < 1.1
3. download source for libB
4. update constraint and repackage libB locally
5. rebuild main application
This happens just about every time there's a new version of any major piece of Haskell software.
If we really want to move into The Future, we need to start versioning individual modules or functions, instead of whole suites of software. But the cognitive burden of doing this is very high, and the software to do it hasn't been written yet.
[1] The Haskell community doesn't technically use semver, but rather a related policy called the package versioning policy. That's a nitpick though, and doesn't affect my argument.
Elsewhere in this thread I've seen just the opposite. Tons of people claiming variants of "breaking changes should just bump the major version."
I'd argue that in the long run, not being able to update dependencies because they broke you is going to be much worse than them fixing the incompatibilities for you.
Either way, you need people to act like adults and communicate, but the multirepo problem is worse.
It's an unfortunate side effect of the limited way some ecosystems handle multi-version dependencies; I would not blame this on semantic versioning (which clearly states which changes are to be considered breaking). With OSGi I can use one module that uses an older version and one module that uses a newer version and use these together in an application (as long as the dependencies are not exposed, which they unfortunately will be in almost all real-world code).
I’m not really sure this is an entirely fair argument.
If you rely on third party packages of any type, you have dependencies that can rapidly and unexpectedly break with an update. Semantic versioning is supposed to help with this, but it doesn’t always help.
This reads to me like an argument for semantic versioning, because otherwise I need to internalize the rules of every package and know that some will break compatibility on the Y, some on the Z, ... Etc.
That at least fixes the qualitative nonsense between minor and patch updates.
I think there needs to be better project definitions around what constitutes a major change.
Projects need to be able to define things like dropping support for old versions of the underlying language in minor versions. So that the last version of support that some people might get is "3.2" and "3.3" may not install at all for them. That means that technically they are in a state where they need to do work to upgrade and are "broken" in a sense, but the actual public API of the software has not changed between "3.2" and "3.3". Supported O/S distro versions should also be able to be abandoned in minor releases. Toolchain updates can also happen in minor releases. Pulling in major versions of dependencies which are technically breaking for anyone who hits a diamond dependency issue, but which produce no major breaking API changes should be able to happen in minor versions.
That means that the contract isn't "I can pull in minor versions and you can never force me to do work" but more strictly that the public API the software exposes won't update.
There's also the problem with semver pinning that projects do where they put hard floor and ceiling pins on all their dependencies, even though their software may be fine with a 5-year old version of the dep (they've just never tested) and it may work fine with the next major release of the dep without any changes at all. Ideally for that last problem, the compatibility matrix fed into the dependency solver should really be a bit more malleable, so that the engineer can realize that the next version of dependency breaks everything and they can retconn the compatibility of their software to pin to the last working version of that dependency. This breaks the perfect immutability of literally everything about a software release, but allows for not being able to predict the future.
And you'd think they'd separate major versions by package so they can coexist (a la commons lang), and you can just pin each major to the latest. Nope. Breaking changes across minor versions.
Except that a shit-ton of developers will code and test with one version of a dependency, and never, ever, ever update it. If the dependency has a catastrophic security hole, that security hole will be pretty much permanent.
And what happens if project A pins projects B and C, which in turn pin DIFFERENT versions of project D? Is there any language or environment out there that can make that work?
- uno depends on json 1.3.6
- dos depends on json 1.4.12
- tres depends on json 2.1.0
Cargo will use json 1.4.12 for uno and dos, and json 2.1.0 for tres.
Hopefully rust builds a culture that respects semantic versioning better than the Ruby & Node cultures do. That has to start at the top. There were several Rails 2.3.X releases with minor ABI incompatibilities. Truly respecting semver would have required these patch level updates to get a new major number.
> versioned multi-repos may solve this for the team[s] demanding incompatible changes to shared code but any team who was happy to use the shared code as it currently is, and was expecting to also benefit from any upcoming compatible improvements will see only problems with this "solution"
Normally this is solved with semantic versioning... you pin to a minor version, so you get all non-breaking changes, but don't pull in breaking changes.
Bugs & compatibility issues are still a problem with the statically-linked version, unless you want to stay on the version of the library with the security vulnerability, you have to upgrade. That means, for either static or dynamic, dealing with bugs and compatibility issues — which I'd argue is another form of bug; if you're practicing semantic versioning (which you should be, as it prevents exactly this issue), this indicates either someone accidentally broke compatibility (a bug in the library), or someone was relying on something outside the established API (a bug in the consumer). For major versions (i.e., where compatibility is intentionally broken), good package managers are able to manage side-by-side installation of well-behaved packaged. (E.g., Portage's "slots" concept.) I'd also mention Nix's design here; my understanding is that it allows you to upgrade the dependency for just some consumers, so you really can then choose between insecure & bugs.
One of the things that prompted the OP was this breakage in Python's cryptography package [1] (OP actually opened this issue) due to the introduction of a Rust dependency in a 0.0.x release. The dependency change didn't change the public API at all, but did still cause plenty of issues downstream. It's a great question on the topic of semver to think about how to handle major dependency changes that aren't API changes. Personally, I would have preferred a new major release, but that's exactly your point syllogism — it's a matter of opinion.
As a sidenote, Alex Gaynor, one of the cryptography package maintainers is on a memory-safe language crusade. Interesting to see how that crusade runs into conflict with the anti-static linking crusade that distro packagers are on. I find both goals admirable from a security perspective. This stuff is hard.
[1] https://github.com/pyca/cryptography/issues/5771
reply