DEV Community

Cover image for Automate Versioning with Git and CMake
Amin Khozaei
Amin Khozaei

Posted on

Automate Versioning with Git and CMake

1. What is Software Versioning?

Software versioning is the practice of assigning unique version numbers to different states or releases of software. This process helps track the software's development, including bug fixes and feature changes over time. It enables developers, teams, and users to understand how the software evolves, what new features have been added, which bugs have been resolved, and what improvements to stability or performance have been made in each release.

Versioning is essential for preserving change histories, ensuring compatibility across various systems, managing dependencies, and providing clarity for end-users and other developers.

Using a formal convention for software versioning is highly beneficial. Once a clear convention is established, it informs all stakeholders—both internal and external—about the software's current state. Without a standardized versioning system, version numbers become meaningless to users and ineffective for managing dependencies. A well-structured version number conveys important information about the version's purpose and the extent of the changes it includes. One widely adopted convention for software versioning is semantic versioning.

SemVer

1.1. SemVer

Semantic Versioning, often abbreviated as SemVer, is a versioning system used to manage and communicate the evolution of software over time. It provides a structured, predictable way to number software releases, making it clear to developers and users what kind of changes have been made in a particular release.

The version number follows a three-part structure: MAJOR.MINOR.PATCH.

For example, a version number could be 1.5.2, where:

1 is the MAJOR version
5 is the MINOR version
2 is the PATCH version
Enter fullscreen mode Exit fullscreen mode

1.1.1. MAJOR version

The MAJOR version is incremented when there are backward-incompatible changes to the software. This means that changes are significant enough to break existing functionality or APIs that depend on the previous version.
Examples of such changes include removing or renaming functions, changing expected behavior, or modifying core components that affect the user interface or workflow.
Example: If you were to upgrade from 1.0.0 to 2.0.0, this would indicate that the software has undergone significant changes that require users to adjust their workflows or code to remain compatible.

1.1.2. MINOR version

The MINOR version is incremented when backward-compatible features are added to the software. This includes the addition of new features, improvements to existing functionality, or changes that do not disrupt the existing behavior or compatibility of the software.
The key idea here is that users can safely update to the new minor version without worrying about breaking their current workflows.
Example: Upgrading from 1.2.0 to 1.3.0 signifies that new features or improvements were added, but the software remains fully compatible with older versions.

1.1.3. PATCH version

The PATCH version is incremented when backward-compatible bug fixes or minor improvements are made. These changes should not affect the software’s core functionality but are intended to fix issues like bugs, security vulnerabilities, or performance optimizations.
Example: If a release goes from 1.5.2 to 1.5.3, it indicates that bug fixes or minor improvements have been made, but the core functionality of the software has not changed.

Pre-release Versions

1.2. Pre-release Versions

Pre-release versions are crucial for software testing and early feedback. Users and developers can install and test pre-release versions, report bugs, and give feedback before the final stable release.
It’s important to note that pre-release versions are considered unstable, and their behavior may change dramatically between versions. SemVer ensures that developers and users know when to expect such instability and to use these versions with caution.
SemVer also allows for pre-release versions, which are versions of the software that are still under development and might be unstable. These versions can be labeled using a hyphen followed by one or more identifiers.
Pre-release versions are typically assigned to versions before the software is considered ready for general use. These versions can include:

  • Alpha versions, which are early-stage releases that may be incomplete and have many bugs.
  • Beta versions, which are more stable than alpha releases but still may have bugs and are intended for broader testing.
  • Release candidate (RC) versions, which are considered feature-complete and are under final testing before the full release.

2. Automating SemVer with Git

Utilizing Git for source code management alongside automated generation of Semantic Versioning (SemVer) numbers can significantly enhance the efficiency of development and release processes. By integrating Git with appropriate tools, organizations can automate versioning based on repository commits and tags. This methodology proves particularly beneficial for teams engaged in Continuous Integration and Continuous Deployment (CI/CD), promoting a more streamlined workflow.

Integrating SemVer with CMake

2.1. Integrating SemVer with CMake

Integrating semantic versioning with CMake provides a structured approach to ensuring that builds are consistently versioned in alignment with Git history. This practice is particularly beneficial for maintaining clarity and consistency across releases.

2.1.1. Template Files

In C/C++ projects, particularly when using build systems like CMake, template files serve as a way to automate the generation of source or header files with specific content that can be configured at build time.
Template files are essentially placeholder files that contain variables or markers. These variables are replaced with specific values during the build process. This approach is useful for dynamically generating configuration headers, version files, or other source files that need to change based on the build context.
Template files often have distinctive suffixes, such as .in. The .in suffix helps developers and build systems recognize that a file is a template, not a regular source or header file. This suffix indicates that the file contains placeholders that need to be replaced with actual values during the build process.

file: version.h.in

#ifndef GSMAPP_VERSION_H
#define GSMAPP_VERSION_H

#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define FULL_VERSION "@FULL_VERSION@"

#endif
Enter fullscreen mode Exit fullscreen mode

Placeholders (@...@): Each placeholder will be replaced by CMake (or any build system used) with the actual values specified in the CMake configuration process. For example, @PROJECT_VERSION_MAJOR@ might be replaced with 1, @PROJECT_VERSION_MINOR@ with 0, etc.

file: CMakeLists.txt

configure_file(
    ${CMAKE_SOURCE_DIR}/version.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/version.h
)
Enter fullscreen mode Exit fullscreen mode

The configure_file command in CMake is used to read a template file, replace placeholders with actual values, and write the results to a new file.

file: CMakeLists.txt

target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
Enter fullscreen mode Exit fullscreen mode

The CMake command target_include_directories specifies the include directories to be used by a target (typically an executable or a library) in a project.

2.2. Extract version information from a Git repository

file: cmake/GitVersion.cmake

function(get_version_from_git)
    find_package(Git QUIET)
    if(NOT Git_FOUND)
        message(WARNING "Git not found")
        return()
    endif()

    execute_process(
        COMMAND ${GIT_EXECUTABLE} describe --tags --always
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        OUTPUT_VARIABLE GIT_TAG
        OUTPUT_STRIP_TRAILING_WHITESPACE
        RESULT_VARIABLE GIT_RESULT
    )

    if(NOT GIT_RESULT EQUAL 0)
        message(WARNING "Failed to get git tag")
        return()
    endif()

    execute_process(
        COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        OUTPUT_VARIABLE GIT_COMMIT_SHORT_HASH
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )

    string(REGEX REPLACE "^v" "" CLEAN_TAG "${GIT_TAG}")
    if(CLEAN_TAG MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.*)?$")

        set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1})
        set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1} PARENT_SCOPE)
        set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_2})
        set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_2} PARENT_SCOPE)
        set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_3})
        set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_3} PARENT_SCOPE)

        set(FULL_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}+${GIT_COMMIT_SHORT_HASH}")
        set(FULL_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}+${GIT_COMMIT_SHORT_HASH}" PARENT_SCOPE)
        set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") 
        set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE)
    else()
        message(WARNING "Tag '${CLEAN_TAG}' does not match semver format")
    endif()
endfunction()
Enter fullscreen mode Exit fullscreen mode
  • Finding Git: Checks whether Git is installed and available. The find_package(Git QUIET) command attempts to locate the Git executable without printing messages upon finding Git.

  • Obtaining the Git Tag: Uses the git describe --tags --always command to retrieve the most descriptive version string possible, based on tags. This command returns the tag name or, if no tag is available, a commit hash.

  • Getting the Commit Hash: Retrieves the short hash of the current commit, limited to 7 characters. This is useful for constructing unique build identifiers.

  • Processing the Tag: Removes a leading ‘v’ from the tag name to normalize it using string(REGEX REPLACE "^v" "" CLEAN_TAG "${GIT_TAG}") command.

  • Semantic Versioning Check: Checks if CLEAN_TAG matches a semantic versioning pattern (e.g., 1.0.0). It captures the major, minor, and patch versions.

2.3. using our custom CMake module

file: CMakeLists.txt

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(GitVersion)
get_version_from_git()
Enter fullscreen mode Exit fullscreen mode
  • Appending to CMAKE_MODULE_PATH: CMAKE_MODULE_PATH is a list of directories that CMake uses to search for modules when processing include() or find_package() commands. This line adds a directory named cmake (located in the root source directory of the project) to the list of paths where CMake looks for custom modules.

  • Including the GitVersion Module: The include() command in CMake is used to include and run another CMake script or module file. This command will look for a file named GitVersion.cmake in the directories listed in CMAKE_MODULE_PATH.

  • Calling get_version_from_git(): This function, defined within the included GitVersion.cmake, is called here to extract version information from the Git repository.

2.4. Printing Version Information

This C code provides a simple function to print version information about an application, highlighting the use of versioning data integrated into the build process.

file: main.c

#include "version.h"

void print_version() {
    printf("Application Version: %s\n", PROJECT_VERSION);
    printf("Full Version: %s\n", FULL_VERSION);
    printf("Version Details:\n");
    printf("Major: %d\n", PROJECT_VERSION_MAJOR);
    printf("Minor: %d\n", PROJECT_VERSION_MINOR);
    printf("Patch: %d\n", PROJECT_VERSION_PATCH);
    printf("Build Date: %s\n", __DATE__);
    printf("Build Time: %s\n", __TIME__);
}
Enter fullscreen mode Exit fullscreen mode

Resources

Top comments (0)