-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmb-pre-commit.cmake
More file actions
379 lines (345 loc) · 13.4 KB
/
Copy pathmb-pre-commit.cmake
File metadata and controls
379 lines (345 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
include_guard(GLOBAL)
# Directory of this list file (cmake/). For CMake < 3.17, mb_pre_commit_setup() falls back to this.
# Do not derive from CMAKE_CURRENT_LIST_FILE: in some FetchContent layouts its DIRECTORY is empty,
# which breaks paths like ${_dir}/pre-commit.in -> /pre-commit.in.
set(_MB_PRE_COMMIT_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}")
# Force TOP_LEVEL vs SUBDIRECTORY setup in tests/CI; default AUTO uses CMAKE_SOURCE_DIR vs
# CMAKE_CURRENT_SOURCE_DIR (see mb_pre_commit_setup_project).
set(MB_PRE_COMMIT_SETUP_LAYOUT
AUTO
CACHE STRING
"mb-pre-commit layout: AUTO, TOP_LEVEL, or SUBDIRECTORY"
)
set_property(
CACHE MB_PRE_COMMIT_SETUP_LAYOUT
PROPERTY STRINGS AUTO TOP_LEVEL SUBDIRECTORY
)
function(mb_pre_commit_setup)
set(options)
set(oneValueArgs
PROJECT_SOURCE_DIR
PROJECT_BINARY_DIR
PRE_COMMIT_MODE
PRE_COMMIT_VERSION
PRE_COMMIT_VENV_DIR
PRE_COMMIT_INSTALL_EXAMPLE_CONFIG
PRE_COMMIT_SWEEP_TARGET
PRE_COMMIT_TOOL_SWEEP_TARGET
)
cmake_parse_arguments(PC "${options}" "${oneValueArgs}" "" ${ARGN})
if(NOT PC_PROJECT_SOURCE_DIR)
set(PC_PROJECT_SOURCE_DIR "${CMAKE_SOURCE_DIR}")
endif()
if(NOT PC_PROJECT_BINARY_DIR)
set(PC_PROJECT_BINARY_DIR "${CMAKE_BINARY_DIR}")
endif()
if(NOT PC_PRE_COMMIT_MODE)
set(PC_PRE_COMMIT_MODE "CUSTOM")
endif()
if(NOT PC_PRE_COMMIT_VERSION)
set(PC_PRE_COMMIT_VERSION "4.5.1")
endif()
if(NOT PC_PRE_COMMIT_VENV_DIR)
set(PC_PRE_COMMIT_VENV_DIR "${PC_PROJECT_SOURCE_DIR}/.venv")
endif()
if(NOT DEFINED PC_PRE_COMMIT_INSTALL_EXAMPLE_CONFIG)
set(PC_PRE_COMMIT_INSTALL_EXAMPLE_CONFIG ON)
endif()
if(NOT DEFINED PC_PRE_COMMIT_SWEEP_TARGET)
set(PC_PRE_COMMIT_SWEEP_TARGET "mb-pre-commit-sweep")
endif()
if(NOT IS_ABSOLUTE "${PC_PROJECT_SOURCE_DIR}")
get_filename_component(
PC_PROJECT_SOURCE_DIR
"${PC_PROJECT_SOURCE_DIR}"
ABSOLUTE
BASE_DIR "${CMAKE_SOURCE_DIR}"
)
endif()
if(NOT IS_ABSOLUTE "${PC_PROJECT_BINARY_DIR}")
get_filename_component(
PC_PROJECT_BINARY_DIR
"${PC_PROJECT_BINARY_DIR}"
ABSOLUTE
BASE_DIR "${CMAKE_BINARY_DIR}"
)
endif()
if(NOT IS_ABSOLUTE "${PC_PRE_COMMIT_VENV_DIR}")
get_filename_component(
PC_PRE_COMMIT_VENV_DIR
"${PC_PRE_COMMIT_VENV_DIR}"
ABSOLUTE
BASE_DIR "${PC_PROJECT_SOURCE_DIR}"
)
endif()
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# Inside function(), CMAKE_CURRENT_LIST_DIR is the caller's file — use the directory of
# this module's list file (CMake 3.17+), else file-scope _MB_PRE_COMMIT_CMAKE_DIR.
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.17")
set(_tool_module_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}")
else()
set(_tool_module_dir "${_MB_PRE_COMMIT_CMAKE_DIR}")
endif()
set(_configs_root "${_tool_module_dir}/../configs")
set(_hook_template "${_tool_module_dir}/pre-commit.in")
get_filename_component(
_setup_script
"${_tool_module_dir}/../python/mb-pre-commit-setup.py"
ABSOLUTE
)
get_filename_component(
_clang_format_wrapper_script
"${_tool_module_dir}/../python/mb-pre-commit-clang-format.py"
ABSOLUTE
)
if(NOT EXISTS "${_setup_script}")
message(
FATAL_ERROR
"mb_pre_commit_setup: setup script not found: ${_setup_script}"
)
endif()
if(NOT EXISTS "${_clang_format_wrapper_script}")
message(
FATAL_ERROR
"mb_pre_commit_setup: clang-format wrapper not found: ${_clang_format_wrapper_script}"
)
endif()
# Re-run configure if the hook template or shipped example configs change.
set_property(
DIRECTORY
APPEND
PROPERTY
CMAKE_CONFIGURE_DEPENDS
"${_hook_template}"
"${_setup_script}"
"${_clang_format_wrapper_script}"
)
# CONFIGURE_DEPENDS registers these files with the build tool so CMake re-runs when
# they change. Do not also append them to CMAKE_CONFIGURE_DEPENDS — that duplicates
# the same outputs in Ninja ("defined as an output multiple times").
# The GLOB list is unused on purpose; only CONFIGURE_DEPENDS side effects matter.
file(
GLOB_RECURSE _example_config_deps_unused
CONFIGURE_DEPENDS
"${_configs_root}/v*/.pre-commit-config.yaml"
"${_configs_root}/v*/.markdownlint.yaml"
)
set(_setup_args
"${Python3_EXECUTABLE}"
"${_setup_script}"
--project-source-dir
"${PC_PROJECT_SOURCE_DIR}"
--project-binary-dir
"${PC_PROJECT_BINARY_DIR}"
--mode
"${PC_PRE_COMMIT_MODE}"
--version
"${PC_PRE_COMMIT_VERSION}"
--venv-dir
"${PC_PRE_COMMIT_VENV_DIR}"
--tool-root
"${_tool_module_dir}"
--python
"${Python3_EXECUTABLE}"
)
if(NOT PC_PRE_COMMIT_INSTALL_EXAMPLE_CONFIG)
list(APPEND _setup_args --no-install-example-config)
endif()
execute_process(COMMAND ${_setup_args} RESULT_VARIABLE _mb_pc_setup_result)
if(NOT _mb_pc_setup_result EQUAL 0)
message(
FATAL_ERROR
"mb_pre_commit_setup: mb-pre-commit-setup.py failed (exit ${_mb_pc_setup_result})"
)
endif()
if(WIN32)
set(_venv_bin_dir "${PC_PRE_COMMIT_VENV_DIR}/Scripts")
set(_venv_python "${PC_PRE_COMMIT_VENV_DIR}/Scripts/python.exe")
set(_clang_format_launcher
"${_venv_bin_dir}/mb-pre-commit-clang-format.cmd"
)
else()
set(_venv_bin_dir "${PC_PRE_COMMIT_VENV_DIR}/bin")
set(_venv_python "${PC_PRE_COMMIT_VENV_DIR}/bin/python3")
set(_clang_format_launcher
"${_venv_bin_dir}/mb-pre-commit-clang-format"
)
endif()
message(
STATUS
"pre-commit clang-format: ${_clang_format_launcher} (installed by mb-pre-commit-setup.py; wraps newest clang-format cached by pre-commit)"
)
# One-word build target: pre-commit on the whole tree (not just staged files).
if(NOT PC_PRE_COMMIT_SWEEP_TARGET STREQUAL "OFF")
set(_sweep_target "${PC_PRE_COMMIT_SWEEP_TARGET}")
if(TARGET "${_sweep_target}")
if(_sweep_target STREQUAL "mb-pre-commit-sweep")
set(_sweep_target mb_pre_commit_sweep)
message(
STATUS
"mb_pre_commit_setup: target 'mb-pre-commit-sweep' already exists; using 'mb_pre_commit_sweep' (pre-commit run --all-files)"
)
endif()
endif()
if(TARGET "${_sweep_target}")
message(
FATAL_ERROR
"mb_pre_commit_setup: PRE_COMMIT_SWEEP_TARGET name '${_sweep_target}' is already a target"
)
endif()
add_custom_target(
"${_sweep_target}"
COMMAND "${_venv_python}" -m pre_commit run --all-files
WORKING_DIRECTORY "${PC_PROJECT_SOURCE_DIR}"
COMMENT "pre-commit: all files"
USES_TERMINAL
)
message(
STATUS
"pre-commit sweep: cmake --build <dir> --target ${_sweep_target} (pre-commit run --all-files)"
)
endif()
# Optional second sweep: mb-pre-commit checkout (e.g. submodule) when PROJECT_SOURCE_DIR is
# the outer project. Only registered when PRE_COMMIT_TOOL_SWEEP_TARGET is set (use OFF to
# explicitly omit; omit the parameter entirely to skip adding this target).
# When PROJECT_SOURCE_DIR is already this checkout, skip if the main sweep uses the same cwd
# (avoids two identical targets). If PRE_COMMIT_SWEEP_TARGET is OFF, the tool sweep still
# registers so you can get a single full-tree run from this tree.
if(
PC_PRE_COMMIT_TOOL_SWEEP_TARGET
AND NOT PC_PRE_COMMIT_TOOL_SWEEP_TARGET STREQUAL "OFF"
)
get_filename_component(
_mb_pc_tool_repo_root
"${_tool_module_dir}/.."
ABSOLUTE
)
set(_mb_pc_skip_tool_sweep FALSE)
if(
NOT PC_PRE_COMMIT_SWEEP_TARGET STREQUAL "OFF"
AND "${_mb_pc_tool_repo_root}" STREQUAL "${PC_PROJECT_SOURCE_DIR}"
)
set(_mb_pc_skip_tool_sweep TRUE)
message(
STATUS
"mb_pre_commit_setup: PRE_COMMIT_TOOL_SWEEP_TARGET ignored (same tree as PROJECT_SOURCE_DIR; main sweep already covers it)"
)
endif()
if(NOT _mb_pc_skip_tool_sweep)
set(_tool_sweep_target "${PC_PRE_COMMIT_TOOL_SWEEP_TARGET}")
if(TARGET "${_tool_sweep_target}")
message(
FATAL_ERROR
"mb_pre_commit_setup: PRE_COMMIT_TOOL_SWEEP_TARGET name '${_tool_sweep_target}' is already a target"
)
endif()
add_custom_target(
"${_tool_sweep_target}"
COMMAND "${_venv_python}" -m pre_commit run --all-files
WORKING_DIRECTORY "${_mb_pc_tool_repo_root}"
COMMENT "pre-commit: all files (mb-pre-commit tool checkout)"
USES_TERMINAL
)
message(
STATUS
"pre-commit sweep (tool repo): cmake --build <dir> --target ${_tool_sweep_target} (pre-commit run --all-files in ${_mb_pc_tool_repo_root})"
)
endif()
endif()
endfunction()
# Install pre-commit for the CMake project that calls this (e.g. devenv as add_subdirectory).
# Uses CMAKE_CURRENT_SOURCE_DIR/BINARY_DIR instead of the top-level CMAKE_SOURCE_DIR so Git
# hooks and .venv land in the submodule tree. Commits inside that submodule then run this
# hook; the parent's mb_pre_commit_setup() does not apply there.
function(_mb_pre_commit_setup_subdirectory_impl)
set(options)
set(oneValueArgs
CALLER_LIST_DIR
CALLER_SOURCE_DIR
CALLER_BINARY_DIR
PRE_COMMIT_SWEEP_TARGET
PRE_COMMIT_INSTALL_EXAMPLE_CONFIG
)
cmake_parse_arguments(SUBPC "${options}" "${oneValueArgs}" "" ${ARGN})
if(
NOT SUBPC_CALLER_LIST_DIR
OR NOT SUBPC_CALLER_SOURCE_DIR
OR NOT SUBPC_CALLER_BINARY_DIR
)
message(
FATAL_ERROR
"_mb_pre_commit_setup_subdirectory_impl: internal caller paths missing"
)
endif()
if(NOT SUBPC_PRE_COMMIT_SWEEP_TARGET)
# Do not use PROJECT_NAME or CMAKE_PROJECT_NAME: in a function they may not reflect
# the caller's directory scope (PROJECT_NAME inherits the top-level project() name).
get_filename_component(_subpc_sweep_id "${SUBPC_CALLER_LIST_DIR}" NAME)
set(SUBPC_PRE_COMMIT_SWEEP_TARGET
"mb-pre-commit-sweep-${_subpc_sweep_id}"
)
endif()
set(_sub_setup_args
PROJECT_SOURCE_DIR
"${SUBPC_CALLER_SOURCE_DIR}"
PROJECT_BINARY_DIR
"${SUBPC_CALLER_BINARY_DIR}"
PRE_COMMIT_SWEEP_TARGET
"${SUBPC_PRE_COMMIT_SWEEP_TARGET}"
)
list(APPEND _sub_setup_args ${SUBPC_UNPARSED_ARGUMENTS})
# Submodule trees usually ship their own .pre-commit-config.yaml; do not overwrite unless
# the caller passes PRE_COMMIT_INSTALL_EXAMPLE_CONFIG explicitly.
if(NOT DEFINED SUBPC_PRE_COMMIT_INSTALL_EXAMPLE_CONFIG)
list(APPEND _sub_setup_args PRE_COMMIT_INSTALL_EXAMPLE_CONFIG OFF)
else()
list(
APPEND _sub_setup_args
PRE_COMMIT_INSTALL_EXAMPLE_CONFIG
"${SUBPC_PRE_COMMIT_INSTALL_EXAMPLE_CONFIG}"
)
endif()
mb_pre_commit_setup(${_sub_setup_args})
endfunction()
# Macro so CMAKE_CURRENT_* are read at the call site (the submodule CMakeLists.txt), not
# inside a function scope where PROJECT_NAME can still be the top-level project() name.
macro(mb_pre_commit_setup_subdirectory)
_mb_pre_commit_setup_subdirectory_impl(
CALLER_LIST_DIR
"${CMAKE_CURRENT_LIST_DIR}"
CALLER_SOURCE_DIR
"${CMAKE_CURRENT_SOURCE_DIR}"
CALLER_BINARY_DIR
"${CMAKE_CURRENT_BINARY_DIR}"
${ARGN}
)
endmacro()
# Pick mb_pre_commit_setup() vs mb_pre_commit_setup_subdirectory() for the calling tree.
# Uses CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR (not PROJECT_IS_TOP_LEVEL): in an
# add_subdirectory() tree without its own project(), PROJECT_IS_TOP_LEVEL stays ON even though
# hooks and .venv must be installed in the submodule directory.
#
# Optional cache MB_PRE_COMMIT_SETUP_LAYOUT: AUTO (default), TOP_LEVEL, or SUBDIRECTORY — useful
# in tests/CI to force a layout without restructuring the CMake tree.
macro(mb_pre_commit_setup_project)
if(MB_PRE_COMMIT_SETUP_LAYOUT STREQUAL "TOP_LEVEL")
mb_pre_commit_setup(${ARGN})
elseif(MB_PRE_COMMIT_SETUP_LAYOUT STREQUAL "SUBDIRECTORY")
mb_pre_commit_setup_subdirectory(${ARGN})
elseif(
MB_PRE_COMMIT_SETUP_LAYOUT STREQUAL ""
OR MB_PRE_COMMIT_SETUP_LAYOUT STREQUAL "AUTO"
)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
mb_pre_commit_setup(${ARGN})
else()
mb_pre_commit_setup_subdirectory(${ARGN})
endif()
else()
message(
FATAL_ERROR
"mb_pre_commit_setup_project: MB_PRE_COMMIT_SETUP_LAYOUT='${MB_PRE_COMMIT_SETUP_LAYOUT}' "
"must be AUTO, TOP_LEVEL, or SUBDIRECTORY"
)
endif()
endmacro()