A06 -Sistemas de compilação baseados em alvos com CMake
Questões
Como podemos lidar com projetos mais complexos com o CMake?
O que exatamente são alvos (targets) na linguagem específica de domínio (DSL) do CMake?
Objetivos
Aprenda que os elementos básicos no CMake não são variáveis, mas alvos.
Saiba mais sobre as propriedades dos alvos e como usá-los.
Aprenda a usar níveis de visibilidade para expressar dependências entre alvos.
Saiba como trabalhar com projetos que abrangem várias pastas.
Saiba como lidar com multiplos alvos em um projeto.
Projetos do mundo real exigem mais do que compilar alguns arquivos fontes
em executáveis e/ou bibliotecas. Na grande maioria dos casos, você se deparará
com projetos que compreendem centenas de arquivos fontes espalhados em uma estrutura
complexa. O uso do CMake ajuda a manter a complexidade do sistema de compilação
sob controle.
Com o advento do CMake 3.0, também conhecido como Modern CMake, houve uma mudança
significativa na forma como a linguagem específica de domínio (DSL) do
CMake é estruturada. Em vez de depender de variáveis para transmitir
informações em um projeto, devemos passar a usar alvos e propriedades.
Um alvo é declarado por add_executable ou add_library: assim,
em termos gerais, um destino mapeia para um artefato de construção
no projeto. [1]
Qualquer destino tem uma coleção de propriedades, que definem como o
artefato de compilação deve ser produzido ecomo ele deve ser usado
por outros destinos dependentes no projeto.
Um alvo é o elemento básico no CMake DSL. Cada destino possui propriedades,
que podem ser lidas com get_target_property e modificado
com set_target_properties. Opções de compilação, definições,
diretórios de inclusão, arquivos de origem, bibliotecas de links e
opções de links são propriedades dos alvos.
É muito mais robusto usar alvos e propriedades do que usar variáveis.
Dado um alvo tgtA, podemos invocar um comando na família target_* como:
O uso dos níveis de visibilidade podem ser os seguintes:
PRIVATE. A propriedade só será usada para construir o alvo dado
como primeiro argumento. Em nosso pseudo-código, tgtB será usado
apenas para construir tgtA mas não será propagado como uma
dependência para outros alvos consumindo tgtA.
INTERFACE. A propriedade será usada apenas para construir destinos que
consumam o alvo fornecido como primeiro argumento. Em nosso pseudo-código,
o tgtC só será propagado como uma dependência para outros alvos que
consomem tgtA.
PUBLIC. A propriedade será usada em ambos para construir o destino
fornecido como o primeiro argumento e os destinos que o consomem.
Em nosso pseudo-código, tgtD será usado para construir tgtA e
será propagado como uma dependência para qualquer outro alvo
que consuma tgtA.
As propriedades dos alvos têm níveis de visibilidade que determinam
como o CMake deve propagá-las entre alvos interdependentes.
Os cinco comandos mais usados para lidar com alvos são:
Até agora vimos que você pode definir propriedades em alvos,
mas também em testes (veja A03 - Criando e executando testes com o CTest).
O CMake permite definir propriedades em vários níveis diferentes
de visibilidade em todo o projeto:
Escopo Global. Elas são equivalentes às variáveis definidas na raiz CMakeLists.txt. Seu uso é, no entanto,
mais poderoso, pois eles podem ser definidos a partir
de qualquer folha CMakeLists.txt.
Escopo do diretório. Elas são equivalentes a variáveis definidas em uma determinada folha CMakeLists.txt.
Target. Essas são as propriedades definidas nos alvos que discutimos acima.
Teste.
Arquivos Fontes. Por exemplo, flags do compilador.
Entradas de cache.
Arquivos instalados.
Para obter uma lista completa de propriedades conhecidas pelo CMake:
$cmake--help-properties|less
Você pode obter o valor atual de qualquer propriedade com:
O script CMakeLists.txt raiz conterá a invocação do comando project: variáveis e alvos declarados na raiz têm escopo efetivamente global.
Lembre-se também que PROJECT_SOURCE_DIR apontará para a pasta que contém o CMakeLists.txt raiz.
Para mover-se entre a raiz e uma folha ou entre folhas,
você usará o comando add_subdirectory:
Normalmente, você só precisa passar o primeiro argumento: a pasta dentro
da árvore de compilação será calculada automaticamente pelo CMake.
Podemos declarar alvos em qualquer nível, não necessariamente na raiz:
um alvo é visível no nível em que é declarado e em todos os níveis superiores.
Exercício 21: Autômatos celulares
Vamos além do “Hello, world” e trabalharemos em um projeto que abrange
várias pastas. Implementaremos um código relativamente simples para
calcular e imprimir na tela autômatos celulares elementares.
Separamos as fontes em src e external para simular um projeto
aninhado que reutiliza um projeto externo.
Seu objetivo é:
Construa uma biblioteca a partir do conteúdo de external e de
cada subpasta de src. Use add_library junto com target_sources e,
para C++, target_include_directories. Pense cuidadosamente sobre
os níveis de visibilidade.
Compile o executável principal. Onde ele está localizado na árvore
de construção? Lembre-se de que o CMake gera uma árvore de compilação
que espelha a árvore de origem.
O executável aceitará 3 argumentos: o comprimento,
o número de passos e a regra do autômato.
Você pode executá-lo com:
$automata40530
Esta é a saída:
length: 40
number of steps: 5
rule: 30
*
***
** *
** ****
** * *
** **** ***
O projeto base está em source/code/day-2/21_automata-cxx.
As fontes estão organizadas na segunte árvore:
A fonte empty.f90 declara, como o nome sugere, um módulo
Fortran vazio. Este módulo é usado apenas dentro da
subpasta evolution: qual nível de visibilidade ele deve
ter em target_sources?
Observe que o CMake pode entender a ordem de compilação
imposta pelos módulos Fortran sem intervenção adicional.
Onde estão os arquivos .mod?
Um exemplo funcional está na subpasta solution.
Você pode decidir onde executáveis, bibliotecas estáticas
e compartilhadas e arquivos Fortran .mod serão armazenados
na árvore de compilação.
As variáveis relevantes são:
CMAKE_RUNTIME_OUTPUT_DIRECTORY, para executáveis.
CMAKE_ARCHIVE_OUTPUT_DIRECTORY, para bibliotecas estáticas.
CMAKE_LIBRARY_OUTPUT_DIRECTORY, para bibliotecas compartilhadas.
CMAKE_Fortran_MODULE_DIRECTORY, Para arquivos Fortran .mod
Modifique seu CMakeLists.txt para gerar o executável
automata em build/bin` e as bibliotecas em build/lib
A árvore de dependência interna
Você pode visualizar as dependências entre os alvos em seu projeto com o Graphviz:
As dependências entre alvos no projeto de autômatos celulares.
Resumo
Usando alvos, você pode obter controle granular sobre como os
artefatos são criados e como suas dependências são tratadas.
Flags de compilador, definições, arquivos de origem, pastas de inclusão,
bibliotecas de links e opções de vinculador são propriedades de um alvo.
Evite usar variáveis para expressar dependências entre alvos:
use os níveis de visibilidade PRIVATE, INTERFACE, PUBLIC e
deixe o CMake descobrir os detalhes.