Library Versions?

When I look around, it seems that Clojure versions are a bit strange. I suspect due to Lein default template mostly. But so many libs begin at 0, or even double zeros 0.0.5

Now, I don’t mind the zero, my problem is why do we have three version numbers?

Lein doesn’t do version ranges, and I don’t think deps.edn does either.

For all that SemVer is criticized for, it seems to me much better then this random 3 numbers that mean nothing at all.

So I was thinking, and here’s a scheme I feel would be pretty interesting:

<clojure-version>.<lib-version>

Where <clojure-version> is the minimum version of Clojure you depend on. And <lib-version> is just a number you increment on any change: backward compatible bug-fix or feature.

The rule is: NEVER BREAK backward compatibility. The only exception to this is taking a dependency on a newer version of Clojure. In which case, the <clojure-version> would get updated accordingly.

If you need to make a backward incompatible change, create a whole new lib with a number appended to the name: my-lib-2 and then my-lib-3. All namespaces MUST reflect this name change as well, to allow both old and new to coexist.

Thoughts?

2 Likes

Clojure is different from scala, backward compatibility has been very good.

I may be mistaken, but isn’t changing clojure dependency to newer version a breaking change for projects stuck on previous versions of clojure?

1 Like

I had a couple of coworkers worrying about version numbers in Clojure libraries, but the fact is that they are pointless anyway!

Just take a look at TensorFlow where you have breaking changes even for minor versions, or Python 1 vs 2 vs 3 and you can see that version numbers are a broken way to convey meaning in software.

I’d rather have better references to whether a library is maintained or not than focusing on “0.0.7” or “1.0”

2 Likes

Rich’s thoughts on versioning (and other stuff around artifacts) – in the linked video, with some interesting commentary on his opinions: https://news.ycombinator.com/item?id=13085952

Clojure libraries use a very wide range of practices for version numbers. At one point, the Clojure/core folks had strong opinions on when/whether a Contrib library could claim “1.0.0” but, yes, the default Leiningen new project version being 0.1.0-SNAPSHOT is partly to blame for all the 0.x.y versions (and for the plethora of *.core namespaces in projects).

If you never break backward compatibility, then pretty much any monotonically increasing version numbering is fine (from the point of view of easily telling whether one version of a library is newer or older than another). Tagging something by date is just as good as anything else.

I quite like the idea of <clojure-version>.<lib-version> (although Clojure itself has three parts to its version number – and has used them all occasionally – so this would give us numbers like 1.5.1.34, unless you just took the “important” part of Clojure’s version… which, according to SemVer, is the “minor” part :slight_smile:).

1 Like

Yes it is, that’s why your major version would bump accordingly.

1.7.4 - Works with 1.7
1.9.4 - Same lib that upgraded to 1.9.

The reason I make dependency on Clojure be in the version, and not require a whole new lib is two fold. It makes it very clear what’s the minimum version it works on. And because you can never have two Clojure dependencies coexist, so you’re obligated to deal with it.

Ah, I was wondering about that. I mean it could be 1.9.0.6 being 1.9.0 minimum Clojure version and 6th iteration of the lib. Even though that’s starting to bother my OCD :stuck_out_tongue:

I think the minor part in Clojure is always bug fix though no? Or are things added to the APIs as well?

I was also wondering for cljc libs, if you’d want the ClojureScript version as well: <clojure-version>.<clojurescript-version>.<lib-version>

These are the issues I face on my team, that I’m trying to address with this scheme:

  1. Upgrading to a new version of a lib which has upgraded to a newer version of Clojure while we have not upgraded yet.

It seems no one realizes a Clojure minimum dependency upgrade is a breaking change. For us, it is the most common breaking change. And we often can’t tell why the library upgraded? Does it actually need the new Clojure APIs, or it just thought it upgrade to keep up? Other time, I just can’t even tell what version the library targets.

Putting that in your lib version would make it really clear. If you updated Clojure min version and I’m not on that version, then I know which version of your lib I can and can’t use.

  1. There’s no way currently to know if a new version of a lib has broken its prior contract.

How do I know it is safe to upgrade? This problem is escalated with transitive dependencies. If a dependency depends on a newer version of a lib which I also depend on, how do I know which version I can force?

I’m taking Rich’s advice here. Lets just stop breaking libraries completly. Just don’t. Then it is always safe to upgrade everything. In case of conflict, you just force the latest, easy, simple.

  1. I have dependencies that conflict in library version, but neither can be forced, because their API diverges.

This means if I upgrade X, I have to use upgraded Y, but Z depends on old Y. Yet old and new Y have diverging APIs, thus the only solution is to patch Z or stop using Z or X.

Again, I take something from Rich’s book here, when you change APIs, when you change semantics in backward incompatible ways, that’s no longer the same library, just create a new one with a new namespace.

Now both dependencies are free to use whatever library they prefer. And they can coexist without conflict.

2 Likes

Wouldn’t that have to be 1.9.5 since it’s also a new version of the library?

Note that there’s no guarantee that version 1.7.4 will work with Clojure 1.8 or 1.9 so I’m not convinced having the Clojure version in the library version buys you a whole lot (even tho I kinda like the idea). We saw quite a few libraries break when core specs were introduced, for example.

I’m with you on not being convinced. Personally, I’m thinking more in terms of when a library was released - e.g. 1811.x. When you have major changes you bump up the date, hoping you don’t make major changes more than once a month :slight_smile:

You don’t know much about compatibility, but at least you know if your artifact is from aeons ago or pretty recent (i.e., maintained).

Now that we have the CLI tools and deps.edn, we don’t even have to release versions of libraries – users can just depend on a given commit (via a Git SHA) – although a “random” hex string offers no visual clues about age so not everyone feels comfortable with that. It’s why I’m still releasing artifacts to Clojars for clj-new and my fork of depstar.

I wonder tho’, is it just the newness of :git/url and :sha that we find off-putting and will we become more comfortable with this “no-version” way of handling dependencies?

The nice thing about the sha is you could have a tool that tells you how many commits you are behind, how far (in time) you are behind head, and even give you a diff between your two versions.

Man, I wonder if you could optimistically commit each version bump on a separate branch, and use your tests with git-bisect to find out when you broke, (assuming you broke).

I guess my point isn’t really how we convey it, we could probably brain storm something better.

But mostly, that we should be able to assume:

  1. Libs are always safe to upgrade, because breaking changes are released as a new lib under a new namespace.

  2. That it will work for a given version of Clojure or ClojureScript, because the versions of Clojure and ClojureScript that a given version of a lib supports are explicitly documented.

If all Clojure/Script libs tried to adhere to these two rules, it would be amazing!

Personally I find git SHAs to be semantically useless. They do have the nice property of being immutable, but there’s no way to look them up in release notes or a changelog, and you can’t easily compare them to see which versions are newer or older. I’d much rather rely on a git tag, even though I know it’s mutable, just for the ease of looking through the changelog for that version number.

I worry that by moving to git SHAs, we’re moving from a unit of “changes” that the library author intended to be a single coherent change (by releasing a new version) to a unit that may not be intended to be used by anyone, or even used at all, but may instead be the midway point of a large refactoring or something.

2 Likes

Regular git tags are mutable, but aren’t signed tags immutable? What if it only allowed signed tags?

Would changing the versioning scheme considered a breaking change ?

Regarding SHA, I think we should publish libraries with tags often (almost always?) so that people relying on a SHA have an easy way to find what’s included in the change log. Agreeing with @timgilbert.

I agree that library authors shouldn’t make breaking changes but I believe that as library consumers we shouldn’t just assume that’s the case.

I fully agree with

In addition, there’s always a problem of transient dependencies mismatch.

Rather than relying on conventions, we could look into a possibility of creating a tool that would somehow (with help of codeq?) deduce the confidence of library being backward compatible.

Some magical static analysis tool and all would be nice for sure. But I think people underestimate how far a community convention could go. I think Clojure could pave the way here.

Just don’t ever break anything.

Maybe I made things more complicated with bundling the Clojure version in.

Let me propose a simpler scheme:

<lib-version>

That’s all. Just one chronologically increasing number or timestamp I don’t care. With the tenet that libs will never break public APIs ever. This could be a git sha also, you can tell the chronology from the git history so it doesn’t matter much.

And when you want a better API, just roll a new lib under a new namespace.

I can look at your project.clj or deps.edn file to figure out your min Clojure version, so I don’t really need it in the version range I guess.

Maybe I need to put some promo material for this, come up with a name and design a seal that libs can put on their page :yum:

2 Likes

So, like zerover, but unironically?

Well, this is what Clojure/core is already doing. In version 1.9.0, the 1 is here only to satisfy the server convention, and what you call lib-version is the 9. Clojure rarely use the last number, but it happened for bug fixes.

In this sense Clojure is almost compatible with your vision but also follows semantic versioning correctly. It “just” doesn’t introduce breaking change. Doing the same in libraries is enough I think.

The “problem” I often see is libraries sticking to pre-1.0 versions, where numbers can mean literally anything. Is it because of the fear of supporting an API? Lack of confidence?