Comandos Essenciais e Estruturas no CMakeLists.txt para Projetos C++

Cenário 1: Projeto de Módulo Único (Estrutura Simples)

Em projetos menores, onde não há uma hierarquia complexa de submódulos, a configuração do CMake é direta. Considere a seguinte estrutura de diretórios:

CoreSystem/
├── CMakeLists.txt
├── include/
│   └── core_engine.h
├── src/
│   ├── app_runner.cpp
│   └── core_engine.cpp
└── build/

Comandos Fundamentais

Versão Mínima e Nome do Projeto

cmake_minimum_required(VERSION 3.15): Define a versão mínima do CMake necessária para processar o script.

project(CoreSystem): Estabelece o nome do projeto e inicializa variáveis internas do sistema.

Definição de Alvos (Targets)

add_executable(app_runner src/app_runner.cpp): Cria um alvo executável chamado app_runner a partir do arquivo fonte especificado.

add_library(core_engine STATIC src/core_engine.cpp): Gera uma biblioteca estática denominada core_engine.

Diretórios de Inclusão e Vinculação

target_include_directories(core_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include): Adiciona o diretório include ao caminho de busca de cabeçalhos. O uso de PUBLIC garante que qualquer alvo que vincule core_engine também herde esse caminho.

target_link_libraries(app_runner PRIVATE core_engine): Vincula a biblioteca core_engine ao executável app_runner, resolvendo dependências de símbolos em tempo de linkagem.

Gerenciamento de Dependências Externas (Exemplo com OpenCV)

set(OpenCV_DIR "/opt/opencv4/lib/cmake/opencv4"): Aponta para o diretório onde os arquivos de configuração do CMake da biblioteca externa estão localizados.

find_package(OpenCV 4.5 REQUIRED): Busca a instalação do OpenCV. A palavra-chave REQUIRED interrompe a configuração caso a biblioteca não seja encontrada. Após a execução, variáveis como OpenCV_LIBS e OpenCV_INCLUDE_DIRS são populadas automaticamente.

Para aplicar essas dependências ao alvo:

target_include_directories(app_runner PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(app_runner PRIVATE ${OpenCV_LIBS})

Configuração de Diretórios de Saída

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin): Direciona todos os executáveis gerados para a pasta bin dentro do diretório de build.

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib): Define o local de saída para bibliotecas estáticas.

Cenário 2: Projeto Multi-Módulo (Estrutura Hierárquica)

Para sistemas maiores, é recomendável dividir o código em submódulos, cada um com seu próprio arquivo de configuração.

PlatformSuite/
├── CMakeLists.txt                # Configuração raiz
├── src/
│   └── main_app.cpp              # Fonte principal
├── network_module/
│   ├── CMakeLists.txt            # Configuração do submódulo de rede
│   ├── src/
│   │   └── net_handler.cpp
│   └── include/
│       └── net_handler.h
└── data_module/
    ├── CMakeLists.txt            # Configuração do submódulo de dados
    ├── src/
    │   └── db_connector.cpp
    └── include/
        └── db_connector.h

Configuração do Módulo Raiz

O arquivo principal é responsável por orquestrar a compilação e conectar os submódulos.

cmake_minimum_required(VERSION 3.15)
project(PlatformSuite)

# Integra os subdiretórios ao sistema de build
add_subdirectory(network_module)
add_subdirectory(data_module)

# Define o executável principal
add_executable(platform_app src/main_app.cpp)

# Vincula as bibliotecas dos submódulos e propaga seus cabeçalhos
target_link_libraries(platform_app PRIVATE net_lib data_lib)

Nota sobre add_subdirectory: É possível compilar diretórios externos especificando um caminho de build distinto, por exemplo: add_subdirectory(../external_deps ext_build).

Configuração dos Submódulos

Cada submódulo gerencia suas próprias fontes e expõe suas interfaces.

Exemplo para network_module/CMakeLists.txt:

project(NetworkModule)

add_library(net_lib STATIC src/net_handler.cpp)

# Expõe o diretório de inclusão para o módulo pai e outros consumidores
target_include_directories(net_lib PUBLIC 
    $<build_interface:>
    $<include>
)
</include></build_interface:>

Propagação de Variáveis para o Escopo Pai

Quando um submódulo precisa compartilhar variáveis (como caminhos de bibliotecas de terceiros) com o módulo raiz, utiliza-se a diretiva PARENT_SCOPE.

set(EXTERNAL_MATH_LIB "${CMAKE_CURRENT_SOURCE_DIR}/third_party/mathlib/libmath.a")
set(EXTERNAL_MATH_LIB ${EXTERNAL_MATH_LIB} PARENT_SCOPE)

set(EXTERNAL_MATH_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/third_party/mathlib/include")
set(EXTERNAL_MATH_INCLUDE ${EXTERNAL_MATH_INCLUDE} PARENT_SCOPE)

Transitividade e Palavras-chave de Visibilidade

O uso correto de PUBLIC em target_include_directories elimina a necessidade de redeclarar caminhos de cabeçalhos no módulo raiz. Se net_lib for marcado como PUBLIC, o platform_app herdará automaticamente os include directories ao usar target_link_libraries.

  • Apenas target_include_directories: A compilação passa, mas a linkagem falha com erros de "referência indefinida" (undefined reference).
  • Apenas target_link_libraries: A linkagem encontra a biblioteca, mas a compilação falha por não encontrar os arquivos de cabeçalho.

Ambos os aspectos (cabeçalhos e binários) devem ser configurados, seja explicitamente ou através da transitividade do CMake moderno.

Diferenças entre Comandos de Escopo Global e por Alvo

No CMake moderno, prefira sempre os comandos prefixados com target_:

  • Erro de #include não encontrado: Utilize target_include_directories para informar ao compilador onde buscar os cabeçalhos.
  • Erro de linkagem (símbolo indefinido): Utilize target_link_libraries para instruir o linker sobre quais bibliotecas vincular.
  • Evite target_link_directories: A menos que estritamente necessário, evite usar este comando. Prefira passar caminhos absolutos ou usar find_package para resolver dependências de forma robusta.

Comandos globais legados como include_directories e link_libraries poluem o escopo e afetam todos os alvos subsequentes no mesmo diretório, o que é considerado uma má prática em projetos complexos.

Cofniguração para Compilação Cruzada (Cross-Compiling)

Ao compilar para uma arquitetura diferente da máquina hospedeira (ex: ARM a partir de x86), um arquivo toolchain (como arm_toolchain.cmake) é utilizado.

# Definição dos compiladores
set(CMAKE_C_COMPILER "aarch64-linux-gnu-gcc")
set(CMAKE_CXX_COMPILER "aarch64-linux-gnu-g++")

# Diretório raiz do sistema de destino (sysroot)
set(CMAKE_SYSROOT "/opt/cross-root/aarch64")

# Ajuste dos caminhos de busca
set(CMAKE_FIND_ROOT_PATH "/opt/cross-root/aarch64")

# Regras de busca para o CMake
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Explicação das variáveis de modo de busca:

  • CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER: O CMake buscará programas (como compiladores ou ferramentas de build) apenas no sistema hospedeiro, ignorando o sysroot.
  • CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY: Restringe a busca de pacotes (via find_package) exclusivamente ao ambiente do sysroot.
  • CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY e CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY: Garantem que bibliotecas e cabeçalhos sejam extraídos apenas do sistema de destino, evitando incompatibilidades de arquitetura.

Tags: CMake C++ Cross-Compilation Build Systems CMakeLists.txt

Publicado em 10-12 15:44