Skip to content

CMake Custom Commands

Who never used CMake and thought: "there could be a command that would do such a thing ....".

When you create a shared library project with a dependency, and I would like all other projects to use this header definition, but you can't do that without copying and pasting that definition across all projects one by one ...

You may want to add an include directory that is global (used by all other projects at the same time).

Something simple, like copying all third-party DLLs (which were not generated by the projects) is done as soon as you compile a new project.

What Does CMake Allow Us To Do?

CMake has a way to create several auxiliary files so that they are included in the main project in order to complement the definition of the compilation projects.

It is possible to create a new command using the macro function:

macro(new_command_name)
...
endmacro()

Within your main CMakeLists.txt, just call the include command with the name of the created files.

Example:

...
include(cmake/add_definitions_global.cmake)
include(cmake/include_directories_global.cmake)
include(cmake/add_linker_flag_global.cmake)
include(cmake/dll_management.cmake)
include(cmake/copy_headers_to_include_directory.cmake)
...

You can find an example in a real project here.

Recently I was able to play around with these CMake commands to complement the build mechanism of my OpenGLStarter library.

In the library I created new commands to extend the functionality of CMake functions to make it easier to define new projects and dependencies within a compilation environment.

Below are listed the functions that I found most interesting that could be added in some way in CMake.

mark_as_internal

This first command transforms a variable defined to be cached so that it is not visible to the user in CMake's graphical interface. This command will be very useful for us to define the following commands that need to use the CMake cache in some way.

#from https://cmake.org/pipermail/cmake/2008-March/020795.html
macro ( mark_as_internal _var )
  set ( ${_var} ${${_var}} CACHE INTERNAL "hide this!" FORCE )
endmacro( mark_as_internal _var )

list_to_string

This command transforms a list (most common type inside CMake) into a space-separated string.

macro(list_to_string list str)
    set(${str} "")
    foreach(entry ${list})
        string(LENGTH "${${str}}" length)
        if( ${length} EQUAL 0 )
            string(APPEND ${str} "${entry}" )
        else()
            string(APPEND ${str} " ${entry}" )
        endif()
    endforeach()
endmacro()

Example:

# creates VARIABLE with a;b;c
set(VARIABLE a b c)
message(STATUS ${VARIABLE})
# add d to VARIABLE. result: a;b;c;d
set(VARIABLE ${VARIABLE} d)
message(STATUS ${VARIABLE})

# after call list_to_string, will transform the a;b;c;d list
# into the string "a b c d"
list_to_string( "${VARIABLE}" VARIABLE_STR )
message(STATUS ${VARIABLE_STR})

add_definitions_global

This command works just like the standard CMake add_definitions, except that it does not add a definition only to the local project, it adds to the upper scope, thus making it applied to all directories that are in the same hierarchy.

A implementação dele pode ser vista nesse link.

############################################################################
# Global compiler flags
############################################################################

set(GLOBAL_DEFINITIONS "" CACHE STRING "")
mark_as_internal(GLOBAL_DEFINITIONS)

macro(add_definitions_global)
    foreach(definition IN ITEMS ${ARGN})
        if (NOT ${definition} IN_LIST GLOBAL_DEFINITIONS)
            list(APPEND GLOBAL_DEFINITIONS ${definition})
            set(GLOBAL_DEFINITIONS ${GLOBAL_DEFINITIONS} CACHE INTERNAL "" FORCE)
        endif() 
    endforeach()
endmacro()
#
# send the variable to the compiler for all projects
#
list_to_string( "${GLOBAL_DEFINITIONS}" GLOBAL_DEFINITIONS_STR )
message(STATUS "global definitions: ${GLOBAL_DEFINITIONS_STR}" )
add_definitions( ${GLOBAL_DEFINITIONS_STR} )
#
# reset global variable after use
#
set(GLOBAL_DEFINITIONS "" CACHE INTERNAL "" FORCE)
#
# initial setup
#
add_definitions_global(-DOS_TARGET_${OS_TARGET}
                       -DARCH_TARGET_${ARCH_TARGET})

Example of use in a project:

project (example)

# local definitions
add_definitions(-DBUILD_OPTIMIZED)
# global definitions 
# (all projets of the parent scope will share the same definition)
add_definitions_global(-DEXAMPLE_STATIC)

...

add_linker_flag_global

CMake has no functions to address the linker like add_definitions. We can set link flags using the target_link_libraries command, but it works locally.

############################################################################
# Global linker flags
############################################################################

set(GLOBAL_LINKER_FLAGS "" CACHE STRING "")
mark_as_internal(GLOBAL_LINKER_FLAGS)

macro(add_linker_flag_global)
    foreach(flag IN ITEMS ${ARGN})
        if (NOT ${flag} IN_LIST GLOBAL_LINKER_FLAGS)
            list(APPEND GLOBAL_LINKER_FLAGS ${flag})
            set(GLOBAL_LINKER_FLAGS ${GLOBAL_LINKER_FLAGS} CACHE INTERNAL "" FORCE)
        endif()
    endforeach()
endmacro()
#
# send the variable to the compiler for all projects
#
list_to_string( "${GLOBAL_LINKER_FLAGS}" GLOBAL_LINKER_FLAGS_STR )
message(STATUS "global linker flags: ${GLOBAL_LINKER_FLAGS_STR}" )
SET(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS} ${GLOBAL_LINKER_FLAGS_STR}")
#
# reset global variable after use
#
set(GLOBAL_LINKER_FLAGS "" CACHE INTERNAL "" FORCE)
#
# initial setup
#

Example of a real implementation here.

With add_linker_flag_global you can configure some flags or linking directives globally.

Example:

if(OS_TARGET STREQUAL mac)
    # Make all projects to compile with
    # Objective-C enable compiler flag on MacOS
    add_linker_flag_global(-ObjC)
endif()

include_directories_global

This command is a version of include_directories, but it affects the upper scope of all projects in the same hierarchy.

############################################################################
# Global include directories
############################################################################

set(GLOBAL_DIRECTORIES "" CACHE STRING "")
mark_as_internal(GLOBAL_DIRECTORIES)

macro(include_directories_global)
    foreach(directory IN ITEMS ${ARGN})
        if (NOT ${directory} IN_LIST GLOBAL_DIRECTORIES)
            list(APPEND GLOBAL_DIRECTORIES ${directory})
            set(GLOBAL_DIRECTORIES ${GLOBAL_DIRECTORIES} CACHE INTERNAL "" FORCE)
        endif() 
    endforeach()
endmacro()
#
# send the variable to the compiler for all projects
#
message(STATUS "global include directories: ${GLOBAL_DIRECTORIES}" )
include_directories( ${GLOBAL_DIRECTORIES} )
#
# reset global variable after use
#
set(GLOBAL_DIRECTORIES "" CACHE INTERNAL "" FORCE)
#
# initial setup
#

Real implementation here.

Example of use in a project:

project(example)

# Any other project will not need
# to manually include this custom
# include directory
include_directories_global( ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT}/custom-include )

...

Managing DLL Files on Windows

One problem with using CMake on windows is that it can happen that you have multiple versions of pre-compiled libraries and you have to link your executable with them.

But it is not enough to just link...

After generating the executable, the program will not run if you don't copy the dynamic library files to the folder of the executable that was generated.

You can copy these dll files to some folder in the system path, or add a new entry in the system path where these libraries are located.

At first glance, it doesn't seem like a very healthy thing to do whenever you set up the environment to compile or run a project.

In linux and mac this usually doesn't happen, because the whole system was made sharing these dynamic library files (on mac they are .frameworks and linux they are .so) according to the packages you install.

In order to manage dynamic library dependencies on windows, a set of commands was created to help define projects that use third-party dlls:

  • register_dll
  • register_dll_absolute_path
  • copy_3rdparty_dll
############################################################################
# DLL Managment
#
# Use these macros only if your
# library have a 3rdparty dll
# that needs to be copied to
# the executable binary directory
############################################################################
set(DLL_LIST "" CACHE STRING "")
mark_as_internal(DLL_LIST)

set(DLL_LIST_NAMES "" CACHE STRING "")
mark_as_internal(DLL_LIST_NAMES)

macro(register_dll)
    foreach(dll IN ITEMS ${ARGN})
        get_filename_component(dll_full_path "${CMAKE_CURRENT_SOURCE_DIR}/${dll}" ABSOLUTE)
        get_filename_component(dll_filename "${CMAKE_CURRENT_SOURCE_DIR}/${dll}" NAME)
        if (NOT ${dll_full_path} IN_LIST DLL_LIST AND 
            NOT ${dll_filename} IN_LIST DLL_LIST_NAMES)

            if(EXISTS "${dll_full_path}")

                list(APPEND DLL_LIST ${dll_full_path})
                set(DLL_LIST ${DLL_LIST} CACHE INTERNAL "" FORCE)

                list(APPEND DLL_LIST_NAMES ${dll_filename})
                set(DLL_LIST_NAMES ${DLL_LIST_NAMES} CACHE INTERNAL "" FORCE)

            else()

                message(FATAL_ERROR "DLL Not Found: ${dll_full_path}")

            endif()

        endif()
    endforeach()
endmacro()

macro(register_dll_absolute_path)
    foreach(dll IN ITEMS ${ARGN})
        get_filename_component(dll_full_path "${dll}" ABSOLUTE)
        get_filename_component(dll_filename "${dll}" NAME)
        if (NOT ${dll_full_path} IN_LIST DLL_LIST AND 
            NOT ${dll_filename} IN_LIST DLL_LIST_NAMES)

            if(EXISTS "${dll_full_path}")

                list(APPEND DLL_LIST ${dll_full_path})
                set(DLL_LIST ${DLL_LIST} CACHE INTERNAL "" FORCE)

                list(APPEND DLL_LIST_NAMES ${dll_filename})
                set(DLL_LIST_NAMES ${DLL_LIST_NAMES} CACHE INTERNAL "" FORCE)

            else()

                message(FATAL_ERROR "DLL Not Found: ${dll_full_path}")

            endif()

        endif()
    endforeach()
endmacro()
#
# send the variable to the compiler for all projects
#
message(STATUS "dll list: ${DLL_LIST_NAMES}" )
#
# reset global variable after use
#
#cannot reset after use... 
# set(DLL_LIST "" CACHE INTERNAL "" FORCE)
# set(DLL_LIST_NAMES "" CACHE INTERNAL "" FORCE)

macro(copy_3rdparty_dll project_name)
    foreach(dll IN ITEMS ${DLL_LIST})
        get_filename_component(dll_filename ${dll} NAME)
        add_custom_command(
            TARGET ${project_name} POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy
                    ${dll}
                    $<TARGET_FILE_DIR:${project_name}>/${dll_filename} )
    endforeach()
endmacro()

Real implementation here.

Example of use in the project that defines that it has dependency on .dll files:

project(example-3rdparty-dll-copy)
# do all CMake configuration of this project
...
# at the end, add the windows library to copy after the project build

if (OS_TARGET STREQUAL win)
   register_dll( lib_win32/file1.dll
                 lib_win32/file2.dll )
endif()

Example of the executable project that copies these dlls to the folder where the executables were generated:

project(example-exe)
...
# copy all dlls registered by any library of this environment
copy_3rdparty_dll( ${PROJECT_NAME} )

copy_file_after_build and copy_directory_after_build

These commands make a file or directory copy to the directory the executable or library was created when the build ends.

A common use case is when you have a resources directory with images, sound, etc.. and your program need these assets in the binary folder when the build finishes.

macro(copy_file_after_build PROJECT_NAME)
    foreach(FILENAME IN ITEMS ${ARGN})
        if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME}")
            add_custom_command(
                TARGET ${PROJECT_NAME} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy
                        ${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME}
                        $<TARGET_FILE_DIR:${PROJECT_NAME}>/${FILENAME} )
        else()
            message(FATAL_ERROR "File Does Not Exists: ${FILENAME}")
        endif()
    endforeach()
endmacro()

macro(copy_directory_after_build PROJECT_NAME)
    foreach(DIRECTORY IN ITEMS ${ARGN})
        if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${DIRECTORY}")
            add_custom_command(
                TARGET ${PROJECT_NAME} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_directory
                        ${CMAKE_CURRENT_SOURCE_DIR}/${DIRECTORY}
                        $<TARGET_FILE_DIR:${PROJECT_NAME}>/${DIRECTORY} )
        else()
            message(FATAL_ERROR "Directory Does Not Exists: ${DIRECTORY}")
        endif()
    endforeach()
endmacro()

Project CMakeLists.txt example:

project (game-tetris)

...

# copy the resources/ folder to the binary path
copy_directory_after_build( ${PROJECT_NAME} resources )

Other Commands

There are a number of other commands that have been implemented according to the need of the framework. You can check them out here.

I hope you enjoyed these tips about CMake.

A big hug!

Alessandro Ribeiro

Leave a Reply

Your email address will not be published. Required fields are marked *