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