This page looks best with JavaScript enabled

Dynamically Insert Version Info From git tag to Your App

 ·   ·  ☕ 5 min read

    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.

    Contents:


    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 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:

    1. One is some kind of version variable you maintain, so that it is visible when you do something like myapp --version.
    2. Second is the version number you maintain in your VCS (known as tags in git terminology)

    Let’s get with the example then?

    Prerequisites

    You should have a fair knowledge in these two technologies.

    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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    package main
    
    import "fmt"
    
    var Version = "development"
    
    func main() {
        fmt.Println("version: ", Version)
    }
    

    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.

    Share on

    Santosh Kumar
    WRITTEN BY
    Santosh Kumar
    Fullstack Developer at Method Studios