Pular para o conteúdo

Configurando um projeto

Poderíamos usar os arquivos tasks.json e launch.json para compilar e executar o código, como fizemos anteriormente. Entretanto, para projetos maiores, o CMake permite uma maior escalabilidade. Além disso, ele permite definir instruções de compilação diferentes para vários sistemas operacionais, além de argumentos opcionais, como flags de compilação e definições de pré-processador.

Para não bagunçar o projeto oriented_graph, vamos copiá-lo para uma nova pasta chamada oriented_graph_with_cmake.

Fedora, WSL ou Mint
cd ~/dev/c_cpp_projects
cp -r oriented_graph oriented_graph_with_cmake
cd oriented_graph_with_cmake

Também podemos adotar a convenção de colocar todos os arquivos de código-fonte dentro de uma pasta chamada src.

Fedora, WSL ou Mint
mkdir -p src
mv main.cpp src
mv node src
mv edge src
mv graph src

O principal arquivo de configuração do CMake é o CMakeLists.txt. Ele é responsável por definir as configurações do projeto e as dependências necessárias para a compilação.

Crie um arquivo chamado CMakeLists.txt na raiz da pasta oriented_graph. Atente-se à letras maiúsculas e minúsculas no nome do arquivo, ele precisa estar escrito exatamente como acima.

Fedora, WSL ou Mint
cd ~/dev/c_cpp_projects/oriented_graph_with_cmake
touch CMakeLists.txt
code .

Vamos preencher trecho por trecho do arquivo CMakeLists.txt para entender o que cada parte faz.

Primeiramente, é necessário definir a versão do CMake que será utilizada. Na época de escrita deste guia, a versão 3.31.6 era a mais recente disponível.

Nós podemos definir um intervalo de versões suportadas, para garantir que o projeto seja compilado corretamente em diferentes ambientes. Isso é feito com o comando cmake_minimum_required.

CMakeLists.txt
## Version
cmake_minimum_required(VERSION 3.12...3.31)

Em seguida, usamos um comando para garantir que versões anteriores à 3.12 passem por uma política de compatibilidade para conseguir realizar a compilação.

CMakeLists.txt
if (${CMAKE_VERSION} VERSION_LESS 3.12)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif ()

Agora, vamos definir o nome do projeto e a linguagem utilizada, que é o C++.

CMakeLists.txt
## Project
project(oriented_graph
LANGUAGES CXX
)

O C++ tem várias versões, e pode ser customizado para diferentes ambientes. No nosso caso, queremos usar uma versão mais recente (no momento a 23) e compatível com o padrão oficial. Para isso, usamos comandos iniciados em set para definir as variáveis de ambiente.

CMakeLists.txt
## Environment
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

As flags de compilação são opções que podem ser passadas para o compilador para alterar o comportamento da compilação.

No nosso caso, queremos ativar todos os avisos de compilação. Além disso, é importante manter a compatibilidade com outros compiladores, como o GCC, o Apple Clang e MSVC.

Para isso, usamos o comando add_compile_options.

CMakeLists.txt
## Warnings
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
add_compile_options(
-Wall -Wextra # Reasonable warnings
-Wpedantic # Strict ISO C++ compliance
-Wconversion # Warn on implicit type conversions
-Wsign-conversion # Warn on signed/unsigned mismatches
)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_compile_options(
/W4 # High warning level
/permissive- # Enforce standards compliance
)
endif()

Nosso projeto guarda o código-fonte na pasta src. Para que o CMake consiga encontrar os arquivos dentro dela, precisamos listá-la no arquivo CMakeLists.txt.

CMakeLists.txt
## Directories
add_subdirectory(src)

Outras pastas comumente usadas são:

  • include: para arquivos de cabeçalho (.h ou .hpp);
  • lib: para bibliotecas externas;
  • tests: para testes automatizados;
  • docs: para documentação; e
  • assets: para arquivos estáticos, como imagens e fontes.

Nosso projeto não possui essas pastas, mas caso você venha a precisar em outros projetos, você usaria o comando add_subdirectory para cada uma delas.

No final, o arquivo CMakeLists.txt deve ficar da seguinte forma.

CMakeLists.txt
## Version
cmake_minimum_required(VERSION 3.12...3.31)
if (${CMAKE_VERSION} VERSION_LESS 3.12)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif ()
## Project
project(oriented_graph
LANGUAGES CXX
)
## Environment
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
## Warnings
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
add_compile_options(
-Wall -Wextra # Reasonable warnings
-Wpedantic # Strict ISO C++ compliance
-Wconversion # Warn on implicit type conversions
-Wsign-conversion # Warn on signed/unsigned mismatches
)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_compile_options(
/W4 # High warning level
/permissive- # Enforce standards compliance
)
endif()
## Directories
add_subdirectory(src)

Agora que já temos o arquivo CMakeLists.txt configurado, precisamos adicionar os arquivos de código-fonte, que estão na pasta src. Para isso, vamos criar um novo arquivo CMakeLists.txt dentro da pasta src.

Fedora, WSL ou Mint
touch src/CMakeLists.txt

Dentro deste novo arquivo src/CMakeLists.txt, precisamos definir o nome do arquivo executável que será gerado após a compilação. No nosso caso, o nome do executável será oriented_graph, e ele será gerado a partir do arquivo main.cpp.

src/CMakeLists.txt
## Executable
add_executable(oriented_graph
main.cpp
)

Agora, precisamos adicionar os módulos que estão dentro da pasta src. Para isso, usamos o comando add_subdirectory para cada um deles.

src/CMakeLists.txt
## Directories
add_subdirectory(node)
add_subdirectory(edge)
add_subdirectory(graph)

O arquivo principal, main.cpp, depende da classe Graph. Isso é definido pela importação do arquivo de cabeçalho graph.hpp, que está dentro da pasta graph.

src/main.cpp
#include "graph/graph.hpp"

Para que o CMake consiga vincular esse arquivo, precisamos adicionar a pasta graph como um diretório de inclusão. Isso é feito com o comando target_include_directories. Ele vai incluir o diretório graph de forma privada ao executável oriented_graph.

Aqui decidimos usar a keyword PRIVATE para que os outros módulos que main.cpp importa não sejam disponibilizados para arquivos que venham a incluir main.cpp. Isso é importante para evitar que outros módulos acessem arquivos que não deveriam.

src/CMakeLists.txt
## Linking libraries
target_link_libraries(oriented_graph
PRIVATE
graph
)

No final, o arquivo src/CMakeLists.txt deve ficar da seguinte forma.

src/CMakeLists.txt
## Executable
add_executable(oriented_graph
main.cpp
)
## Directories
add_subdirectory(node)
add_subdirectory(edge)
add_subdirectory(graph)
## Linking libraries
target_link_libraries(oriented_graph
PRIVATE
graph
)

A forma de criar diferentes módulos no CMake é definir cada um como uma biblioteca estática. Elas são montadas por um arquivo *.hpp que contém funções e variáveis que podem ser utilizadas por outros arquivos do projeto.

Assim como fizemos para a pasta src, precisamos criar um arquivo CMakeLists.txt para cada módulo. Esses arquivos são responsáveis por definir as configurações específicas de cada um.

Fedora, WSL ou Mint
touch src/node/CMakeLists.txt
touch src/edge/CMakeLists.txt
touch src/graph/CMakeLists.txt

Vamos tomar como exemplo o módulo edge, precisamos definir o nome da biblioteca, que será edge. Para isso, acesse o arquivo src/edge/CMakeLists.txt, e utilize o comando add_library.

src/edge/CMakeLists.txt
## Library
add_library(edge)

Em seguida, precisamos adicionar os arquivos de código-fonte e de cabeçalho que fazem parte do módulo. O módulo edge possui apenas um arquivo de cabeçalho, o edge.hpp, e um arquivo de código-fonte, o edge.cpp. Usaremos o comando target_sources para adicionar esses arquivos à biblioteca.

Os arquivos de cabeçalho são adicionados como públicos para que eles possam ser incluídos por outros módulos. Já os arquivos de código-fonte são adicionados como privados, de forma que outros módulos precisem utilizar o arquivo de cabeçalho para conseguir acessá-los.

src/edge/CMakeLists.txt
## Sources
target_sources(edge
PUBLIC
edge.hpp
PRIVATE
edge.cpp
)

Agora, precisamos definir o diretório edge como um diretório de inclusão, para que ele possa ser acessado por outros módulos. Isso é feito com o comando target_include_directories.

O CMake vai incluir o diretório edge de forma pública à biblioteca edge. É necessário usar a keyword PUBLIC para que outros arquivos consigam incluir a classe Edge, definida no arquivo edge.hpp.

src/edge/CMakeLists.txt
## Define module
target_include_directories(edge
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

O módulo edge depende da classe Node. Isso é definido pela importação do arquivo de cabeçalho node.hpp, que está dentro da pasta node.

src/edge/edge.hpp
#include "../node/node.hpp"

Para que o CMake consiga vincular esse arquivo, precisamos adicionar a pasta node como um diretório de inclusão. Isso é feito com o comando target_link_libraries. Da mesma forma que fizemos para o executável oriented_graph, vamos incluir o diretório node de forma privada à biblioteca edge.

src/edge/CMakeLists.txt
## Link libraries
target_link_libraries(edge
PRIVATE
node
)

O módulo node não possui dependências, então não precisamos nos preocupar com isso. Já o módulo graph depende do módulo node e do módulo edge. Dessa forma, devemos incluir ambos na lista de dependências do comando target_link_libraries.

No final, o arquivo src/node/CMakeLists.txt deve ficar da seguinte forma.

src/node/CMakeLists.txt
## Library
add_library(node)
## Sources
target_sources(node
PUBLIC
node.hpp
PRIVATE
node.cpp
)
## Define module
target_include_directories(node
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

O arquivo src/edge/CMakeLists.txt deve ficar da seguinte forma.

src/edge/CMakeLists.txt
## Library
add_library(edge)
## Sources
target_sources(edge
PUBLIC
edge.hpp
PRIVATE
edge.cpp
)
## Define module
target_include_directories(edge
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
## Link libraries
target_link_libraries(edge
PRIVATE
node
)

O arquivo src/graph/CMakeLists.txt deve ficar da seguinte forma.

src/graph/CMakeLists.txt
## Library
add_library(graph)
## Sources
target_sources(graph
PUBLIC
graph.hpp
PRIVATE
graph.cpp
)
## Define module
target_include_directories(graph
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
## Link libraries
target_link_libraries(graph
PRIVATE
node
edge
)

Por fim, vamos fazer stage das modificações e realizar um novo commit.

Fedora, WSL ou Mint
git add .
git commit -m "Configure CMake"