-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCMakeLists.txt
552 lines (494 loc) · 17 KB
/
CMakeLists.txt
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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
cmake_minimum_required(VERSION 3.25)
#[[
TODO:
- [ ] create base CI images for all supported platforms and compilers (w/ gha-tool)
- [x] separate out gcc, clang and msvc support out to separate files
- [x] set up coverage report and numbers for llvm-cov and gcov
- [x] set up test report target
- [x] convert all flag functions to use the `get_flag_value` helper
- [ ] add proper support for other linkers since cmake support absolutely sucks
- [ ] add support for linking against musl without musl-clang/gcc wrappers
- [ ] break apart test support into separate files
- [x] break apart musl and other libc support into separate directories
- [x] support for static glibc with tcc
- [x] support for musl libc
- [x] clang static
- [x] gcc static
- [x] tcc static
- [x] clang dynamic
- [x] gcc dynamic
- [x] tcc dynamic
- [ ] add flag functions specific to building shared libs / static archives
- remove static/non-static flag functions?
- add function specific to creating libraries / executables, ie.
project_name_add_library / project_name_add_executable, similar to
add_test_project
- [ ] break apart test coverage flags into separate flag function
- [ ] set up benchmark targets
- [ ] set up automatic target signing
- [ ] for libraries:
- guard symbol auto-generation
- library versioning
- [ ] clang-format and clang-tidy targets
- [ ] support for control flow and stack protections:
- https://developers.redhat.com/articles/2022/06/02/use-compiler-flags-stack-protection-gcc-and-clang
- https://blog.quarkslab.com/clang-hardening-cheat-sheet.html
- [ ] support shipping separated debug symbols: https://www.tweag.io/blog/2023-11-23-debug-fission/
- [ ] SAST targets
- [ ] clang static analyzer target: https://www.youtube.com/watch?v=nTslG8HtKeA
- do both CTU and non-CTU since there may be non-overlapping cases
- do not use scan-build as it is 3rd party and does not support multi-TU analysis
- look into Z3 analyzer constraint (crosscheck-with-z3=true)
- [ ] nasa static analyzer target
- [ ] packaging:
- AUR (src / pkg)
- deb
- rpm
- vcpkg
- conan
- build2
- choco (windows)
- snap
- [x] break apart cmake build system to separate repo (cakemake?)
- [ ] start work on c++
]]
string(TOUPPER "${PROJECT_NAME}" VARIABLE_PREFIX)
string(REPLACE "-" "_" VARIABLE_PREFIX "${VARIABLE_PREFIX}")
string(TOLOWER "${PROJECT_NAME}_" FUNCTION_PREFIX)
string(REPLACE "-" "_" FUNCTION_PREFIX "${FUNCTION_PREFIX}")
set(TARGET_PREFIX "${PROJECT_NAME}-")
#[[always enable testing]]
enable_testing()
#[[disallow in-source builds]]
if("${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}")
message(FATAL_ERROR "In-source builds are not permitted")
endif()
#[[ensure compiler is supported]]
#TODO: msvc, ClangCL, Intel icc, Intel dpc, IBM XL C, qcc, pcc, ack, cyg-win
list(
APPEND "${VARIABLE_PREFIX}_SUPPORTED_COMPILER_IDS"
Clang
AppleClang
GNU
TinyCC
)
#set compiler shorthand identifier so we can easily check later
if(
CMAKE_C_COMPILER_ID STREQUAL "Clang"
OR CMAKE_C_COMPILER_ID STREQUAL "AppleClang"
)
set("${VARIABLE_PREFIX}_COMPILER_ID" "Clang")
elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU")
set("${VARIABLE_PREFIX}_COMPILER_ID" "GNU")
elseif(CMAKE_C_COMPILER_ID STREQUAL "TinyCC")
set("${VARIABLE_PREFIX}_COMPILER_ID" "TinyCC")
else()
message(
FATAL_ERROR
"Unsupported compiler: '${CMAKE_C_COMPILER_ID}'\nSupported compilers "
"include: '${${VARIABLE_PREFIX}_SUPPORTED_COMPILER_IDS}'"
)
endif()
#ensure we updated the list of formally supported compilers
list(
FIND
"${VARIABLE_PREFIX}_SUPPORTED_COMPILER_IDS"
"${CMAKE_C_COMPILER_ID}"
found
)
if(found STREQUAL -1)
message(
FATAL_ERROR
"Forgot to add support for new compiler '${CMAKE_C_COMPILER_ID}' to list of "
"supported compilers!"
)
endif()
unset(found)
message(
STATUS
"${PROJECT_NAME}: Compiling with '${${VARIABLE_PREFIX}_COMPILER_ID}'"
)
#[[
TODO: create project-specific top-level targets
- these are platform-specific, move to platform-specific directories
]]
#create global top-level targets
include("${CMAKE_CURRENT_LIST_DIR}/targets.cmake")
#[[
top-level clean target
custom user-specified clean targets can add themselves to this target as
dependencies and when this target is built, all user-specified clean targets
will be built and the auto-generated 'clean' target will be invoked
]]
add_custom_target(
"${TARGET_PREFIX}clean-all"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
COMMAND exit
VERBATIM
)
add_dependencies(clean-all "${TARGET_PREFIX}clean-all")
#[[
top-level target to build all tests
tests that are declared as part of the project are added as dependencies of
this target, such that we can build this target to build all tests
NOTE: performs a no-op command by default, in case no user-specified coverage
targets are added as dependencies of this target
]]
add_custom_target(
"${TARGET_PREFIX}build-test-all"
COMMAND exit
VERBATIM
)
add_dependencies(build-test-all "${TARGET_PREFIX}build-test-all")
#[[
top-level target to build and run all tests
NOTE: profile and coverage data is not cleaned between runs
]]
add_custom_target(
"${TARGET_PREFIX}test-all"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
COMMAND ${CMAKE_COMMAND} --build "${PROJECT_BINARY_DIR}" --target "${TARGET_PREFIX}build-test-all"
COMMAND ctest
VERBATIM
)
add_dependencies(test-all "${TARGET_PREFIX}test-all")
#[[
top-level test coverage target
NOTE: performs a no-op command by default, in case no user-specified coverage
targets are added as dependencies of this target
]]
add_custom_target(
"${TARGET_PREFIX}coverage"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
COMMAND exit
VERBATIM
)
add_dependencies(coverage "${TARGET_PREFIX}coverage")
#[[
top-level test coverage clean up target
NOTE: performs a no-op command by default, in case no user-specified coverage
targets are added as dependencies of this target
]]
add_custom_target(
"${TARGET_PREFIX}clean-coverage"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
COMMAND exit
VERBATIM
)
add_dependencies("${TARGET_PREFIX}clean-all" "${TARGET_PREFIX}clean-coverage")
add_dependencies(clean-coverage "${TARGET_PREFIX}clean-coverage")
#TODO: top-level benchmark target
#[[
top-level CI target
runs:
1. `clean-coverage`: cleans all prior test profile data and test reports
2. `test-all`: runs all tests
3. `coverage`: generates test coverage reports using test profile data
4. `benchmark`: TODO
]]
add_custom_target(
"${TARGET_PREFIX}ci"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
COMMAND ${CMAKE_COMMAND} --build "${PROJECT_BINARY_DIR}" --target "${TARGET_PREFIX}clean-coverage"
COMMAND ${CMAKE_COMMAND} --build "${PROJECT_BINARY_DIR}" --target "${TARGET_PREFIX}test-all"
COMMAND ${CMAKE_COMMAND} --build "${PROJECT_BINARY_DIR}" --target "${TARGET_PREFIX}coverage"
#TODO: add benchmarks to ci
#COMMAND ${CMAKE_COMMAND} --build "${PROJECT_BINARY_DIR}" --target "${TARGET_PREFIX}benchmark"
VERBATIM
)
add_dependencies(ci "${TARGET_PREFIX}ci")
#[[global variables for targets configured for this project]]
#private compiler flags for targets configured with cakemake
set("${VARIABLE_PREFIX}_COMPILER_FLAGS" "")
#public compiler flags for targets configured with cakemake
set("${VARIABLE_PREFIX}_PUBLIC_COMPILER_FLAGS" "")
#private linker flags for targets configured with cakemake
set("${VARIABLE_PREFIX}_LINKER_FLAGS" "")
#public linker flags for targets configured with cakemake
set("${VARIABLE_PREFIX}_PUBLIC_LINKER_FLAGS" "")
#[[
convenience function to produce either an immediate boolean value or the value
of a variable
]]
function(get_flag_value flag_or_value dst dst_is_flag)
if(flag_or_value STREQUAL TRUE)
set("${dst}" TRUE PARENT_SCOPE)
set("${dst_is_flag}" FALSE PARENT_SCOPE)
elseif(flag_or_value STREQUAL FALSE)
set("${dst}" FALSE PARENT_SCOPE)
set("${dst_is_flag}" FALSE PARENT_SCOPE)
else()
set("${dst}" "${${flag_or_value}}" PARENT_SCOPE)
set("${dst_is_flag}" TRUE PARENT_SCOPE)
endif()
endfunction()
#[[
include compiler directory
functions used below are defined, for their respective platform / compiler
combination, in the subdirectories included in this switch block
NOTE: variables created within the file scope will not be propagated to the
global scope unless they are added to the
`${VARIABLE_PREFIX}_VARS_TO_PROPAGATE` list
]]
if("${${VARIABLE_PREFIX}_COMPILER_ID}" STREQUAL "Clang")
include("${CMAKE_CURRENT_LIST_DIR}/clang/CMakeLists.txt")
elseif("${${VARIABLE_PREFIX}_COMPILER_ID}" STREQUAL "GNU")
include("${CMAKE_CURRENT_LIST_DIR}/gcc/CMakeLists.txt")
elseif("${${VARIABLE_PREFIX}_COMPILER_ID}" STREQUAL "TinyCC")
include("${CMAKE_CURRENT_LIST_DIR}/tcc/CMakeLists.txt")
else()
#reminder to update when we add support for a new compiler
message(
FATAL_ERROR
"Support for compiler '${${VARIABLE_PREFIX}_COMPILER_ID}' is umimplemented!"
)
endif()
#[[
utility function for declaring a common test target
adds two new targets: `${target_name}` and `${target_name}-static` that can
be used wherever
usage: add_common_test_project(<name> <path>)
]]
function(
"${FUNCTION_PREFIX}add_common_test_project"
target_name
project_directory
)
#ensure we were given a target name
string(LENGTH "${target_name}" target_name_length)
if(${target_name_length} EQUAL 0)
message(
FATAL_ERROR
"Missing target name!"
)
endif()
unset(target_name_length)
#ensure we were given a project directory
string(LENGTH "${project_directory}" project_directory_length)
if(${project_directory_length} EQUAL 0)
message(
FATAL_ERROR
"Missing project directory!"
)
endif()
unset(project_directory_length)
#project compile and link flag variables
set(compiler_flags_var "${VARIABLE_PREFIX}_COMPILER_FLAGS")
set(linker_flags_var "${VARIABLE_PREFIX}_LINKER_FLAGS")
#find sources
set(source_directory "${project_directory}/src")
set(include_directory "${project_directory}/include")
file(
GLOB_RECURSE
TEST_COMMON_SOURCES
LIST_DIRECTORIES FALSE
"${source_directory}/*.c"
)
#shared target
add_library("${target_name}" SHARED)
target_sources("${target_name}" ${TEST_COMMON_SOURCES})
target_include_directories("${target_name}" PUBLIC "${include_directory}")
target_compile_options("${target_name}" PRIVATE ${${compiler_flags_var}})
target_link_options("${target_name}" PRIVATE ${${linker_flags_var}})
#static target
add_library("${target_name}-static" STATIC)
target_sources("${target_name}-static" ${TEST_COMMON_SOURCES})
target_include_directories("${target_name}" PUBLIC "${include_directory}")
target_compile_options("${target_name}" PRIVATE ${${compiler_flags_var}})
target_link_options("${target_name}" PRIVATE ${${linker_flags_var}})
#enable PIC for both targets
set_property(
TARGET "${target_name}" "${target_name}-static"
PROPERTY POSITION_INDEPENDENT_CODE TRUE
)
#append common test library targets to global target list for this project
list(
APPEND "${VARIABLE_PREFIX}_TARGETS"
"${target_name}"
"${target_name}-static"
)
set(
"${VARIABLE_PREFIX}_TARGETS"
"${${VARIABLE_PREFIX}_TARGETS}"
PARENT_SCOPE
)
endfunction()
#[[
usage: add_source_project(callback)
Expects a callback with the format
`function(define_my_target name_prefix dst_prefix)`; the provided callback
must:
- include the `name_prefix` parameter in the name during the declaration
of the target
- provide the target name in the destination variable specified by
`${dst_prefix}_NAME`
- provide the target source directory in the destination variable specified
by `${dst_prefix}_SOURCE_DIRECTORY`
Example:
function(define_my_target name_prefix dst_prefix)
set(name "${name_prefix}my-target")
set(source_dir "${PROJECT_SOURCE_DIR}/src")
add_library("${name}" SHARED)
add_sources("${name}" PUBLIC "${source_dir}/lib.c")
#...
set("${dst_prefix}_NAME" "${name}" PARENT_SCOPE)
set("${dst_prefix}_SOURCE_DIRECTORY" "${source_dir}" PARENT_SCOPE)
endfunction()
]]
function("${FUNCTION_PREFIX}add_source_project" callback)
#ensure we were provided with a callback
string(LENGTH "${callback}" callback_LENGTH)
if(${callback_LENGTH} EQUAL 0)
message(FATAL_ERROR "Missing callback function name!")
endif()
unset(callback_LENGTH)
#invoke callback with empty prefix to create global default importable target
cmake_language(CALL "${callback}" "" asp)
#ensure the callback provided us with the resulting target name
string(LENGTH "${asp_NAME}" asp_NAME_LENGTH)
if(${asp_NAME_LENGTH} EQUAL 0)
message(FATAL_ERROR "Target name not set in 'asp_NAME' variable!")
endif()
unset(asp_NAME_LENGTH)
#append target name to global list of targets
set(target_list_var "${VARIABLE_PREFIX}_TARGETS")
list(APPEND "${target_list_var}" "${asp_NAME}")
set("${target_list_var}" "${${target_list_var}}" PARENT_SCOPE)
endfunction()
#[[
utility function for adding test projects
usage: add_test_project(
NAME <name>
SOURCE_DIRECTORY <path>
INSTRUMENTED_DEPENDENCIES [<callbacks>...]
LINK_LIBRARIES [<libraries>...]
)
]]
function("${FUNCTION_PREFIX}add_test_project")
#parse arguments
cmake_parse_arguments(
atp
""
"NAME;SOURCE_DIRECTORY"
"INSTRUMENTED_DEPENDENCIES;LINK_LIBRARIES"
${ARGN}
)
#[[validate arguments]]
#ensure we were given a test project name
string(LENGTH "${atp_NAME}" atp_NAME_length)
if(${atp_NAME_length} EQUAL 0)
message(
FATAL_ERROR
"Missing test project name!"
)
endif()
unset(atp_NAME_length)
#ensure we were given a test project directory
string(LENGTH "${atp_SOURCE_DIRECTORY}" atp_SOURCE_DIRECTORY_length)
if(${atp_SOURCE_DIRECTORY_length} EQUAL 0)
message(
FATAL_ERROR
"Missing test project directory!"
)
endif()
unset(atp_SOURCE_DIRECTORY_length)
#[[define instrumented dependencies, if specified]]
set(global_target_list_var "${VARIABLE_PREFIX}_TARGETS")
if(DEFINED atp_INSTRUMENTED_DEPENDENCIES)
foreach(dependency IN LISTS atp_INSTRUMENTED_DEPENDENCIES)
#invoke callback with test name prefix to create test-specific target
cmake_language(CALL "${dependency}" "test-${atp_NAME}-" instrumented)
#validate `instrumented_NAME`
string(LENGTH "${instrumented_NAME}" instrumented_NAME_LENGTH)
if(${instrumented_NAME_LENGTH} EQUAL 0)
message(FATAL_ERROR "Target name not set in 'instrumented_NAME' variable!")
endif()
unset(instrumented_NAME_LENGTH)
#add target name to list of instrumented targets for use below
list(APPEND instrumented_dependencies "${instrumented_NAME}")
#add target name to global list of targets
list(APPEND "${global_target_list_var}" "${instrumented_NAME}")
set(
"${global_target_list_var}"
"${${global_target_list_var}}"
PARENT_SCOPE
)
unset(instrumented_NAME)
endforeach()
endif()
#find sources
set(source_directory "${atp_SOURCE_DIRECTORY}/src")
set(include_directory "${atp_SOURCE_DIRECTORY}/include")
set(test_output_directory "${PROJECT_BINARY_DIR}/test/${atp_NAME}")
set(test_project_output_directory "${test_output_directory}/build")
set(test_profile_output_directory "${test_output_directory}/profile")
set(test_report_output_directory "${test_output_directory}/report")
file(
GLOB_RECURSE
TEST_SOURCES
LIST_DIRECTORIES FALSE
"${source_directory}/*.c"
)
#configure tests
foreach(test_src IN LISTS TEST_SOURCES)
message(STATUS "processing test src: ${test_src}")
#transform test executable name
file(TO_CMAKE_PATH "${test_src}" test_src_path)
cmake_path(
RELATIVE_PATH test_src_path
BASE_DIRECTORY "${source_directory}"
OUTPUT_VARIABLE test_src_path
)
string(
REPLACE
"/"
"."
test_name
"${test_src_path}"
)
string(LENGTH "${test_name}" test_name_length)
math(EXPR test_name_length "${test_name_length} - 2")
string(
SUBSTRING
"${test_name}"
0
${test_name_length}
test_name
)
set(test_name "${test_name}")
unset(test_src_path)
#call platform/compiler-specific `add_test(...)` function
cmake_language(
CALL "${FUNCTION_PREFIX}add_test"
TEST_NAME "${test_name}"
TEST_PROJECT_NAME "${atp_NAME}"
TEST_SOURCES "${test_src}"
TEST_OUTPUT_DIRECTORY "${test_project_output_directory}"
TEST_PROFILE_OUTPUT_DIRECTORY "${test_profile_output_directory}"
TEST_REPORT_OUTPUT_DIRECTORY "${test_report_output_directory}"
TEST_INCLUDE_DIRECTORIES "${include_directory}"
TEST_LINK_INSTRUMENTED_DEPENDENCIES ${instrumented_dependencies}
TEST_LINK_LIBRARIES ${atp_LINK_LIBRARIES}
)
#NOTE: have to propagate `${VARIABLE_PREFIX}_TARGETS` due to scope nesting
set(
"${global_target_list_var}"
"${${global_target_list_var}}"
PARENT_SCOPE
)
endforeach()
#call platform/compiler-specific `add_coverage_targets(...)` function
cmake_language(
CALL "${FUNCTION_PREFIX}add_coverage_targets"
TEST_PROJECT_NAME "${atp_NAME}"
TEST_PROFILE_OUTPUT_DIRECTORY "${test_profile_output_directory}"
TEST_REPORT_OUTPUT_DIRECTORY "${test_report_output_directory}"
TEST_LINK_INSTRUMENTED_DEPENDENCIES ${instrumented_dependencies}
)
#NOTE: have to propagate `${VARIABLE_PREFIX}_TARGETS` due to scope nesting
set(
"${global_target_list_var}"
"${${global_target_list_var}}"
PARENT_SCOPE
)
endfunction()