For the sake of a fully working example, we'll start with:
cmake_minimum_required(VERSION 3.25)
project(example)
It probably doesn't need 3.25, but that's what I tested it on, so YMMV.
Now, here's a fairly robust function you can use to try several different preprocessor definitions in service of ensuring a target can see a symbol:
include(CheckSymbolExists)
function(ensure_symbol)
cmake_parse_arguments(PARSE_ARGV 0 ARG "PUBLIC" "TARGET;SYMBOL" "INCLUDES;DEFINITIONS")
# Determine visibility for symbols that need to be added
get_property(type TARGET "${ARG_TARGET}" PROPERTY TYPE)
if (type STREQUAL "INTERFACE_LIBRARY")
set(visibility INTERFACE)
elseif (ARG_PUBLIC)
set(visibility PUBLIC)
else ()
set(visibility PRIVATE)
endif ()
# The base name of the cache variables to create
string(TOUPPER "HAVE_${ARG_SYMBOL}" var_name)
# Check the base version
check_symbol_exists("${ARG_SYMBOL}" "${ARG_INCLUDES}" "${var_name}")
if (${var_name})
return ()
endif ()
# Otherwise, start trying alternatives
foreach (def IN LISTS ARG_DEFINITIONS)
set(local_name "${var_name}${def}")
set(CMAKE_REQUIRED_DEFINITIONS "-D${def}")
check_symbol_exists("${ARG_SYMBOL}" "${ARG_INCLUDES}" "${local_name}")
if (${local_name})
target_compile_definitions("${ARG_TARGET}" "${visibility}" "${def}")
return()
endif ()
endforeach ()
message(FATAL_ERROR "Could not ensure ${ARG_TARGET} can use ${ARG_SYMBOL}")
endfunction()
It uses check_symbol_exists
in a loop over candidate symbols to define in order to make the symbol available. Whichever one works, it attaches to the target via target_compile_definitions
.
The main complicating feature is that it gives users control over the visibility of the definition to linkees. It's always INTERFACE
for interface libraries (which is required), but for other target types, it defaults to PRIVATE
. The user may set it to PUBLIC
by passing the appropriate option to ensure_symbol
.
Finally, using it is pretty simple:
add_executable(myapp main.cpp)
ensure_symbol(
TARGET myapp
SYMBOL asprintf
INCLUDES stdio.h
DEFINITIONS _GNU_SOURCE _BSD_SOURCE
)
I tested it locally, and it adds _GNU_SOURCE
on my machine, which can be seen at the bottom of the following output:
$ cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release
-- The C compiler identification is GNU 11.3.0
-- The CXX compiler identification is GNU 11.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for asprintf
-- Looking for asprintf - not found
-- Looking for asprintf
-- Looking for asprintf - found
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alex/test/build
$ cmake --build build/ --verbose
[1/2] /usr/bin/c++ -D_GNU_SOURCE -O3 -DNDEBUG -MD -MT CMakeFiles/myapp.dir/main.cpp.o -MF CMakeFiles/myapp.dir/main.cpp.o.d -o CMakeFiles/myapp.dir/main.cpp.o -c /home/alex/test/main.cpp
[2/2] : && /usr/bin/c++ -O3 -DNDEBUG CMakeFiles/myapp.dir/main.cpp.o -o myapp && :