Sometimes I find myself copying a lot of CMake sections, because I need similar functionality in different places. While duplication is not a great way of reuse, CMake was always just one of those languages that I never took the time to properly understand. I know enough to be dangerous and knowing anything more would just lead to a false sense of security that I would be able to do all kinds of nifty things without looking anything up.

At this point though, I’ve copied this section around to numerous different applications - and multiple instances within each - and I can never remember which is the most recent - and least buggy hopefully. So today I took the time to figure out how functions in CMake work. So here it is:

# cmake/enable-sanitizers.cmake
function(add_sanitizer_options TARGET)
  string(TOUPPER ${TARGET} UPPER_TARGET)
  option(ENABLE_ASAN_FOR_${UPPER_TARGET} "enable asan for ${TARGET}" ON)
  option(ENABLE_TSAN_FOR_${UPPER_TARGET} "enable tsan for ${TARGET}" OFF)
  if(ENABLE_ASAN_FOR_${UPPER_TARGET} AND ENABLE_TSAN_FOR_${UPPER_TARGET})
    message(SEND_ERROR "tsan and asan are mutually exclusive")
  endif()
  if(ENABLE_ASAN_FOR_${UPPER_TARGET})
      target_link_libraries(${TARGET} PRIVATE -fsanitize=address,undefined)
      target_compile_options(${TARGET} PRIVATE -fsanitize=address,undefined)
  endif()
  if(ENABLE_TSAN_FOR_${UPPER_TARGET})
      target_link_libraries(${TARGET} PRIVATE -fsanitize=thread)
      target_compile_options(${TARGET} PRIVATE -fsanitize=thread)
  endif()
endfunction()

Most of the above is pretty standard if/else but there are a few things in there that I will certainly find useful down the line. string(TOUPPER ...) and similar functions are useful for keeping things neat and message(SEND_ERROR ...) is the closest you can get to a panic from what I can tell. CMake is a weird language.

Adding this under a cmake directory in my repos, freed me from having scattered elsewhere. Using it is as simple as adding the cmake directory to your CMAKE_MODULE_PATH - sort of like the -I flag on compilers - and including and calling it where needed.

# CMakeLists.txt
# allow calling `include` on files in the cmake directory
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
# CMakeLists.txt everywhere you need it
include(enable-sanitizers)
# assuming target named `program`
add_sanitizer_options(program PUBLIC)

This adds the ENABLE_ASAN_FOR_PROGRAM and ENABLE_TSAN_FOR_PROGRAM options when you run ccmake or cmake-gui, that you can enable and disable at will!

This particular example is from my DSP framework where I have a pretty substantial test directory at this point. It includes test programs, unittests, benchmarks, etc, that each have their own CMakeLists.txt files, and it was becoming very cumbersome copying everything back and forth all the time.

The reason I have held off on learning CMake is that the code is already complex enough as is and if I start doing these kinds of “optimizations” and “tricks” all over my build system, the build system is going to end up in the same bucket. Unfortunately that is usually how it goes with C and C++ projects and their accompanying build systems when they reach a certain level of complexity. With that being said this is a very welcomed change as long as I am the only person working on the framework.