Pular para o conteúdo

Comandos Customizados no CMake

Quem nunca se deparou com o CMake e pensou: "podia existir um comando que fizesse tal coisa ....".

Quando você cria um projeto de biblioteca compartilhada com uma dependência, e gostaria que todos os outros projetos usem essa definição de cabeçalho, mas não da para fazer isso sem sair copiando e colando essa definição em todos os projetos um a um...

Talvez você queira adicionar um diretório de includes que fosse global (usado por todos os outros projetos ao mesmo tempo).

Até uma coisa simples, como a cópia de todas as DLLs de terceiros (que não foram geradas pelos projetos) seja feita assim que você compilar um projeto.

O Que o CMake nos Permite Fazer?

O CMake possui uma forma de criar vários arquivos auxiliares para que eles sejam inclusos no projeto principal de forma a complementar a definição dos projetos de compilação.

É possível criar um comando novo através da função macro:

macro(new_command_name)
...
endmacro()

E dentro do seu CMakeLists.txt principal, basta chamar o comando include com o nome dos arquivos criados.

Exemplo:

...
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)
...

Você pode encontrar um exemplo em um projeto real nesse link.

Recentemente eu pude brincar um pouco com esses comandos do CMake para complementar o mecanismo de build da minha biblioteca OpenGLStarter.

Nela eu criei novos comandos para estender a funcionalidade das funções do CMake para facilitar definir novos projetos e dependências dentro de um ambiente de compilação.

Abaixo estão listadas as funções que achei mais interessantes que poderiam ser agregadas de alguma forma no CMake.

mark_as_internal

Esse primeiro comando transforma uma variável definida para ser guardada em cache não fique visível para o usuário na interface gráfica do CMake. Esse comando será muito útil para usarmos na definição dos comandos a seguir que precisam usar do cache do CMake de alguma forma.

#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

Esse comando transforma uma lista (tipo mais comum dentro do CMake) em uma string separada por espaços.

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()

Exemplo:

# 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

Esse comando funciona exatamente como o add_definitions padrão do CMake, só que ele não adiciona uma definição somente ao projeto local, ele adiciona ao escopo superior, fazendo assim, ser aplicado a todos os diretórios que se encontram na mesma hierarquia.

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})

Exemplo de uso em um projeto:

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

O CMake não possui funções para tratar o linker como o add_definitions. Nós podemos configurar as flags de linkagem usando o comando target_link_libraries, mas ele tem efeito local.

############################################################################
# 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
#

Exemplo de implementação real aqui.

Com o add_linker_flag_global você pode configurar algumas flags ou diretivas de linkagem globalmente.

Exemplo:

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

Esse comando é uma versão do include_directories, mas ele afeta o escopo superior de todos os projetos na mesma hierarquia.

############################################################################
# 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
#

Implementação real aqui.

Exemplo de uso em um projeto:

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 )

...

Gerenciamento de Arquivos DLL Dentro do Windows

Um problema em usar o CMake no windows é que pode ocorrer de você ter várias versões de bibliotecas pre-compiladas e você tem que linkar seu executável com elas.

Mas não basta somente linkar...

Depois de gerar o executável, o programa não vai rodar se você não copiar os arquivos de bibliotecas dinâmicas para a pasta do executável que foi gerado.

É possível você copiar esses arquivos dlls para alguma pasta no path do sistema, ou acrescentar uma nova entrada no path do sistema para onde essas bibliotecas se encontram.

A primeira vista, não parece ser uma coisa muito saudável de se fazer sempre que você configurar o ambiente para compilar ou executar um projeto.

No linux e mac isso geralmente não ocorre, porque todo o sistema é voltado para compartilhamento desses arquivos de bibliotecas dinâmicas (no mac são os .frameworks e no linux são os .so) de acordo com os pacotes que você instala.

Para poder gerenciar as dependências de bibliotecas dinâmicas no windows foi criado um conjunto de comandos para ajudar na definição de projetos que usam dlls de terceiros:

  • 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()

Implementação real aqui.

Exemplo de uso no projeto que define que possui dependencia de arquivos .dll:

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()

Exemplo do projeto executável que copia essas dlls para a pasta onde foram gerados os executáveis:

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

Esses comandos fazem a cópia de arquivos ou diretórios para as pastas finais dos executáveis ou bibliotecas que foram criadas.

Um caso de uso comum é quando você tem uma pasta de recursos com imagens, sons, etc... e seu programa precisa destes assets na pasta do binário que foi gerado ao final do build.

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()

Exemplo do CMakeLists.txt de um projeto:

project (game-tetris)

...

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

Outros Comandos

Existe uma série de outros comandos que fui implementando de acordo com a necessidade do framework. Vocês podem conferi-los nesse link.

Espero que tenham gostado dessas dicas a respeito do CMake.

Um grande abraço!

Alessandro Ribeiro

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *