1+ # git_watcher.cmake
2+ # https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git_watcher.cmake
3+ #
4+ # Released under the MIT License.
5+ # https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE
6+
7+
8+ # This file defines a target that monitors the state of a git repo.
9+ # If the state changes (e.g. a commit is made), then a file gets reconfigured.
10+ # Here are the primary variables that control script behavior:
11+ #
12+ # PRE_CONFIGURE_FILE (REQUIRED)
13+ # -- The path to the file that'll be configured.
14+ #
15+ # POST_CONFIGURE_FILE (REQUIRED)
16+ # -- The path to the configured PRE_CONFIGURE_FILE.
17+ #
18+ # GIT_STATE_FILE (OPTIONAL)
19+ # -- The path to the file used to store the previous build's git state.
20+ # Defaults to the current binary directory.
21+ #
22+ # GIT_WORKING_DIR (OPTIONAL)
23+ # -- The directory from which git commands will be run.
24+ # Defaults to the directory with the top level CMakeLists.txt.
25+ #
26+ # GIT_EXECUTABLE (OPTIONAL)
27+ # -- The path to the git executable. It'll automatically be set if the
28+ # user doesn't supply a path.
29+ #
30+ # DESIGN
31+ # - This script was designed similar to a Python application
32+ # with a Main() function. I wanted to keep it compact to
33+ # simplify "copy + paste" usage.
34+ #
35+ # - This script is invoked under two CMake contexts:
36+ # 1. Configure time (when build files are created).
37+ # 2. Build time (called via CMake -P).
38+ # The first invocation is what registers the script to
39+ # be executed at build time.
40+ #
41+ # MODIFICATIONS
42+ # You may wish to track other git properties like when the last
43+ # commit was made. There are two sections you need to modify,
44+ # and they're tagged with a ">>>" header.
45+
46+ # Short hand for converting paths to absolute.
47+ macro (PATH_TO_ABSOLUTE var_name)
48+ get_filename_component (${var_name} "${${var_name} }" ABSOLUTE )
49+ endmacro ()
50+
51+ # Check that a required variable is set.
52+ macro (CHECK_REQUIRED_VARIABLE var_name)
53+ if (NOT DEFINED ${var_name} )
54+ message (FATAL_ERROR "The \" ${var_name} \" variable must be defined." )
55+ endif ()
56+ PATH_TO_ABSOLUTE(${var_name} )
57+ endmacro ()
58+
59+ # Check that an optional variable is set, or, set it to a default value.
60+ macro (CHECK_OPTIONAL_VARIABLE var_name default_value)
61+ if (NOT DEFINED ${var_name} )
62+ set (${var_name} ${default_value} )
63+ endif ()
64+ PATH_TO_ABSOLUTE(${var_name} )
65+ endmacro ()
66+
67+ CHECK_REQUIRED_VARIABLE(PRE_CONFIGURE_FILE)
68+ CHECK_REQUIRED_VARIABLE(POST_CONFIGURE_FILE)
69+ CHECK_OPTIONAL_VARIABLE(GIT_STATE_FILE "${CMAKE_BINARY_DIR} /git-state-hash" )
70+ CHECK_OPTIONAL_VARIABLE(GIT_WORKING_DIR "${CMAKE_SOURCE_DIR} " )
71+
72+ # Check the optional git variable.
73+ # If it's not set, we'll try to find it using the CMake packaging system.
74+ if (NOT DEFINED GIT_EXECUTABLE)
75+ find_package (Git QUIET REQUIRED)
76+ endif ()
77+ CHECK_REQUIRED_VARIABLE(GIT_EXECUTABLE)
78+
79+
80+ set (_state_variable_names
81+ GIT_RETRIEVED_STATE
82+ GIT_HEAD_SHA1
83+ GIT_IS_DIRTY
84+ GIT_AUTHOR_NAME
85+ GIT_AUTHOR_EMAIL
86+ GIT_COMMIT_DATE_ISO8601
87+ GIT_COMMIT_SUBJECT
88+ GIT_COMMIT_BODY
89+ GIT_DESCRIBE
90+ # >>>
91+ # 1. Add the name of the additional git variable you're interested in monitoring
92+ # to this list.
93+ )
94+
95+
96+
97+ # Macro: RunGitCommand
98+ # Description: short-hand macro for calling a git function. Outputs are the
99+ # "exit_code" and "output" variables.
100+ macro (RunGitCommand)
101+ execute_process (COMMAND
102+ "${GIT_EXECUTABLE} " ${ARGV}
103+ WORKING_DIRECTORY "${_working_dir} "
104+ RESULT_VARIABLE exit_code
105+ OUTPUT_VARIABLE output
106+ ERROR_QUIET
107+ OUTPUT_STRIP_TRAILING_WHITESPACE)
108+ if (NOT exit_code EQUAL 0)
109+ set (ENV{GIT_RETRIEVED_STATE} "false" )
110+ endif ()
111+ endmacro ()
112+
113+
114+
115+ # Function: GetGitState
116+ # Description: gets the current state of the git repo.
117+ # Args:
118+ # _working_dir (in) string; the directory from which git commands will be executed.
119+ function (GetGitState _working_dir)
120+
121+ # This is an error code that'll be set to FALSE if the
122+ # RunGitCommand ever returns a non-zero exit code.
123+ set (ENV{GIT_RETRIEVED_STATE} "true" )
124+
125+ # Get whether or not the working tree is dirty.
126+ RunGitCommand(status --porcelain)
127+ if (NOT exit_code EQUAL 0)
128+ set (ENV{GIT_IS_DIRTY} "false" )
129+ else ()
130+ if (NOT "${output} " STREQUAL "" )
131+ set (ENV{GIT_IS_DIRTY} "true" )
132+ else ()
133+ set (ENV{GIT_IS_DIRTY} "false" )
134+ endif ()
135+ endif ()
136+
137+ # There's a long list of attributes grabbed from git show.
138+ set (object HEAD)
139+ RunGitCommand(show -s "--format=%H" ${object} )
140+ if (exit_code EQUAL 0)
141+ set (ENV{GIT_HEAD_SHA1} ${output} )
142+ endif ()
143+
144+ RunGitCommand(show -s "--format=%an" ${object} )
145+ if (exit_code EQUAL 0)
146+ set (ENV{GIT_AUTHOR_NAME} "${output} " )
147+ endif ()
148+
149+ RunGitCommand(show -s "--format=%ae" ${object} )
150+ if (exit_code EQUAL 0)
151+ set (ENV{GIT_AUTHOR_EMAIL} "${output} " )
152+ endif ()
153+
154+ RunGitCommand(show -s "--format=%ci" ${object} )
155+ if (exit_code EQUAL 0)
156+ set (ENV{GIT_COMMIT_DATE_ISO8601} "${output} " )
157+ endif ()
158+
159+ RunGitCommand(show -s "--format=%s" ${object} )
160+ if (exit_code EQUAL 0)
161+ # Escape quotes
162+ string (REPLACE "\" " "\\\" " output "${output} " )
163+ set (ENV{GIT_COMMIT_SUBJECT} "${output} " )
164+ endif ()
165+
166+ RunGitCommand(show -s "--format=%b" ${object} )
167+ if (exit_code EQUAL 0)
168+ if (output )
169+ # Escape quotes
170+ string (REPLACE "\" " "\\\" " output "${output} " )
171+ # Escape line breaks in the commit message.
172+ string (REPLACE "\r\n " "\\ r\\ n\\\r\n " safe "${output} " )
173+ if (safe STREQUAL output )
174+ # Didn't have windows lines - try unix lines.
175+ string (REPLACE "\n " "\\ n\\\n " safe "${output} " )
176+ endif ()
177+ else ()
178+ # There was no commit body - set the safe string to empty.
179+ set (safe "" )
180+ endif ()
181+ set (ENV{GIT_COMMIT_BODY} "\" ${safe} \" " )
182+ else ()
183+ set (ENV{GIT_COMMIT_BODY} "\"\" " ) # empty string.
184+ endif ()
185+
186+ # Get output of git describe
187+ RunGitCommand(describe --always ${object} )
188+ if (NOT exit_code EQUAL 0)
189+ set (ENV{GIT_DESCRIBE} "unknown" )
190+ else ()
191+ set (ENV{GIT_DESCRIBE} "${output} " )
192+ endif ()
193+
194+ # >>>
195+ # 2. Additional git properties can be added here via the
196+ # "execute_process()" command. Be sure to set them in
197+ # the environment using the same variable name you added
198+ # to the "_state_variable_names" list.
199+
200+ endfunction ()
201+
202+
203+
204+ # Function: GitStateChangedAction
205+ # Description: this function is executed when the state of the git
206+ # repository changes (e.g. a commit is made).
207+ function (GitStateChangedAction)
208+ foreach (var_name ${_state_variable_names} )
209+ set (${var_name} $ENV{${var_name} })
210+ endforeach ()
211+ configure_file ("${PRE_CONFIGURE_FILE} " "${POST_CONFIGURE_FILE} " @ONLY)
212+ endfunction ()
213+
214+
215+
216+ # Function: HashGitState
217+ # Description: loop through the git state variables and compute a unique hash.
218+ # Args:
219+ # _state (out) string; a hash computed from the current git state.
220+ function (HashGitState _state)
221+ set (ans "" )
222+ foreach (var_name ${_state_variable_names} )
223+ string (SHA256 ans "${ans} $ENV{${var_name} }" )
224+ endforeach ()
225+ set (${_state} ${ans} PARENT_SCOPE)
226+ endfunction ()
227+
228+
229+
230+ # Function: CheckGit
231+ # Description: check if the git repo has changed. If so, update the state file.
232+ # Args:
233+ # _working_dir (in) string; the directory from which git commands will be ran.
234+ # _state_changed (out) bool; whether or no the state of the repo has changed.
235+ function (CheckGit _working_dir _state_changed)
236+
237+ # Get the current state of the repo.
238+ GetGitState("${_working_dir} " )
239+
240+ # Convert that state into a hash that we can compare against
241+ # the hash stored on-disk.
242+ HashGitState(state)
243+
244+ # Issue 14: post-configure file isn't being regenerated.
245+ #
246+ # Update the state to include the SHA256 for the pre-configure file.
247+ # This forces the post-configure file to be regenerated if the
248+ # pre-configure file has changed.
249+ file (SHA256 ${PRE_CONFIGURE_FILE} preconfig_hash)
250+ string (SHA256 state "${preconfig_hash}${state} " )
251+
252+ # Check if the state has changed compared to the backup on disk.
253+ if (EXISTS "${GIT_STATE_FILE} " )
254+ file (READ "${GIT_STATE_FILE} " OLD_HEAD_CONTENTS)
255+ if (OLD_HEAD_CONTENTS STREQUAL "${state} " )
256+ # State didn't change.
257+ set (${_state_changed} "false" PARENT_SCOPE)
258+ return ()
259+ endif ()
260+ endif ()
261+
262+ # The state has changed.
263+ # We need to update the state file on disk.
264+ # Future builds will compare their state to this file.
265+ file (WRITE "${GIT_STATE_FILE} " "${state} " )
266+ set (${_state_changed} "true" PARENT_SCOPE)
267+ endfunction ()
268+
269+
270+
271+ # Function: SetupGitMonitoring
272+ # Description: this function sets up custom commands that make the build system
273+ # check the state of git before every build. If the state has
274+ # changed, then a file is configured.
275+ function (SetupGitMonitoring)
276+ add_custom_target (check_git
277+ ALL
278+ DEPENDS ${PRE_CONFIGURE_FILE}
279+ BYPRODUCTS
280+ ${POST_CONFIGURE_FILE}
281+ ${GIT_STATE_FILE}
282+ COMMENT "Checking the git repository for changes..."
283+ COMMAND
284+ ${CMAKE_COMMAND}
285+ -D_BUILD_TIME_CHECK_GIT=TRUE
286+ -DGIT_WORKING_DIR=${GIT_WORKING_DIR}
287+ -DGIT_EXECUTABLE=${GIT_EXECUTABLE}
288+ -DGIT_STATE_FILE=${GIT_STATE_FILE}
289+ -DPRE_CONFIGURE_FILE=${PRE_CONFIGURE_FILE}
290+ -DPOST_CONFIGURE_FILE=${POST_CONFIGURE_FILE}
291+ -P "${CMAKE_CURRENT_LIST_FILE} " )
292+ endfunction ()
293+
294+
295+
296+ # Function: Main
297+ # Description: primary entry-point to the script. Functions are selected based
298+ # on whether it's configure or build time.
299+ function (Main)
300+ if (_BUILD_TIME_CHECK_GIT)
301+ # Check if the repo has changed.
302+ # If so, run the change action.
303+ CheckGit("${GIT_WORKING_DIR} " changed)
304+ if (changed OR NOT EXISTS "${POST_CONFIGURE_FILE} " )
305+ GitStateChangedAction()
306+ endif ()
307+ else ()
308+ # >> Executes at configure time.
309+ SetupGitMonitoring()
310+ endif ()
311+ endfunction ()
312+
313+ # And off we go...
314+ Main()
0 commit comments