Have you ever ran into a situation where you wanted to version your application, but doing
git tag and making a separate commit for updating version number was a two-step process?
Isn’t that kinda frustrating? You can even miss to update the version number in your code,
which happens to be with me a lot. In this post we’ll see how to convert this two-step process into one.
- C/C++ Background Info
- Building the Sample Application
If you are an open-source enthusiast like me, you might have seen version number of software like these:
Which follows Semantic Versioning and is the most common and the recommended way. For example:
$ bat --version bat 0.15.0
The above version number is often complemented with one of the following.
20190202(which obviously is a date of build)
35fc2ad(which is a short version of sha1 hash of commit)
There might be others that I’m missing.
With help of a simple example, we’ll see how to combine these both steps into one. Once again let me remind what these two processes I am talking about:
- One is some kind of
versionvariable you maintain, so that it is visible when you do something like
- Second is the version number you maintain in your VCS (known as
tags in git terminology)
Let’s get with the example then?
You should have a fair knowledge in these two technologies.
- Bash (coreutils actually)
Some C/C++ background info
As a compiled language, Go inherits a lot from C, and behaves the same way C/C++ does.
I can’t say about other languages of the C family. But in C/C++ source code files are taken and compiled to object files. Then different object files are linked together to make the executable work.
Some program without main functions (libraries) are developed separately and are linked on runtime. There are benefits for developing libraries separately, but that’s not the scope of this post.
For a software written in C/C++ I can see list of dynamically linked libraries as follows:
$ ldd `which darktable` linux-vdso.so.1 (0x00007ffcc8fbc000) /usr/lib64/libstdc++.so.6 (0x00007f1a14ab0000) /lib64/libgcc_s.so.1 (0x00007f1a14a90000) libdarktable.so => /usr/bin/../lib64/darktable/libdarktable.so (0x00007f1a146e8000) libc.so.6 => /lib64/libc.so.6 (0x00007f1a144e8000) libm.so.6 => /lib64/libm.so.6 (0x00007f1a143a0000) /lib64/ld-linux-x86-64.so.2 (0x00007f1a14ca8000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f1a14378000) libgomp.so.1 => /lib64/libgomp.so.1 (0x00007f1a14330000) libglib-2.0.so.0 => /lib64/libglib-2.0.so.0 (0x00007f1a14200000) libgtk-3.so.0 => /lib64/libgtk-3.so.0 (0x00007f1a13a30000) libgdk-3.so.0 => /lib64/libgdk-3.so.0 (0x00007f1a13928000) [...]
In Go, there is one similar mechanism by which we can pass dynamic data to go build toolkit amid linking.
So without further ado, let’s see what it is.
Building the Sample Application
Below is the minimal working example I will use to demonstrate the workings on linking.
I have created a workspace named
autover and written a
main.go file in it.
Let’s compile and run?
$ go build && ./autover version: development
go build triggers
go tool link in background preceded by flags and object files.
From outside, we can pass
-ldflags to the linker.
This is how typically
-ldflag is passed to build process.
go build -ldflags="-flag key=value"
Now going forward, we can inject piece of text to a certain variable inside the code.
From the link docs:
-X importpath.name=value Set the value of the string variable in importpath named name to value. [...]
importpath.name is the name of the variable and
value is any text we want to assign to it.
This is how we’re gonna use this build flag:
go build -ldflags="-X 'main.Version=v0.1.1'"
Version variable from the example? Yes, we can modify using -ldflags. We have to quote anything passed to
-X though, just to make sure nothing breaks.
$ go build -ldflags="-X 'main.Version=v0.1.1'" && ./app version: v1.0.0
Do you see version number coming? Yes!!
The point to be noted is the variable name. It does not have to be
main.Version. Is can be anything, and any module other than
But how we gonna mix git tag information to it? We can use shell command substitution to put output of another shell command as an input to other command. Like from above you might have noticed I did
which darktable to
which darktable is substituted to
/usr/bin/darktable on my system and this is what passed to
Guess what we’ll pass to
-ldflags "-X 'main.Version'" instead
v0.1.1? Yes, the output of
The whole build command will be…
go build -ldflags="-X 'main.Version=`git tag`'" && ./autover
Ninja technique to get latest tag
Now you might be afraid what if there are more than one tags in the repo? Would the assignment be messed up? The answer is yes. To get the first line of the output I tried this:
git tag | tail -n 1
This worked until I had a few tags.
$ git tag 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 $ git tag | tail -n 1 1.2.6
But this will break on tags like
1.2.10 which actually comes to the first.
$ git tag 1.2.10 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9
As you can see, the latest tag is at the top. Very bad! Our method failed 😭. So much inconsistency.
After a few research, I found a way to get the latest tag the git way.
git describe --tags
Now you know what to do next.
As for the date and commit hash part, they can be achieved in the same manner.
Note: If you are doing this in package other than
main, you gotta use fully qualified package name.
Now you are ready to incorporate this into your more complex codebase. But the basics will be same. If you find any error or inconsistency, please let me know in the comment. Or if you are a twitter person, I’m @sntshk.