CMake e C ++ - irmãos para sempre

Amizade para sempre


Durante o processo de desenvolvimento, gosto de alterar compiladores, modos de compilação, versões de dependência, executar análises estáticas, medir desempenho, coletar cobertura, gerar documentação etc. E eu realmente amo o CMake, porque me permite fazer tudo o que quero.


Muitos repreendem o CMake, e geralmente com justiça, mas se você olhar, nem tudo é tão ruim, e ultimamente é muito bom , e a direção do desenvolvimento é bastante positiva.


Neste artigo, quero dizer como é simples organizar uma biblioteca de cabeçalho C ++ no sistema CMake para obter a seguinte funcionalidade:


  1. Montagem;
  2. Testes de inicialização automática;
  3. Medição da cobertura do código;
  4. Instalação;
  5. Documentação automática
  6. Geração de sandbox online;
  7. Análise estática

Quem já entende os prós e os fabricantes pode fazer o download do modelo do projeto e começar a usá-lo.


Conteúdo


  1. Projeto de dentro para fora
    1. Estrutura do projeto
    2. Arquivo principal do CMake (./CMakeLists.txt)
      1. Informações do Projeto
      2. Opções do Projeto
      3. Opções de compilação
      4. Objetivo principal
      5. Instalação
      6. Testes
      7. A documentação
      8. Sandbox online
    3. Script para testes (test / CMakeLists.txt)
      1. Teste
      2. Cobertura
    4. Script para documentação (doc / CMakeLists.txt)
    5. Script para uma sandbox online (online / CMakeLists.txt)
  2. Projeto fora
    1. Assembléia
      1. Geração
      2. Assembléia
    2. Opções
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Objetivos da assembléia
      1. Por padrão
      2. mylib-unit-tests
      3. verificar
      4. cobertura
      5. doc
      6. caixa de varinha
    4. Exemplos
  3. As ferramentas
  4. Análise estática
  5. Posfácio


Projeto de dentro para fora



Estrutura do projeto


. ├── CMakeLists.txt ├── README.en.md ├── README.md ├── doc │  ├── CMakeLists.txt │  └── Doxyfile.in ├── include │  └── mylib │  └── myfeature.hpp ├── online │  ├── CMakeLists.txt │  ├── mylib-example.cpp │  └── wandbox.py └── test ├── CMakeLists.txt ├── mylib │  └── myfeature.cpp └── test_main.cpp 

Falaremos principalmente sobre como organizar os scripts do CMake, para que sejam analisados ​​em detalhes. Todos podem ver o restante dos arquivos diretamente na página do modelo do projeto .



Arquivo principal do CMake (./CMakeLists.txt)



Informações do Projeto


Primeiro de tudo, você precisa solicitar a versão correta do sistema CMake. O CMake está evoluindo, as assinaturas da equipe, o comportamento em diferentes condições estão mudando. Para que o CMake entenda imediatamente o que queremos dele, precisamos corrigir imediatamente nossos requisitos para ele.


 cmake_minimum_required(VERSION 3.13) 

Em seguida, designamos nosso projeto, seu nome, versão, idiomas usados ​​etc. (consulte project ).


Nesse caso, especificamos a linguagem CXX (que significa C ++) para que o CMake não force e não procure o compilador da linguagem C (por padrão, dois idiomas estão incluídos no CMake: C e C ++).


 project(Mylib VERSION 1.0 LANGUAGES CXX) 

Aqui você pode verificar imediatamente se nosso projeto está incluído em outro projeto como um subprojeto. Isso ajudará muito no futuro.


 get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY) 


Opções do Projeto


Nós fornecemos duas opções.


A primeira opção - MYLIB_TESTING - para desativar os testes de unidade. Isso pode ser necessário se tivermos certeza de que tudo está em ordem com os testes e queremos, por exemplo, apenas instalar ou empacotar nosso projeto. Ou nosso projeto é incluído como um subprojeto - nesse caso, o usuário do nosso projeto não está interessado em executar nossos testes. Você não testa as dependências que você usa?


 option(MYLIB_TESTING "  " ON) 

Além disso, faremos uma opção separada MYLIB_COVERAGE para medir a cobertura do código com testes, mas serão necessárias ferramentas adicionais, portanto você precisará ativá-lo explicitamente.


 option(MYLIB_COVERAGE "    " OFF) 


Opções de compilação


É claro que somos programadores legais e esperamos o nível máximo de diagnóstico de tempo de compilação do compilador. Nem um único mouse passará.


 add_compile_options( -Werror -Wall -Wextra -Wpedantic -Wcast-align -Wcast-qual -Wconversion -Wctor-dtor-privacy -Wenum-compare -Wfloat-equal -Wnon-virtual-dtor -Wold-style-cast -Woverloaded-virtual -Wredundant-decls -Wsign-conversion -Wsign-promo ) 

Desabilitaremos também as extensões para cumprir totalmente com o padrão da linguagem C ++. Por padrão, eles estão incluídos no CMake.


 if(NOT CMAKE_CXX_EXTENSIONS) set(CMAKE_CXX_EXTENSIONS OFF) endif() 


Objetivo principal


Nossa biblioteca consiste apenas em arquivos de cabeçalho, o que significa que não temos exaustão na forma de bibliotecas estáticas ou dinâmicas. Por outro lado, para usar nossa biblioteca de fora, é necessário instalá-la, ser capaz de encontrá-la no sistema e conectá-la ao seu projeto e, ao mesmo tempo, esses mesmos cabeçalhos, bem como, possivelmente, alguns adicionais propriedades.


Para esse fim, criamos uma biblioteca de interfaces.


 add_library(mylib INTERFACE) 

Vincule os cabeçalhos à nossa biblioteca front-end.


O uso moderno, elegante e jovem do CMake implica que cabeçalhos, propriedades etc. transmitido através de uma única finalidade. Portanto, basta dizer target_link_libraries(target PRIVATE dependency) e todos os cabeçalhos associados ao destino de dependency estarão disponíveis para fontes pertencentes ao target . E nenhum [target_]include_directories é necessário. Isso será demonstrado abaixo ao analisar o script CMake para testes de unidade .


Também vale a pena prestar atenção aos chamados -: $<...> .


Este comando associa os cabeçalhos necessários à nossa biblioteca front-end e, se a nossa biblioteca estiver conectada a um destino na mesma hierarquia do CMake, os cabeçalhos do diretório ${CMAKE_CURRENT_SOURCE_DIR}/include serão associados a ele e, se o nosso Como a biblioteca está instalada no sistema e conectada a outro projeto usando o find_package , os cabeçalhos do diretório de include relativos ao diretório de instalação serão associados a ele.


 target_include_directories(mylib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) 

Defina o padrão do idioma. Claro, o último. Ao mesmo tempo, não apenas incluímos o padrão, mas também o distribuímos para aqueles que usarão nossa biblioteca. Isso é alcançado devido ao fato de a propriedade set ter a categoria INTERFACE (consulte o comando target_compile_features ).


 target_compile_features(mylib INTERFACE cxx_std_17) 

Criamos um alias para nossa biblioteca. Além disso, para a beleza, ele estará em um "espaço para nome" especial. Isso será útil quando diferentes módulos aparecerem em nossa biblioteca e vamos conectá-los independentemente um do outro. Como no Boost, por exemplo .


 add_library(Mylib::mylib ALIAS mylib) 


Instalação


Instalando nossos cabeçalhos no sistema. Tudo é simples aqui. Dizemos que a pasta com todos os cabeçalhos deve estar no diretório de include relativo ao local da instalação.


 install(DIRECTORY include/mylib DESTINATION include) 

Em seguida, informamos o sistema de compilação que queremos chamar find_package(Mylib) em projetos de terceiros e obtemos o Mylib::mylib destino.


 install(TARGETS mylib EXPORT MylibConfig) install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake) 

O próximo feitiço deve ser entendido da seguinte forma. Quando chamamos find_package(Mylib 1.2.3 REQUIRED) em um projeto de terceiros e, nesse caso, a versão real da biblioteca instalada será incompatível com a versão 1.2.3 , o CMake gerará automaticamente um erro. Ou seja, você não precisará seguir as versões manualmente.


 include(CMakePackageConfigHelpers) write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY AnyNewerVersion ) install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake) 


Testes


Se os testes forem desativados explicitamente usando a opção apropriada ou se o nosso projeto for um subprojeto, ou seja, conectado a outro projeto do CMake usando o add_subdirectory , não iremos mais além na hierarquia, e o script que descreve os comandos para gerar e executar os testes simplesmente não será iniciado .


 if(NOT MYLIB_TESTING) message(STATUS "  Mylib ") elseif(IS_SUBPROJECT) message(STATUS "Mylib     ") else() add_subdirectory(test) endif() 


A documentação


A documentação também não será gerada no caso de um subprojeto.


 if(NOT IS_SUBPROJECT) add_subdirectory(doc) endif() 


Sandbox online


Da mesma forma, o subprojeto também não terá caixas de proteção online.


 if(NOT IS_SUBPROJECT) add_subdirectory(online) endif() 


Script para testes (test / CMakeLists.txt)



Teste


Primeiro de tudo, encontramos o pacote com a estrutura de teste desejada (substitua-a pela sua favorita).


 find_package(doctest 2.3.3 REQUIRED) 

Criamos nosso arquivo executável com testes. Normalmente, adiciono apenas o arquivo no qual a função main estará diretamente no binário executável.


 add_executable(mylib-unit-tests test_main.cpp) 

E os arquivos que descrevem os próprios testes são adicionados mais tarde. Mas isso não é necessário.


 target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp) 

Conectamos dependências. Observe que target_include_directories apenas target_include_directories destinos target_include_directories CMake que precisávamos ao nosso binário e não chamamos o comando target_include_directories . Os cabeçalhos da estrutura de teste e do nosso Mylib::mylib , bem como os parâmetros de construção (no nosso caso, esse é o padrão da linguagem C ++) rastrearam junto com esses objetivos.


 target_link_libraries(mylib-unit-tests PRIVATE Mylib::mylib doctest::doctest ) 

Por fim, crie um destino fictício, cujo "assembly" seja equivalente à execução de testes e inclua esse destino no assembly padrão (o atributo ALL é responsável por isso). Isso significa que a montagem, por padrão, inicia o lançamento dos testes, ou seja, nunca esqueceremos de executá-los.


 add_custom_target(check ALL COMMAND mylib-unit-tests) 


Cobertura


Em seguida, habilitamos a medição da cobertura do código, se a opção correspondente for especificada. Não vou entrar em detalhes, porque eles se relacionam mais com a ferramenta para medir a cobertura do que com o CMake. É importante observar que, com base nos resultados, será criada uma meta de coverage , com a qual é conveniente começar a medir a cobertura.


 find_program(GCOVR_EXECUTABLE gcovr) if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE) message(STATUS "    ") target_compile_options(mylib-unit-tests PRIVATE --coverage) target_link_libraries(mylib-unit-tests PRIVATE gcov) add_custom_target(coverage COMMAND ${GCOVR_EXECUTABLE} --root=${PROJECT_SOURCE_DIR}/include/ --object-directory=${CMAKE_CURRENT_BINARY_DIR} DEPENDS check ) elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE) set(MYLIB_COVERAGE OFF) message(WARNING "       gcovr") endif() 


Script para documentação (doc / CMakeLists.txt)


Doxigênio encontrado .


 find_package(Doxygen) 

Em seguida, verificamos se o usuário definiu a variável de idioma. Se sim, então não toque, se não, então pegue russo. Em seguida, configure os arquivos do sistema Doxygen. Todas as variáveis ​​necessárias, incluindo o idioma, chegam lá durante o processo de configuração (consulte o configure_file ).


Em seguida, criamos o destino do doc , que iniciará a geração da documentação. Como a geração de documentação não é a maior necessidade no processo de desenvolvimento, por padrão o objetivo não será habilitado, ele deverá ser iniciado explicitamente.


 if (Doxygen_FOUND) if (NOT MYLIB_DOXYGEN_LANGUAGE) set(MYLIB_DOXYGEN_LANGUAGE Russian) endif() message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}") configure_file(Doxyfile.in Doxyfile) add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) endif () 


Script para uma sandbox online (online / CMakeLists.txt)


Aqui encontramos o terceiro Python e criamos um destino wandbox que gera uma solicitação que corresponde à API do serviço Wandbox e a envia. Em resposta, é fornecido um link para a sandbox concluída.


 find_program(PYTHON3_EXECUTABLE python3) if(PYTHON3_EXECUTABLE) set(WANDBOX_URL "https://wandbox.org/api/compile.json") add_custom_target(wandbox COMMAND ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include | curl -H "Content-type: application/json" -d @- ${WANDBOX_URL} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS mylib-unit-tests ) else() message(WARNING "  -    python 3- ") endif() 


Projeto fora


Agora considere como usar tudo.



Assembléia


A montagem deste projeto, como qualquer outro projeto no sistema de montagem do CMake, consiste em dois estágios:



Geração


 cmake -S // -B /// [ ...] 

Se o comando acima não funcionou devido à versão antiga do CMake, tente omitir -S :
 cmake // -B /// [ ...] 

Mais sobre opções .



Montagem do projeto


 cmake --build /// [--target target] 

Leia mais sobre metas de montagem .



Opções



MYLIB_COVERAGE


 cmake -S ... -B ... -DMYLIB_COVERAGE=ON [  ...] 

Inclui uma meta de coverage , com a qual você pode começar a medir a cobertura do código com testes.



MYLIB_TESTING


 cmake -S ... -B ... -DMYLIB_TESTING=OFF [  ...] 

Fornece a capacidade de desligar o conjunto de teste da unidade e o alvo de check . Como resultado, a medição da cobertura do código por testes é desativada (consulte MYLIB_COVERAGE ).


Além disso, o teste é desativado automaticamente se o projeto estiver conectado a outro projeto como um subprojeto usando o add_subdirectory .



MYLIB_DOXYGEN_LANGUAGE


 cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [  ...] 

Alterna o idioma da documentação que o destino do doc gera para o especificado. Para obter uma lista dos idiomas disponíveis, consulte o site da Doxygen .


Por padrão, o russo está ativado.



Objetivos da assembléia



Por padrão


 cmake --build path/to/build/directory cmake --build path/to/build/directory --target all 

Se o alvo não for especificado (o que equivale ao objetivo de all ), coleciona tudo o que é possível e também chama o alvo de check .



mylib-unit-tests


 cmake --build path/to/build/directory --target mylib-unit-tests 

Compila testes de unidade. Ativado por padrão.



verificar


 cmake --build /// --target check 

Executa testes de unidade coletados (coleta, se ainda não). Ativado por padrão.


Veja também mylib-unit-tests .



cobertura


 cmake --build /// --target coverage 

Analisa a execução (executa, se ainda não) de testes de unidade para cobrir o código com testes usando o programa gcovr .


A exaustão do revestimento será mais ou menos assim:


 ------------------------------------------------------------------------------ GCC Code Coverage Report Directory: /path/to/cmakecpptemplate/include/ ------------------------------------------------------------------------------ File Lines Exec Cover Missing ------------------------------------------------------------------------------ mylib/myfeature.hpp 2 2 100% ------------------------------------------------------------------------------ TOTAL 2 2 100% ------------------------------------------------------------------------------ 

O destino está disponível apenas quando MYLIB_COVERAGE .


Veja também check .



doc


 cmake --build /// --target doc 

Inicia a geração da documentação do código usando o sistema Doxygen .



caixa de varinha


 cmake --build /// --target wandbox 

A resposta do serviço é mais ou menos assim:


 { "permlink" : "QElvxuMzHgL9fqci", "status" : "0", "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci" } 

Para fazer isso, use o serviço Wandbox . Não sei como eles têm servidores de borracha, mas acho que essa oportunidade não deve ser abusada.



Exemplos


Montagem do projeto no modo de depuração com medição de cobertura


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON cmake --build /// --target coverage --parallel 16 

Instalação do projeto sem montagem e teste preliminares


 cmake -S // -B /// -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/// cmake --build /// --target install 

Construir no modo de liberação pelo compilador especificado


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=///// cmake --build /// --parallel 4 

Geração de documentação em inglês


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English cmake --build /// --target doc 


As ferramentas


  1. CMake 3.13


    De fato, o CMake 3.13 é necessário apenas para executar alguns dos comandos do console descritos nesta ajuda. Do ponto de vista da sintaxe dos scripts do CMake, a versão 3.8 é suficiente se a geração for chamada de outras maneiras.


  2. Biblioteca de testes Doctest


    O teste pode ser desativado (consulte a MYLIB_TESTING ).


  3. Doxygen


    Para alternar o idioma em que a documentação será gerada, a opção MYLIB_DOXYGEN_LANGUAGE é MYLIB_DOXYGEN_LANGUAGE .


  4. Intérprete Python 3


    Para gerar automaticamente caixas de proteção online .




Análise estática


Com o CMake e algumas boas ferramentas, você pode fornecer análises estáticas com movimentos mínimos do corpo.


Cppcheck


O CMake possui suporte interno para a ferramenta de análise estática Cppcheck .


Para fazer isso, use a opção CMAKE_CXX_CPPCHECK :


 cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-I///include" 

Depois disso, a análise estática será iniciada automaticamente todas as vezes durante a compilação e recompilação das fontes. Você não precisa fazer nada extra.


Clang


Usando a maravilhosa ferramenta de scan-build , você também pode executar a análise estática em duas contagens:


 scan-build cmake -S // -B /// -DCMAKE_BUILD_TYPE=Debug scan-build cmake --build /// 

Aqui, diferentemente do caso do Cppcheck, você precisa executar a montagem todas as vezes através do scan-build .



Posfácio


O CMake é um sistema muito poderoso e flexível que permite implementar funcionalidades para todos os gostos e cores. E, embora a sintaxe às vezes deixe muito a desejar, o diabo ainda não é tão terrível quanto é pintado. Use o sistema de criação do CMake para o benefício da sociedade e da saúde.




Baixar modelo de projeto

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


All Articles