Introduction
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.
Take a moment to connect with me on LinkedIn.
If you are an open-source enthusiast like me, you might have seen version number of software like these:
1.0.2
/v1.0.2
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 which 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
version
variable you maintain, so that it is visible when you do something likemyapp --version
. - Second is the version number you maintain in your VCS (known as
tag
s in git terminology)
Let’s get with the example then?
Prerequisites
You should have a fair knowledge in these two technologies.
- Go
- Git
- 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
ldflags
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'"
Remember the 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 main
.
git tag
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 ldd
command. which darktable
is substituted to /usr/bin/darktable
on my system and this is what passed to ldd
command.
Guess what we’ll pass to -ldflags "-X 'main.Version'"
instead v0.1.1
? Yes, the output of git tag
.
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.
Related reading:
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.
Subscribe below for more articles like this one.