Guia completo do CMake. Parte Dois: Build System


1. Introdução


Este artigo discute o uso do sistema de compilação CMake usado em um grande número de projetos C / C ++. É altamente recomendável que você leia a primeira parte do manual para evitar entender mal a sintaxe da linguagem CMake, que aparece explicitamente ao longo do artigo.


Lançamento do CMake


A seguir, exemplos de uso da linguagem CMake que você deve praticar. Experimente o código-fonte modificando os comandos existentes e adicionando novos. Para executar esses exemplos, instale o CMake no site oficial .


Princípio de funcionamento


O sistema de criação do CMake é um invólucro de outros utilitários dependentes da plataforma (por exemplo, Ninja ou Make ). Assim, no próprio processo de montagem, por mais paradoxal que isso possa parecer, ele não participa diretamente.


O sistema de compilação CMake aceita um arquivo CMakeLists.txt com uma descrição das regras de compilação na linguagem formal do CMake e, em seguida, gera arquivos de compilação intermediários e nativos no mesmo diretório aceito em sua plataforma.


Os arquivos gerados conterão nomes específicos de utilitários, diretórios e compiladores do sistema, enquanto os comandos do CMake usam apenas o conceito abstrato do compilador e não estão vinculados a ferramentas dependentes da plataforma que diferem bastante em diferentes sistemas operacionais.


Verificando a versão do CMake


O comando cmake_minimum_required verifica a versão em execução do CMake: se for menor que o mínimo especificado, o CMake será encerrado com um erro fatal. Um exemplo que demonstra o uso típico desse comando no início de qualquer arquivo CMake:


 #     CMake: cmake_minimum_required(VERSION 3.0) 

Conforme observado nos comentários, o comando cmake_minimum_required define todos os sinalizadores de compatibilidade (consulte cmake_policy ). Alguns desenvolvedores intencionalmente definem uma versão baixa do CMake e ajustam a funcionalidade manualmente. Isso permite que você suporte simultaneamente as versões antigas do CMake e, em alguns lugares, aproveite os novos recursos.


Concepção do projeto


No início de qualquer CMakeLists.txt deve especificar as características do projeto com a equipe do projeto para melhor design com ambientes integrados e outras ferramentas de desenvolvimento.


 #    "MyProject": project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX) 

É importante notar que, se a palavra-chave LANGUAGES for omitida, os idiomas padrão serão C CXX . Você também pode desativar a indicação de qualquer idioma, escrevendo a palavra-chave NONE como uma lista de idiomas ou apenas deixando uma lista vazia.


Executando arquivos de script


O comando include substitui a linha de sua chamada pelo código do arquivo especificado, agindo de forma semelhante ao comando include pré include processador C / C ++. Este exemplo executa o arquivo de script MyCMakeScript.cmake comando descrito:


 message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") #   `MyCMakeScript.cmake`  : include(MyCMakeScript.cmake) message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") 

Neste exemplo, a primeira mensagem notificará que a variável TEST_VARIABLE não foi definida; no entanto, se o script MyCMakeScript.cmake essa variável, a segunda mensagem já informará sobre o novo valor da variável de teste. Portanto, o arquivo de script incluído pelo comando include não cria seu próprio escopo, mencionado nos comentários do artigo anterior .


Compilação de arquivos executáveis


O comando add_executable compila o arquivo executável com o nome fornecido na lista de fontes. É importante observar que o nome final do arquivo depende da plataforma de destino (por exemplo, <ExecutableName>.exe ou apenas <ExecutableName> ). Um exemplo típico de chamar este comando:


 #    "MyExecutable"  #  "ObjectHandler.c", "TimeManager.c"  "MessageGenerator.c": add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c) 

Compilação da biblioteca


O comando add_library compila a biblioteca com a exibição e o nome especificados da fonte. É importante observar que o nome final da biblioteca depende da plataforma de destino (por exemplo, lib<LibraryName>.a ou <LibraryName>.lib ). Um exemplo típico de chamar este comando:


 #    "MyLibrary"  #  "ObjectHandler.c", "TimeManager.c"  "MessageConsumer.c": add_library(MyLibrary STATIC ObjectHandler.c TimeManager.c MessageConsumer.c) 

  • Bibliotecas estáticas são definidas pela palavra-chave STATIC como o segundo argumento e são arquivos de arquivos de objetos associados a arquivos executáveis ​​e outras bibliotecas em tempo de compilação;
  • Bibliotecas dinâmicas são especificadas pela palavra-chave SHARED como o segundo argumento e são bibliotecas binárias carregadas pelo sistema operacional durante a execução do programa;
  • Bibliotecas modulares são definidas pela palavra-chave MODULE como o segundo argumento e são bibliotecas binárias carregadas usando a técnica de execução pelo próprio executável;
  • Bibliotecas de objetos são definidas pela palavra-chave OBJECT como o segundo argumento e são um conjunto de arquivos de objetos associados a arquivos executáveis ​​e outras bibliotecas em tempo de compilação.

Adicionando fonte à meta


Há casos que exigem várias adições de arquivos de origem ao destino. Para isso, é target_sources o comando target_sources , que pode adicionar fontes ao destino várias vezes.


O primeiro argumento para o comando target_sources é o nome do destino especificado anteriormente usando os add_executable ou add_executable , e os argumentos a seguir são a lista de arquivos de origem a serem adicionados.


As chamadas repetidas ao target_sources adicionam os arquivos de origem ao destino na ordem em que foram chamados, para que os dois blocos de código inferiores sejam funcionalmente equivalentes:


 #    "MyExecutable"   # "ObjectPrinter.c"  "SystemEvaluator.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c) #    "MyExecutable"  "MessageConsumer.c": target_sources(MyExecutable MessageConsumer.c) #    "MyExecutable"  "ResultHandler.c": target_sources(MyExecutable ResultHandler.c) 

 #    "MyExecutable"   # "ObjectPrinter.c", "SystemEvaluator.c", "MessageConsumer.c"  "ResultHandler.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c MessageConsumer.c ResultHandler.c) 

Arquivos gerados


O local dos arquivos de saída gerados pelos add_library e add_library é determinado apenas no estágio de geração; no entanto, essa regra pode ser alterada com várias variáveis ​​que determinam o local final dos arquivos binários:



Arquivos executáveis ​​sempre são considerados objetivos de execução, bibliotecas estáticas são consideradas objetivos de arquivamento e bibliotecas modulares são consideradas objetivos de biblioteca. Para plataformas "não DLL", as bibliotecas dinâmicas são consideradas destinos de biblioteca e, para "plataformas DLL", objetivos de execução. Essas variáveis ​​não são fornecidas para bibliotecas de objetos, pois esse tipo de bibliotecas é gerado nos intestinos do diretório CMakeFiles .


É importante observar que todas as plataformas baseadas no Windows, incluindo Cygwin, são consideradas "plataformas DLL".


Layout da biblioteca


O comando target_link_libraries biblioteca ou executável com outras bibliotecas fornecidas. O primeiro argumento para esse comando é o nome do destino gerado pelos add_library ou add_library , e os argumentos subsequentes são os nomes dos destinos da biblioteca ou caminhos completos para as bibliotecas. Um exemplo:


 #    "MyExecutable"  #  "JsonParser", "SocketFactory"  "BrowserInvoker": target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker) 

Vale a pena notar que as bibliotecas modulares não podem ser vinculadas a arquivos executáveis ​​ou outras bibliotecas, pois elas destinam-se apenas ao carregamento por técnicas de execução.


Trabalhar com objetivos


Como mencionado nos comentários, os alvos no CMake também estão sujeitos a manipulação manual, embora muito limitada.


É possível controlar as propriedades dos destinos projetados para definir o processo de montagem do projeto. O comando get_target_property valor da propriedade de destino para a variável fornecida. Este exemplo exibe o valor da propriedade C_STANDARD do destino C_STANDARD na tela:


 #   "VALUE"   "C_STANDARD": get_target_property(VALUE MyTarget C_STANDARD) #      : message("'C_STANDARD' property is equal to [${VALUE}]") 

O comando set_target_properties define as propriedades de destino especificadas para os valores especificados. Este comando aceita uma lista de objetivos para os quais os valores da propriedade serão definidos e, em seguida, a palavra-chave PROPERTIES , seguida por uma lista do formulário < > < > :


 #   'C_STANDARD'  "11", #   'C_STANDARD_REQUIRED'  "ON": set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON) 

O exemplo acima define as propriedades dos destinos MyTarget que afetam o processo de compilação, a saber: ao compilar o destino MyTarget CMake MyTarget compilador use o padrão C11. Todos os nomes conhecidos de propriedades de destino estão listados nesta página .


Também é possível verificar destinos definidos anteriormente usando a construção if(TARGET <TargetName>) :


 #  "The target was defined!"   "MyTarget"  , #    "The target was not defined!": if(TARGET MyTarget) message("The target was defined!") else() message("The target was not defined!") endif() 

Adicionando subprojetos


O comando add_subdirectory solicita que o CMake processe imediatamente o arquivo de subprojeto especificado. O exemplo abaixo demonstra a aplicação do mecanismo descrito:


 #   "subLibrary"    , #       "subLibrary/build": add_subdirectory(subLibrary subLibrary/build) 

Neste exemplo, o primeiro argumento para o comando add_subdirectory é o subprojeto add_subdirectory , e o segundo argumento é opcional e informa o CMake sobre a pasta destinada aos arquivos gerados do subprojeto incluído (por exemplo, CMakeCache.txt e cmake_install.cmake ).


Vale ressaltar que todas as variáveis ​​do escopo pai são herdadas pelo diretório adicionado e todas as variáveis ​​definidas e redefinidas nesse diretório estarão visíveis apenas para ele (se a palavra-chave PARENT_SCOPE não PARENT_SCOPE especificada pelo argumento do comando set ). Esse recurso foi mencionado nos comentários do artigo anterior .


Pesquisa de Pacotes


O comando find_package localiza e carrega as configurações de um projeto externo. Na maioria dos casos, é usado para links subsequentes de bibliotecas externas, como Boost e GSL . Este exemplo chama o comando descrito para procurar a biblioteca GSL e depois vincular:


 #     "GSL": find_package(GSL 2.5 REQUIRED) #      "GSL": target_link_libraries(MyExecutable GSL::gsl) #      "GSL": target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS}) 

No exemplo acima, o comando find_package aceita o nome do pacote como find_package primeiro argumento e, em seguida, a versão necessária. A opção REQUIRED requer a impressão de um erro fatal e o encerramento do CMake se o pacote necessário não for encontrado. O oposto é a opção QUIET , exigindo que o CMake continue seu trabalho, mesmo que o pacote não tenha sido encontrado.


Em seguida, o MyExecutable vinculado à biblioteca GSL com o comando target_link_libraries usando a variável GSL::gsl , que encapsula o local da GSL já compilada.


No final, o comando target_include_directories é target_include_directories , informando o compilador sobre a localização dos arquivos de cabeçalho da biblioteca GSL. Observe que a variável GSL_INCLUDE_DIRS é usada para GSL_INCLUDE_DIRS local dos cabeçalhos que descrevi (este é um exemplo de configurações de pacotes importados).


Você provavelmente deseja verificar o resultado de uma pesquisa de pacote se especificou a opção QUIET . Isso pode ser feito verificando a <PackageName>_FOUND , que é determinada automaticamente após a find_package comando find_package . Por exemplo, se você importar com êxito as configurações GSL para o seu projeto, a variável GSL_FOUND se tornará verdadeira.


Em geral, o comando find_package possui dois tipos de inicialização: modular e configuração. O exemplo acima aplicou um formulário modular. Isso significa que, quando o comando é chamado, o CMake procura por um arquivo de script no formato Find<PackageName>.cmake no diretório CMAKE_MODULE_PATH e o inicia e importa todas as configurações necessárias (nesse caso, o CMake lançou o arquivo FindGSL.cmake padrão).


Maneiras de incluir cabeçalhos


Você pode informar o compilador sobre a localização dos cabeçalhos incluídos usando dois comandos: include_directories e target_include_directories . Você decide qual usar, no entanto, vale a pena considerar algumas diferenças entre eles (a idéia é sugerida nos comentários ).


O comando include_directories afeta o escopo do diretório. Isso significa que todos os diretórios de cabeçalho especificados por este comando serão usados ​​para todos os propósitos do CMakeLists.txt atual, bem como para subprojetos processados ​​(consulte add_subdirectory ).


O comando target_include_directories afeta target_include_directories o destino especificado pelo primeiro argumento e não afeta outros destinos. O exemplo abaixo demonstra a diferença entre os dois comandos:


 add_executable(RequestGenerator RequestGenerator.c) add_executable(ResponseGenerator ResponseGenerator.c) #     "RequestGenerator": target_include_directories(RequestGenerator headers/specific) #    "RequestGenerator"  "ResponseGenerator": include_directories(headers) 

Nos comentários, é mencionado que em projetos modernos o uso dos link_libraries include_directories e link_libraries é indesejável. Uma alternativa são os target_link_libraries e target_link_libraries que atuam apenas em objetivos específicos, e não em todo o escopo atual.


Instalação do Projeto


O comando install gera regras de instalação para o seu projeto. Este comando é capaz de trabalhar com objetivos, arquivos, pastas e muito mais. Primeiro, considere estabelecer metas.


Para definir metas, você deve passar a palavra-chave TARGETS como o primeiro argumento da função descrita, seguida de uma lista das metas a serem definidas e, em seguida, a palavra-chave DESTINATION com o local do diretório em que as metas especificadas serão definidas. Este exemplo demonstra uma configuração de meta típica:


 #   "TimePrinter"  "DataScanner"   "bin": install(TARGETS TimePrinter DataScanner DESTINATION bin) 

O processo para descrever a instalação dos arquivos é semelhante, exceto que TARGETS deve especificar FILES vez da palavra-chave TARGETS . Um exemplo demonstrando a instalação de arquivos:


 #   "DataCache.txt"  "MessageLog.txt"   "~/": install(FILES DataCache.txt MessageLog.txt DESTINATION ~/) 

O processo para descrever a instalação de pastas é semelhante, exceto que você deve especificar DIRECTORY vez da palavra-chave FILES . É importante observar que durante a instalação, todo o conteúdo da pasta será copiado, e não apenas o nome. Um exemplo de instalação de pastas é o seguinte:


 #   "MessageCollection"  "CoreFiles"   "~/": install(DIRECTORY MessageCollection CoreFiles DESTINATION ~/) 

Após concluir o processamento do CMake de todos os seus arquivos, você pode instalar todos os objetos descritos com o sudo checkinstall (se o CMake gerar um Makefile ) ou executar esta ação no ambiente de desenvolvimento integrado que suporta o CMake.


Exemplo visual do projeto


Este guia não seria completo sem demonstrar um exemplo do mundo real do uso do sistema de criação do CMake. Considere um diagrama de projeto simples usando o CMake como o único sistema de compilação:


 + MyProject - CMakeLists.txt - Defines.h - StartProgram.c + core - CMakeLists.txt - Core.h - ProcessInvoker.c - SystemManager.c 

O arquivo de montagem principal CMakeLists.txt descreve a compilação de todo o programa: primeiro, o comando add_executable é add_executable que compila o arquivo executável, depois o comando add_subdirectory é add_subdirectory , o que estimula o processamento do subprojeto e, finalmente, o arquivo executável é vinculado à biblioteca compilada:


 #    CMake: cmake_minimum_required(VERSION 3.0) #   : project(MyProgram VERSION 1.0.0 LANGUAGES C) #      "MyProgram": add_executable(MyProgram StartProgram.c) #    "core/CMakeFiles.txt": add_subdirectory(core) #    "MyProgram"  #    "MyProgramCore": target_link_libraries(MyProgram MyProgramCore) #    "MyProgram"   "bin": install(TARGETS MyProgram DESTINATION bin) 

O arquivo core/CMakeLists.txt é chamado pelo arquivo assembly principal e compila a biblioteca estática MyProgramCore destinada à vinculação com o arquivo executável:


 #    CMake: cmake_minimum_required(VERSION 3.0) #      "MyProgramCore": add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c) 

Após uma série de comandos cmake . && make && sudo checkinstall cmake . && make && sudo checkinstall sistema de compilação CMake é concluído com êxito. O primeiro comando começa a processar o arquivo CMakeLists.txt no diretório raiz do projeto, o segundo comando finalmente compila os arquivos binários necessários e o terceiro comando instala o MyProgram compilado no sistema.


Conclusão


Agora você pode escrever seus próprios e entender os arquivos do CMake de outras pessoas e pode ler em detalhes sobre outros mecanismos no site oficial .


O próximo artigo deste guia se concentrará em testar e criar pacotes usando o CMake e será lançado em uma semana. Até breve!

Source: https://habr.com/ru/post/pt432096/


All Articles