You're welcome to evaluate and select libraries with the best possible track record for avoiding backwards incompatible changes - I do that myself. But, especially for libraries that are relatively early in their development process, having a hard "no breakages" policy isn't realistic.
I still think semver is the best fix for this. If you make a backwards-incompatible change, you bump the major version number and you extensively document the change.
If you're consuming libraries, the best way to address this problem is with solid test coverage. That way any time there's a new library version you can lean on your test suite to help you understand if it's going to break anything for you or not.
I think that "infeasible" is rarely true.
Let's take, as a well-known example, Python3. Well, the python developers wanted to change the default string representation, the fact that "print" was a statement, and the behavior of integer division, but this absolutely could have been done by things such as a new syntax for Unicode strings (such as, huh, u"hello"), making integer division dependent on "import from __future__"statements, using a *.py3 file extension for breaking new syntax, and so on. It would have been more work for developers, true, and this was ultimately their decision, but it was not technically required.
What developers of library and infrastructure need to realize is that very soon there is far more code which depends on established APIs and that breaking this code is, on absolute terms, more work, and is disruptive in collective terms.
It is like a tree where most of the biomass is in the leaves, not in the trunk. For example, there is a Wiki software named MoinMoin Wiki, written in Python, and supporting multiple markup formats, which is written in Python. It is popular and also used for the Python wiki, so you could be assured it is used in infrastruccture. But there is no Python 3 port because the developers did not had time for that, and in this respect the breaking changes of Python 3 are disruptive. There are also many scientific code bases written in Python 2 which will never be ported to Python 3, because there is simply no funding and no scientific career for such work.
And this becomes very visible in larger projects which depend on a lot of libraries which in turn have transitive dependencies on multiple levels. Just look at the full dependency graph of something like Gazebo, Tensorflow or Gimp. The former might well have several hundred dependencies As soon as one of there libraries demands that a newer version of the dependency is used, and this dependency introduces breaking change, it is likely that another dependency in the same project is broken by it. And telling to users "to just fix and upgrade" is not going to work, it does not scale, and people will at some point stop to upgrade your stuff altogether. Imagine the Linux kernel worked this way! No sane person would use it.
Or TeX, C, RS232?
Maybe at the source level ... at the binary driver level there is no stability at all, and it's a huge reason why the driver scene for linux still languishes in so many areas.
Now there are reasons linux has chosen this model but it nonetheless demonstrates this is not a "free" decision without any consequences.
You are evading the argument, what we were talking about was stability of APIs in respect to the userland code. Linux never promised binary compatibility at driver level.
Also, Linux has far better stability in hardware support than competing systems. If a relative tells me they have a photographic slide scanner or printer that stopped to work with some Windows x.y release, chances are quite good that it is continued to be supported by Linux.
Those are all inspiring examples of projects that went truly above and beyond the norm and achieved great success as a result.
People shouldn't be publishing such libraries, nor should people be using them. With the exception of my first few FOSS projects, all of my open source libraries represent the 2nd, 3rd, or even 4th incarnation of a concept.
That's just being responsible--don't knowingly litter the ecosystem with junk.
I take a similar view at work. People are too quick to break out components into modules and libraries, long before they have a firm notion of what a proper interface should look like. If you can't commit to a stable API, then often times it's better to just copy whole functions and files between projects and let them evolve naturally. It takes time to see where that evolution goes, and which commonalities emerge in the implementation and surrounding application.
But what about sharing bug and security fixes, you ask? Well, without a stable API you'll invariably end up with different projects stuck using different versions, anyhow. That's why people invented Docker :( What you end up with isn't an actually shared library, just the pretense of sharing, plus a bunch of unnecessary build complexity.
That's why we have pre-1.0 version numbers (and alphas and betas) - they let us release code early for people to provide feedback before we've committed to a non-breaking API.
Even with a stable 1.0 release things change. People using the library may come up with logical feature requests that the original design didn't consider, and which require backwards incompatible changes. That's when you bump to a 2.0 release.
My dream project would have a version number of something like 1.237 - meaning there have been 237 new features releases since the 1.0 release without backwards compatibility breaks. My hat is off to any project that pulls that off though, it's a rare accomplishment!
This is a solved problem, it's just that some library maintainers don't practice proper communication of breaking changes.
If your breaking changes are a total paradigm shift in the intended usage of the library/API then sure, it should be a new library. But quite often those breaking changes are just things like "field Foo is now required when performing process Bar" in order to prevent some sort of invalid state from being reached. In those cases communicating the breaking changes via versioning is the way to go imo.
There is a huge difference: Most languages and runtimes do not support having the same dynamic library twice in the same process. This means that if you just bump the major version number, you can only have one version of a library in a process. Now consider the case that your library L1 has a dependency L2 and is included in an application A which also includes another library L3 which has the same dependency L2 as you.
Now think what happens if L2 has a breaking change, and you upgrade L1 to use the new version, but L3 does not work with the new version, and does not upgrade it. In this case, the application A - the user of your library - is broken, and there is little the developer of A can do about it.
However, there are two kinds of libraries: One which access global objects, like GUI libraries, the glibc socket library, and such. Or which provide data interchange formats, such as Python's Numpy. These regularly need to be unique across an implementation.
And the other ones which do things like data transformation, parsing configuration, some numeric processing, and such. In this case, you could link L2 statically, if you were using C. r you could L2v2 and the library l3 could use L2v1. But in most languages and runtimes, this will only work if L2 version 1 and the incompatible L2 version 2 have a different name.
> Just bump the major version but continue supporting the old major version with security releases etc.
My experience is that the "continue supporting the old major version with security releases etc" almost never happens. People release incompatible versions and force upgrades on users because they do not want to do extra work. If they were supporting older versions, they would still have to do some of the extra work. They might do it some time in some cases but even then they will cease to do it soon.
If one wants continuous updates and support, one is usually MUCH better off to use libraries which just do not do compatibility-breaking changes. Because these have decided to do the work required up-front, and have little extra work with supplying urgent fixes to an old API interface.
Could you explain why each of these libraries needs to be unique in a runtime instance? I don't know much about GUIs or networking, but numpy doesn't seem like it needs to be unique.
> This is a solved problem, it's just that some library maintainers don't practice proper communication of breaking changes.
It's not just a communication problem, it's also the way that a lot of language tooling works. For example on the JVM you can (mostly) only have a single version of a given (fully qualified) class on a given application's classpath, so it's quite important for incompatible new versions of a library to use a completely new set of class names - thus you'll see e.g. commons-lang3 using org.apache.commons.lang3 alongside commons-lang using org.apache.commons.lang.
Also look into how many massive libraries basically started as someones weekend project for fun. I posit that the quality of the open source ecosystem would go down, not up if we tried to somehow enforce these kinds of standards on public projects.
I know this isn't a dependency issue, but it's a great example of why things like semver don't work perfectly in practice. It's hard to figure out what the effects of your code really are in the end. You might assume a change is minor when it really is major. I wish programming languages could figure out what the version change should be for a given code change.
My point is that if a user of your library is failing after a change, you should treat that as a bug in the library. Backwards incompatible changes should only be on purpose, and only when absolutely necessary.
Of course, it's only their own code they're affecting here, so it doesn't really matter as much as a library for users you don't know. Plus it is a pretty cool solution, I just don't think it should be necessary.
Semver doesn't cover changes in behavior. What a "breaking change" is when it comes to behavior changes is up to the person who implemented it.
"Software using Semantic Versioning MUST declare a public API. This API could be declared in the code itself or exist strictly in documentation. However it is done, it SHOULD be precise and comprehensive."
If the API changes semantics between different releases, it is a breaking change. It does not matter whether the types in the function signature is identical. If
But the core problem with semantic versioning is that it somehow legitimizes to make breaking changes all the time. It is not OK to beat somebody up in a bar. It is also not OK to tell somebody that he is going to be beaten up, and then beat him up.
Now, semantic versioning holds the idea that it is OK to make breaking changes, if only you tell people before. And this is a lousy idea. Breaking changes break other people's code, especially if they use it in deep graphs of dependencies. And they can't necessarily do anything about it, because they might have two dependencies which are depending on your library, only that the first one now requires version 2.0 at least, while the second one will not work with anything newer than version 1.99.17. And thus the whole project is broken. Actually, requiring the new version 2.0 in the first dependency is a breaking change, too, because it breaks backwards-compatibility.
And this is something which the Python devs just did not comprehend - that this kind of breakage trickles up dependency graphs and makes it extremely difficult for users with a limited budget to make upgrades, even if most of their features are highly desired.
Having tooling to acknowledge that in fact no one will be affected by a change is a huge productivity boost.
Yes I do agree with that part
Such as Linux?
The "always backwards compatibility" and "never backwards compatibility" schools of thought might just as well be "no programmer should ever use a garbage collector" or "all programmers should only ever use garbage collectors".
Tools like OP help people who want to accept backwards incompatibility.
Are you saying that it is better to have to deal with a bunch of tiny changes, so that when you have to deal with a big change you'll be used to it? If so, I strongly disagree.
I would much rather have to deal with a big annoying change every 10 years, than a small annoying change every 6 months (which would still likely need the big one every 10 years).
And then people go and muck with your internals even if you protected them, because that's the only way they found to do what they needed to do, and they were much more interested in doing the thing than following your guidelines.
And that's before even mentioning broken features which requires changes which will invariably break other stuff.
The only way to know for sure you're never breaking compatibility is to never release any update. Otherwise you're playing the odds.
This gives you the moral high ground to adopt semantic versioning: the correct behavior is the documented behavior. If someone has code that relies on an undocumented method or undocumented behavior, it's fair game to break that with only a minor version bump.
If your documentation isn't any good, SemVer is almost a fiction.
Also more facetiously, xkcd/1172
I think API breakages are usually just caused by developers which failed to define consistent and thought-out interfaces from the start, and library developers which do not want to do any extra work to continue to provide APIs with the old semantics.
I know that it can be made to work, because I have worked in industrial automation and PLC libraries, and while there are not always and everywhere geniuses at work, in this field legacy interfaces are just never scrapped because they need to continue to work. What you publish needs to continue to work for 20+ years, because nobody wants to scrap a six-figure printing machine just because of an update to the PLC libraries. So, it does not require geniuses to do that. It is more a matter of what the consumers of a library tolerate.
And the same is valid for language evolution. Common Lisp for example, or C are extremely stable. Python users have come to tolerate a far higher amount of breakage and instability, but I do not buy that this is in any way technically required. The breakage might not matter that much if the code is only used in fast-moving start-ups of which 99% will be bust anyway in four years time. But there are domains where the costs of this are just too high, such as science projects, industrial automation, enterprise systems, and many more, because systems are too complex and expensive to update.
But most breaking changes such as in semantic versioning major number changes do intentionally break client code, which is an entirely different thing. There are a few rules one can follow which make it pretty improbable that such foreseeable breakage will happen. And this will constitute more than 99.9% of API breakage - even more if you stipulate that undocumented features must not be relied upon, and client developers follow that rule. Developers relying on undocumented behavior is usually due to a lack of clear documentation, so it is largely avoidable, too.
There is also one interesting insight I had when looking at error codes and exceptions: Normally, backwards compatibility is broken when you take things away from an interface - functions from a library, values from an enumeration type, and so on (Rich Hickey has made a brilliant talk about this). However, the general thing is that backward-compatible change must never narrow pre-conditions, nor widen post-conditions.
This means that it is not OK to just add extra error codes to the return values of a function interface, or to add new classes of exceptions. This breaks backward compatibility.
You can of course add enumeration values, optional arguments, keyword arguments and such to function call arguments, and these are great ways to make interface changes backwards-compatible.
Another insight is that backward-incompatible changes tend to cascade up larger dependency graphs. The broader and deeper the dependency graphs becomes, the more probable it is that a backward-compatible change (e.g. requiring a newer dependency) becomes itself a breaking change up the dependency chain. The philosophy in this age seems to be that of coourse the other library authors should "just fix their stuff", they get the responsibility of upgrading to the new version of the dependency. I do not agree to this at all - I think if something changes in a component, and this breaks the software, the responsibility is always within that component, and nowhere else. If some change in the component causes breakage, this is a breaking change, by definition. And this is also true if it is an upgrade from Qt4 to Qt5, or OpenCV, or imageio-py2 to imageio-py3 or such.
One could increment the mayor version number but this does not fix the problem. What fixes the problem is to not break stuff in the first place.
I am quite convinced that as in the future, ever more programming will be done in libraries and components, there will be ecosystems which do not accept that kind of breakage. Linux is already a good example, and it is wildly successful.
Is that really a solution though? You're just going to end up with an unsupported old version and a supported yet breaking new version which probably doesn't even have upgrade guidance... Not sure I see it. I think libraries should work hard as you say to not break backwards compat, but I also think the library author has the right to change some function signatures when they change major behaviors too
> I also think the library author has the right to change some function signatures when they change major behaviors too
When the the semantics change, it is already a backwards-incompatible change anyway, so it is cleaner to provide an extra function name for that new version.
There might be extreme cases where it is not possible or desirable to keep backwards compatibility, such as hardware bugs in Intel CPUs speculative execution, or APIs which turn out to be so thoroughly botched that they cannot be made safe. But these are exceptions.
One thing is to compose from backwards-compatible components makes it much much smother and easier to upgrade to new versions in a system which has many components. Paradoxically, it makes upgrading easier, and this is what library authors ultimately really want from users. While breaking compatibility does not only leads to a rat-tail of consequential breakage and forced upgrades which leads to more breakage, and makes everyone hesitant to do even small upgrades, ensuring backwards-compatibility leads to people upgrading without fear that they are interrupted for weeks and months.
Depending on a library could also insert a record on the library's CI system to indicate the dependency. The library CI will ping a URL with a version number to indicate that "you need to re-test with this version". It would certainly make software more robust and evolvable.
I've thought about it before and called it library mesh: https://github.com/samsquire/ideas2#10-library-mesh
If the updated library breaks your stuff then can't you just... not update?
And if you want to update to the new library for XYZ new cool stuff it does, then you are already putting in effort to utilise XYZ. In which case you can sort out the new ABC fix too.
It is extra work but it is part of having updated to the new coolness.
It annoys me when libraries "clean up" (churn) their codebase when they should've thought more carefully about designing the interface (the contract) before publishing it. Code churn often creates more pointless work for N library users. I swear that most code churn is to make a project look "alive" or to give people something to do / job security.
Deprecate slowly and have semi-automated user code fixups and notes.
It's a tradeoff, like everything. Try to get the API right the first time. Try to avoid introducing new APIs. Try to avoid breaking the old API. There's no silver bullet.
Instead we got https://github.com/pypa/pip/issues/8713 back in August and then https://github.com/pypa/pip/issues/9187 in November which is pinned and still open to this day.
They have a core library that other projects use.
They want to release a new version of this core library, and upgrade all the projects to use the newer version.
If the CI breaks for the projects using the library, they want to catch it.
This still depends on the projects using the library having enough and good enough testing for something to break in the CI when this core library does something bad. That is a huge investment (having those high quality tests). The rest of this is just a script that runs the tests from those projects when people merge code into the core library, which seems like very much not a big deal.
By pulling all the reverse dependencies and testing those _before_ releasing & upgrading you reduce the chance that you need another bugfix release.
And with hundreds of projects the average quality of tests does not matter that much. Some projects will happily pass, while some may fail. Those that fail will provide valuable insights.
I think there are actually at least 3 possibilities when you find a new version of a component breaks something that depends on it?
1) Continue using the old version for however long it still meets it's requirements and then do (2).
2) Make the local changes required to be compatible with the new version.
3) There is actually a bug in the new version and you need to fix the dependency.
And (1) is common in the open source and cross organization world. But it is less desirable if both the consumers and producers of the library work in the same place. Version spread causes maintenance cost.
And at one point it was a common theme that "an upgrade of JVMKit would have avoided this incident".