Friday, October 18, 2013

Versioning For Dummies

This post has moved! Click here to view the new home of this post.

If you are writing a library you need to understand version numbers, otherwise you will create hell for everyone who every wants to use it. There is a formal writeup for this approach on semver.org which I recommend you read, but I am going to go into depth about making your software future proof for you and everyone who uses it.

Hall of Fame

A great example of good versioning is UNIX shared libraries. They have the major version number in their soname so that version errors can be detected and they are linked to their basename so that accessing them is easy.

For example, I have a library foo version 1.7.2. I create the file libfoo.so.1.7 with a soname of libfoo.so.1 Then I link that file to libfoo.so.1 and libfoo.so. This way if I try to link with it clang -lfoo prog1.c it hits libfoo.so and gets linked with the soname libfoo.so.1 therefore when I try to run it it hits libfoo.so.1 and gets the correct version.

That's all fine and dandy but what about a different program that uses libfoo version 2? It all works fine. When I install libfoo 2 it gets installed as libfoo.so.2.0.0 with soname libfoo.so.2. The symlinks are similar to version one, however the newer version generally gets the raw version libfoo.so -> libfoo.so.2.0.0. Now program 1 still finds the version it needs and program two clang -lfoo prog2.c gets compiled with and uses the new version. Everyone is happy.

But what happens if I need to rebuild program 1? It's very simple. Instead of using the shortcut for choosing the latest version you must select it manually clang /usr/lib/libfoo.so.1 prog1.c. This isn't quite as pretty but it keeps old programs working until they are updated to use the newer version of the library.

Hall of Shame

The package management done by npm makes me cry. It can best be described as a hack.

When you install dependencies it installs them locally, right beside your program, then for each dependency it installs them inside their directory. npm doesn't solve the versioning problem, it avoids it. Now each library you use has independent copies of its libraries. This is a waste of just about every resource, all to avoid conflicts between your dependency using a different versions of libraries than you. This could easily be solved with a simple naming scheme and good versioning but instead all efficiency and convenience is thrown away. (Did you remember to update foo's bar's baz's copy of libbugy?).

Well How Do I do Better?

Option 1: Never break backwards compatibility.

If you are actually thinking about this slap yourself. You will eventually want to break compatibility, I'm not recommending that you do it often but eventually breaking compatibility allows your code to tidy itself up instead of being a mess of hacks to enable backwards compatibility.

Option 2: Write your software for the future.

Allowing multiple versions side-by-side

There you go! Let's work through this. The first step is laying out your software to allow for future expansion. For example, if you are creating a config file use /etc/fooX.cfg where X is the major version number. This way two versions can be installed side by side. Do this for all of your paths /usr/lib/fooX/*, ~/.config/fooX/ et cetera. Depending on what language you are using you might also want to pick a different namespace. If you have any executables call them fooX and optionally provide a symlink to foo for ease of use.

Using version numbers correctly.

Other than that you just need to version your software correctly. To provide people with meaningful version information you will need three numbers. Here I will call them major, minor and patch although they are often called different things. The way I sum the number up is that any software that works with version a.b.c will also work with version x.y.z provided that x == a and y >= b. I will explain the components in a bit.

The major version number is simple. If ANY backwards incompatible changes are made the major version number must be changed. The only exception to this is version 0.*.* where stability is not guaranteed. So mess around on version zero for as long as you like but as soon as you hit version 1 you need to bump the major version every time you make an incompatible change.

The minor version number is for new features. Every time you add something this number must be raised. This allows people who use these new features to specify that they need at least the version which they were added in. Every time you add a new feature you must bump this version.

The patch version is just to make different releases distinct. If you make a change that doesn't affect the intended public API this is the version to change. I say intended because fixed a bug that made some function give wrong values is considered a patch level change.

Thats all there is to it. Now for a quick overview of comparing version numbers. I will be comparing the version needed a.b.c to the version we have available x.y.z. First we will compare the major version. The major version numbers shall only ever be compared for equality. If a is different than x the versions are incompatible. Next we will compare the minor versions, minor versions are the only component that the order is relevant. If y is less than or equal to b than it is considered to be satisfied. The patch versions are not compared, they are irrelevant when determining if versions are compatible.

I have written a quick program to compare versions for those who find that easier to read.

#include <assert.h>

/// Check if versions are compatible.
/**
 * Compares the required and available versions and returns true iff the
 * requirement is satisfied.
 */
int compatible(unsigned r_major, unsigned r_minor, unsigned r_patch,
               unsigned a_major, unsigned a_minor, unsigned a_patch)
{
 if ( r_major == 0 )
 {
  return r_major == a_major && // For version 0 there are no compatibility
         r_minor == a_minor && // guarantees, so if the versions are different
         r_patch == a_patch ;  // we must assume they are not compatible.
 }
 if ( r_major != a_major ) return 0; // Available version is not backwards compatible.
 if ( r_minor >  a_minor ) return 0; // Needs features not in available version.
 
 // Don't compare minor versions, they are not relevant.
 
 return 1;
}

int main (int argc, char **argv)
{
 assert(compatible(1,2,3, 1,2,3));
 assert(compatible(0,2,3, 0,2,3));
 assert(compatible(1,2,1, 1,2,3));
 assert(compatible(1,2,5, 1,2,3));
 assert(compatible(1,2,3, 1,5,2));
 
 assert(!compatible(0,2,4, 0,2,3));
 assert(!compatible(0,3,3, 0,2,3));
 assert(!compatible(1,3,3, 1,2,3));
 assert(!compatible(1,2,3, 2,2,3));
 assert(!compatible(2,2,3, 1,2,3));
}

No comments:

Post a Comment