Introdução ao CMake

7 minutos de leitura

Há alguns meses venho me forçado a aprender uma linguagem que há tempos tenho vontade de estudar. E essa linguagem é C++. No entanto, ao aprender C++ não aprendemos só a programar, mas também a operar as ferramentas de compilação e, da mesma forma que no passado, quando eu aprendi C, para C++ eu decidi aprender junto uma outra ferramenta de build. Na época, quando estudava C, eu decidi aprender a fazer Makefiles para usar com meus programas. Hoje, para C++ eu decidi que irei usar CMake para esta tarefa.

Este artigo irá prover um conjunto de instruções diretas de como usar CMake para compilar projetos C++. Todos os passos serão feitos usando Linux como base.

Introdução

A ferramenta make e os arquivos de Makefile provêm um sistema de compilação bastante prático para gerenciar a compilação e recompilação de programas (e projetos) escritos em qualquer linguagem. Eu frequentemente uso Makefiles para automatizar processos de build, desde projetos em C até artigos escritos em LaTeX. No entanto, as vezes a construção de Makefiles começam a se tornar uma parte bastante complexa da manutenção do projeto, especialmente quando começamos a construir projetos com múltiplas pastas. Um exemplo de Makefile complexo, pode ser encontrado em meu último projeto em C: libadt.

E é nesse quesito que o CMake mais se destaca - CMake é um gerador de Makefiles multiplataforma. Sim, você não entendeu errado, o CMake não compila seu projeto, ele somente gera os Makefiles que você usará para compilar o projeto, usando make!

Primeiro exemplo: Hello World

Aproveitando que estou começando a estudar C++, iremos começar então com o mais básico de todos os códigos da Humanidade:

Aqui temos somente um arquivo para compilar. Por isto, na mesma pasta desse arquivo iremos colocar o seguinte arquivo CMakeLists.txt:

Nosso arquivo CMakeLists.txt é composto de três linhas:

  • A primeira linha determina a versão mínima do CMake para nosso projeto. Essa versão pode ser qualquer versão para os propósitos do nosso post, mas é importante dizer que a versão do CMake determina quais funcionalidades estarão disponíveis em nosso ambiente de build. Em nosso caso, estou usando uma versão anterior à versão do sistema que estou trabalhando (3.5.1);
  • A segunda linha é referente ao comando project() que configura o nome do nosso projeto;
  • A terceira linha determina que um executável será criado tendo como base o arquivo helloworld.cpp. O primeiro argumento é o nome do executável que deverá ser criado e o segundo argumento o caminho do fonte a ser usado para construir o executável.

Para construir nosso projeto, primeiramente verifique se o cmake está instalado e qual versão está usando. Se o cmake não estiver instalado (em geral não está, pois não faz parte da suíte básica das distribuições Linux) será preciso instalá-lo (No Ubuntu, são os pacotes build-essential e cmake).

Para compilar, entre na pasta do projeto e execute o comando cmake ., onde o . indica a pasta onde se encontra o arquivo CMakeLists.txt que ele usará como base para a construção do Makefile:

eu@Ox0fff ~/dev/cmake/ex01 $ cmake .
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/eu/dev/cmake/ex01

Após a execução do cmake, serão gerados alguns arquivos na pasta atual do projeto. Entre eles, você encontrará o Makefile necessário para que possamos compilar nosso projeto. Como nosso Makefile já foi criado, usaremos o comando make para finalmente compilar nosso querido Hello World:

eu@Ox0fff ~/dev/cmake/ex01 $ make
Scanning dependencies of target hello
[ 50%] Building CXX object CMakeFiles/hello.dir/helloworld.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
eu@Ox0fff ~/dev/cmake/ex01 $ ./hello
Hello World!

Funcionou! No entanto, como você pode imaginar, esse processo é um tanto exagerado para somente compilarmos um simples Hello World. No nosso caso, poderíamos ter usado um simples g++ helloworld.cpp -o hello para compilar que alcançaríamos o mesmo resultado. No entanto, é importante considerarmos esse primeiro exemplo para que vocês possam ver os passos de uma geração de Makefile. Se quiserem ver o resultado da geração do cmake, vocês podem abrir o arquivo Makefile e ver por vocês mesmos a quantidade de código que o CMake gerou sozinho para automatizar a compilação.

Segundo exemplo: Projeto com Subpastas

À medida que nossos projetos de desenvolvimento crescem, é comum nós organizarmos os mesmos em subpastas. É nestas horas que nossos Makefiles começam a ficar mais “detalhados” quando temos subpastas presentes - de fato, é prática comum adicionar Makefiles individuais em cada subpasta presente no projeto ou o uso de includes dentro de cada pasta, que são referenciados pelo Makefile principal, que divide o script de build para cada uma das subpastas. Esses Makefiles ou includes são invocados (cada um à sua maneira) através do Makefile principal, presente na raiz do projeto.

Definitivamente o CMake se torna bastante útil nessa situação. No exemplo que apresentaremos agora, um projeto com uma estrutura de pastas será usado, conforme a listagem abaixo:

.
|-- CMakeLists.txt
|-- build
|-- includes
|   \-- Mensagem.h
\-- src
    |-- main.cpp
    \-- Mensagem.cpp
3 directories, 4 files

Neste segundo exemplo, não apresentarei os códigos-fonte do exemplo para não aumentar em demasia o tamanho deste post. No entanto, no final do post haverá um link para o repositório dos exemplos do blog, onde você poderá conferir os fontes na íntegra, inclusive com os arquivos para usar com o CMake.

Você pode ver na estrutura acima que iremos colocar nossos arquivos de cabeçalho na pasta includes e os nossos códigos-fonte na pasta src. Também foi criada uma pasta build (atualmente vazia) onde normalmente o nosso executável final será colocado, junto com todos os temporários gerados. Nosso arquivo CMakeLists.txt ficará com o seguinte formato (ligeiramente diferente do anterior):

As principais alterações no nosso arquivo são as seguintes:

  • O comando include_directories, usado para indicar onde estarão os cabeçalhos usados para a compilação do nosso projeto;
  • O comando set(FONTES ...) que é usado para criar uma variável (FONTES) que conterá os caminhos de todos os arquivos de código-fonte (.cpp) do projeto. No entanto, como cada arquivo precisa ser adicionado manualmente, deixamos esta linha comentada;
  • O comando file() que usamos para adicionar arquivos no nosso projeto. GLOB (ou GLOB_RECURSE) é usado para criar uma lista de todos os arquivos que correspondem ao wildcard informado;
  • O comando add_executable() agora usa a variável que criamos acima em vez de fazer referência direta à cada arquivo de fonte, com o fim de compilar nosso executável mensagem.

Para nosso exemplo, nós iremos colocar todos os arquivos gerados automaticamente dentro da pasta build, então iremos fazer as coisas um pouco diferentes. Como passamos onde está o nosso CMakeFiles.txt para o cmake, aqui fica fácil de resolver essa questão:

eu@Ox0fff ~/dev/cmake/ex02 $ cd build/
eu@Ox0fff ~/dev/cmake/ex02/build $ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
(...)
-- Build files have been written to: /home/eu/dev/cmake/ex02/build

Aqui na pasta build foi gerado nosso Makefile, que já referencia corretamente aos arquivos que estão nas pastas includes e src. Nosso projeto poderá ser compilado diretamente da pasta build, da mesma forma que o exemplo anterior:

eu@Ox0fff ~/dev/cmake/ex02/build $ make
Scanning dependencies of target mensagem
[ 33%] Building CXX object CMakeFiles/mensagem.dir/src/Mensagem.cpp.o
[ 66%] Building CXX object CMakeFiles/mensagem.dir/src/main.cpp.o
[100%] Linking CXX executable mensagem
[100%] Built target mensagem
eu@Ox0fff ~/dev/cmake/ex02/build $ ./mensagem
Classe criada, texto = 'Hello World mais avançado!'.
Exibindo mensagem: Hello World mais avançado!

A vantagem de usarmos a pasta build é a de facilitar a separação do que é o projeto e o que é a geração automática do build e da compilação do projeto. Isto também facilita a limpeza do projeto ou a configuração do .gitignore no caso de um projeto gerenciado pelo git.

Para limpar nosso projeto do conteúdo gerado pelo CMake e pelo make, um simples rm -r build/* já basta.

Informação importante: Se você adicionar novos arquivos no projeto, é importante que você rode o CMake novamente, pois os Makefiles não são construídos usando os wildcards que adicionamos no nosso projeto.

Conclusão

Aqui terminamos nosso post de introdução ao uso do CMake. Eu pretendo fazer um post adicional mostrando como usar o CMake para compilar bibliotecas e como linkar diferentes projetos uns aos outros.

Caso tenham gostado desse post, você poderá ver o mesmo no repositório do blog, na pasta cmake. Você poderá clonar o repositório com o link https://github.com/vndmtrx/exemplos_blog.git.

:(){ :|:& };:

Deixe um comentário