1# git_watcher.cmake 2# 3# License: MIT 4# Source: https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git_watcher.cmake 5 6 7# This file defines the functions and targets needed to monitor 8# the state of a git repo. If the state changes (e.g. a commit is made), 9# then a file gets reconfigured. 10# 11# The behavior of this script can be modified by defining any of these variables: 12# 13# PRE_CONFIGURE_GIT_VERSION_FILE (REQUIRED) 14# -- The path to the file that'll be configured. 15# 16# POST_CONFIGURE_GIT_VERSION_FILE (REQUIRED) 17# -- The path to the configured PRE_CONFIGURE_GIT_VERSION_FILE. 18# 19# GIT_STATE_FILE (OPTIONAL) 20# -- The path to the file used to store the previous build's git state. 21# Defaults to the current binary directory. 22# 23# GIT_WORKING_DIR (OPTIONAL) 24# -- The directory from which git commands will be run. 25# Defaults to the directory with the top level CMakeLists.txt. 26# 27# GIT_EXECUTABLE (OPTIONAL) 28# -- The path to the git executable. It'll automatically be set if the 29# user doesn't supply a path. 30# 31# Script design: 32# - This script was designed similar to a Python application 33# with a Main() function. I wanted to keep it compact to 34# simplify "copy + paste" usage. 35# 36# - This script is made to operate in two CMake contexts: 37# 1. Configure time context (when build files are created). 38# 2. Build time context (called via CMake -P) 39# If you see something odd (e.g. the NOT DEFINED clauses), 40# consider that it can run in one of two contexts. 41 42# Short hand for converting paths to absolute. 43macro(PATH_TO_ABSOLUTE var_name) 44 get_filename_component(${var_name} "${${var_name}}" ABSOLUTE) 45endmacro() 46 47# Check that a required variable is set. 48macro(CHECK_REQUIRED_VARIABLE var_name) 49 if(NOT DEFINED ${var_name}) 50 message(FATAL_ERROR "The \"${var_name}\" variable must be defined.") 51 endif() 52 PATH_TO_ABSOLUTE(${var_name}) 53endmacro() 54 55# Check that an optional variable is set, or, set it to a default value. 56macro(CHECK_OPTIONAL_VARIABLE var_name default_value) 57 if(NOT DEFINED ${var_name}) 58 set(${var_name} ${default_value}) 59 endif() 60 PATH_TO_ABSOLUTE(${var_name}) 61endmacro() 62 63CHECK_REQUIRED_VARIABLE(PRE_CONFIGURE_GIT_VERSION_FILE) 64CHECK_REQUIRED_VARIABLE(POST_CONFIGURE_GIT_VERSION_FILE) 65CHECK_OPTIONAL_VARIABLE(GIT_STATE_FILE "${GENERATED_SRC}/git_state") 66#CHECK_REQUIRED_VARIABLE(GIT_STATE_FILE) 67CHECK_OPTIONAL_VARIABLE(GIT_WORKING_DIR "${PROJECT_SOURCE_DIR}") 68 69# Check the optional git variable. 70# If it's not set, we'll try to find it using the CMake packaging system. 71if(NOT DEFINED GIT_EXECUTABLE) 72 find_package(Git QUIET) 73endif() 74CHECK_REQUIRED_VARIABLE(GIT_EXECUTABLE) 75 76 77 78# Function: GitStateChangedAction 79# Description: this function is executed when the state of the git 80# repo changes (e.g. a commit is made). 81function(GitStateChangedAction _state_as_list) 82 # Set variables by index, then configure the file w/ these variables defined. 83 LIST(GET _state_as_list 0 GIT_RETRIEVED_STATE) 84 LIST(GET _state_as_list 1 GIT_HEAD_SHA1) 85 LIST(GET _state_as_list 2 GIT_IS_DIRTY) 86 configure_file("${PRE_CONFIGURE_GIT_VERSION_FILE}" "${POST_CONFIGURE_GIT_VERSION_FILE}" @ONLY) 87endfunction() 88 89 90 91# Function: GetGitState 92# Description: gets the current state of the git repo. 93# Args: 94# _working_dir (in) string; the directory from which git commands will be executed. 95# _state (out) list; a collection of variables representing the state of the 96# repository (e.g. commit SHA). 97function(GetGitState _working_dir _state) 98 99 # Get the hash for HEAD. 100 set(_success "true") 101 execute_process(COMMAND 102 "${GIT_EXECUTABLE}" rev-parse --verify HEAD 103 WORKING_DIRECTORY "${_working_dir}" 104 RESULT_VARIABLE res 105 OUTPUT_VARIABLE _hashvar 106 ERROR_QUIET 107 OUTPUT_STRIP_TRAILING_WHITESPACE) 108 if(NOT res EQUAL 0) 109 set(_success "false") 110 set(_hashvar "GIT-NOTFOUND") 111 endif() 112 113 # Get whether or not the working tree is dirty. 114 execute_process(COMMAND 115 "${GIT_EXECUTABLE}" status --porcelain 116 WORKING_DIRECTORY "${_working_dir}" 117 RESULT_VARIABLE res 118 OUTPUT_VARIABLE out 119 ERROR_QUIET 120 OUTPUT_STRIP_TRAILING_WHITESPACE) 121 if(NOT res EQUAL 0) 122 set(_success "false") 123 set(_dirty "false") 124 else() 125 if(NOT "${out}" STREQUAL "") 126 set(_dirty "true") 127 else() 128 set(_dirty "false") 129 endif() 130 endif() 131 132 # Return a list of our variables to the parent scope. 133 set(${_state} ${_success} ${_hashvar} ${_dirty} PARENT_SCOPE) 134endfunction() 135 136 137 138# Function: CheckGit 139# Description: check if the git repo has changed. If so, update the state file. 140# Args: 141# _working_dir (in) string; the directory from which git commands will be ran. 142# _state_changed (out) bool; whether or no the state of the repo has changed. 143# _state (out) list; the repository state as a list (e.g. commit SHA). 144function(CheckGit _working_dir _state_changed _state) 145 146 # Get the current state of the repo. 147 GetGitState("${_working_dir}" state) 148 149 # Set the output _state variable. 150 # (Passing by reference in CMake is awkward...) 151 set(${_state} ${state} PARENT_SCOPE) 152 153 if(EXISTS "${POST_CONFIGURE_GIT_VERSION_FILE}") 154 if("${PRE_CONFIGURE_GIT_VERSION_FILE}" IS_NEWER_THAN "${POST_CONFIGURE_GIT_VERSION_FILE}") 155 file(REMOVE "${POST_CONFIGURE_GIT_VERSION_FILE}") 156 file(REMOVE "${GIT_STATE_FILE}") 157 set(${_state_changed} "true" PARENT_SCOPE) 158 return() 159 endif() 160 else() 161 file(REMOVE "${GIT_STATE_FILE}") 162 set(${_state_changed} "true" PARENT_SCOPE) 163 return() 164 endif() 165 166 # Check if the state has changed compared to the backup on disk. 167 if(EXISTS "${GIT_STATE_FILE}") 168 file(READ "${GIT_STATE_FILE}" OLD_HEAD_CONTENTS) 169 if(OLD_HEAD_CONTENTS STREQUAL "${state}") 170 # State didn't change. 171 set(${_state_changed} "false" PARENT_SCOPE) 172 return() 173 endif() 174 endif() 175 176 # The state has changed. 177 # We need to update the state file on disk. 178 # Future builds will compare their state to this file. 179 file(WRITE "${GIT_STATE_FILE}" "${state}") 180 set(${_state_changed} "true" PARENT_SCOPE) 181endfunction() 182 183 184 185# Function: SetupGitMonitoring 186# Description: this function sets up custom commands that make the build system 187# check the state of git before every build. If the state has 188# changed, then a file is configured. 189function(SetupGitMonitoring) 190 add_custom_target(check_git_repository 191 ALL 192 DEPENDS ${PRE_CONFIGURE_GIT_VERSION_FILE} 193 BYPRODUCTS ${POST_CONFIGURE_GIT_VERSION_FILE} 194 BYPRODUCTS ${GIT_STATE_FILE} 195 COMMENT "Checking the git repository for changes..." 196 COMMAND 197 ${CMAKE_COMMAND} 198 -D_BUILD_TIME_CHECK_GIT=TRUE 199 -DGIT_WORKING_DIR=${GIT_WORKING_DIR} 200 -DGIT_EXECUTABLE=${GIT_EXECUTABLE} 201 -DGIT_STATE_FILE=${GIT_STATE_FILE} 202 -DPRE_CONFIGURE_GIT_VERSION_FILE=${PRE_CONFIGURE_GIT_VERSION_FILE} 203 -DPOST_CONFIGURE_GIT_VERSION_FILE=${POST_CONFIGURE_GIT_VERSION_FILE} 204 -P "${CMAKE_CURRENT_LIST_FILE}") 205endfunction() 206 207 208 209# Function: Main 210# Description: primary entry-point to the script. Functions are selected based 211# on whether it's configure or build time. 212function(Main) 213 if(_BUILD_TIME_CHECK_GIT) 214 # Check if the repo has changed. 215 # If so, run the change action. 216 CheckGit("${GIT_WORKING_DIR}" did_change state) 217 if(did_change) 218 GitStateChangedAction("${state}") 219 endif() 220 else() 221 # >> Executes at configure time. 222 SetupGitMonitoring() 223 endif() 224endfunction() 225 226# And off we go... 227Main() 228