4.4.6. Making Versioning Consistent

When you make a new version / revision of your product, you need to update it at many places. e.g. Number that is shown in about section of a GUI Based program, inside your generated documentation, in the output of --version of the command line application.

Manually updating it at many places is error-prone. Hence, have a strategy in place that you can be consistent everywhere. You only need to update version number at once place and everywhere else it should get automatically updated.

Here is a sample script (Helper Script: doNewVersion.py) that can generate create a version information files for various applications that you may need while building your software project.

If we run above script with:

python my_version_file_generator.py 1 2 7

the following files are generated.

The file my_Product_Ver.h can be included inside C/C++ based build.
/* (C) 2024 Purnank */

/* Version information for my_Product for C/C++ files */

#ifndef MY_PRODUCT_VER_H
#define MY_PRODUCT_VER_H

#define MY_PRODUCT_VER_MAJOR (1)
#define MY_PRODUCT_VER_MINOR (2)
#define MY_PRODUCT_VER_DEV   (7)
#define MY_PRODUCT_VER_STR   "1.2.7"

#endif /* MY_PRODUCT_VER_H *
The file my_Product_Ver.nsh can be used for NSIS Installers.
/* (C) 2024 Purnank */

/* Version information for my_Product for NSIS Installer
 *
 * Include this file as::
 *
 *       !include /path/to/this/file.nsh
 */

!define MY_PRODUCT_VER_FULLSTRING         "my_Product_1.2.7"
!define MY_PRODUCT_VER_FILEVERSION        "1.2.7.0"
!define MY_PRODUCT_VER_MAJOR              "1"
!define MY_PRODUCT_VER_MINOR              "2"
!define MY_PRODUCT_VER_DEV                "7"
The file my_Product_Ver.mk can be used by Makefiles.
# (C) 2024 Purnank

# Version information for my_Product for Make Files
#
# Include this file as::
#
#       include /path/to/this/file
#

MY_PRODUCT_VER_MAJOR=1
MY_PRODUCT_VER_MINOR=2
MY_PRODUCT_VER_DEV=7
The file my_Product_Ver.cmake can be used inside CMake based builds.
# (C) 2024 Purnank

# Version information for my_Product for CMake Files
#
# Include this file as::
#
#       INCLUDE(/path/to/this/file)
#


SET(MY_PRODUCT_VER_MAJOR "1")
SET(MY_PRODUCT_VER_MINOR "2")
SET(MY_PRODUCT_VER_DEV   "7")
The file my_Product_Ver.doxy can be used in doxygen.
# (C) 2024 Purnank

# Version information for my_Product for Doxygen
#
# Include this file as::
#
#       @INCLUDE /path/to/this/file
#

PROJECT_NUMBER         = "v1.2.7"

4.4.6.1. Capturing Git Information

You can capture information about the build from Git version control system.

Command

Information

Sample output

git describe

Shows human-readable information of your product

It depends on an annotated tag (Tag created with either a message for with -a flag )

v0.1-28-g3124b20

i.e. last tag was v0.1 and there are 28 revisions after

that.

git rev-parse HEAD

Unique SHA1 of the current commit

3124b2012f29f3ae520273d796f364f0430bfc75

git diff --quiet

Useful in shell scripts.

Returns 0 if there are no changes.

Returns 1 if there are changes.

Sample script:

git diff --quiet; if [[ $? == 0 ]]; then echo No Changes;
else echo There are changes; fi

git rev-parse --abbrev-ref HEAD

Get the current branch name

main

git tag --points-at HEAD

Get the TAG for this revision

MY_TAG_v1.2.3

git describe --exact-match --tags

Get the TAG for this revision

Same as above

MY_TAG_v1.2.3

4.4.6.2. Helper Script: doNewVersion.py

Here is the content of the helper script that can generate version information for various types of build steps.

The file my_version_file_generator.py
# (C) 2024 Purnank
""" Helper script to generate version information.
"""

import sys

PRODUCT_NAME = "my_Product"
COPYRIGHT = "(C) 2024 Purnank"


def main():
    """ Entry point of the file """
    if len(sys.argv) == 4:
        all_decimals = map(lambda x: x.isdecimal(), sys.argv[1:])
        if False in all_decimals:
            usage("Not all version numbers are decimal")
        else:
            (major, minor, dev) = [int(i) for i in sys.argv[1:]]
            my_version_file_generator(major, minor, dev)
    elif len(sys.argv) > 4:
        usage("Extra arguments passed")
    else:
        usage("Not enough arguments")


def usage(error):
    """ Print Usage """
    print("\n")
    print("Error: %s\n" % error)
    print("Usage:\n")
    print("    %s <major> <minor> <dev>" % sys.argv[0])
    print("\n")
    print("Please ensure <major> <minor> and <dev> are all decimal numbers.")
    print("\n")


TEMPLATES_H = """/* {copyright} */

/* Version information for {my_product} for C/C++ files */

#ifndef {my_product_u}_VER_H
#define {my_product_u}_VER_H

#define {my_product_u}_VER_MAJOR ({major})
#define {my_product_u}_VER_MINOR ({minor})
#define {my_product_u}_VER_DEV   ({dev})
#define {my_product_u}_VER_STR   "{major}.{minor}.{dev}"

#endif /* {my_product_u}_VER_H *

"""

TEMPLATES_NSH = """/* {copyright} */

/* Version information for {my_product} for NSIS Installer
 *
 * Include this file as::
 *
 *       !include /path/to/this/file.nsh
 */

!define {my_product_u}_VER_FULLSTRING         "{my_product}_{major}.{minor}.{dev}"
!define {my_product_u}_VER_FILEVERSION        "{major}.{minor}.{dev}.0"
!define {my_product_u}_VER_MAJOR              "{major}"
!define {my_product_u}_VER_MINOR              "{minor}"
!define {my_product_u}_VER_DEV                "{dev}"

"""
TEMPLATES_MAKE = """# {copyright}

# Version information for {my_product} for Make Files
#
# Include this file as::
#
#       include /path/to/this/file
#

{my_product_u}_VER_MAJOR={major}
{my_product_u}_VER_MINOR={minor}
{my_product_u}_VER_DEV={dev}

"""
TEMPLATES_CMAKE = """# {copyright}

# Version information for {my_product} for CMake Files
#
# Include this file as::
#
#       INCLUDE(/path/to/this/file)
#


SET({my_product_u}_VER_MAJOR "{major}")
SET({my_product_u}_VER_MINOR "{minor}")
SET({my_product_u}_VER_DEV   "{dev}")

"""
TEMPLATES_DOXYGEN = """# {copyright}

# Version information for {my_product} for Doxygen
#
# Include this file as::
#
#       @INCLUDE /path/to/this/file
#

PROJECT_NUMBER         = "v{major}.{minor}.{dev}"
"""

EXTENSION_TEMPLATES = {
    ".h": TEMPLATES_H,
    ".nsh": TEMPLATES_NSH,
    ".mk": TEMPLATES_MAKE,
    ".cmake": TEMPLATES_CMAKE,
    ".doxy": TEMPLATES_DOXYGEN,
}


def my_version_file_generator(major, minor, dev):
    """ Generate version files for given major.minor.dev """
    print("Generating version files for %d.%d.%d" % (major, minor, dev))
    values = {
        "copyright": COPYRIGHT,
        "my_product": PRODUCT_NAME,
        "my_product_u": PRODUCT_NAME.upper(),
        "major": major,
        "minor": minor,
        "dev": dev,
    }
    for ext, template in EXTENSION_TEMPLATES.items():
        file_name = "%s_Ver%s" % (PRODUCT_NAME, ext)
        with open(file_name, "w") as ver:
            ver.write(template.format(**values))
        print(".. Updated %s" % file_name)


if __name__ == '__main__':
    main()