CMake e C++ são irmãos para sempre

CMake e C++ são irmãos para sempre

Durante o desenvolvimento, gosto de alterar compiladores, construir modos, versões de dependências, realizar análises estáticas, medir desempenho, coletar cobertura, gerar documentação, etc. E eu adoro o CMake porque me permite fazer tudo o que quero.

Muitas pessoas criticam o CMake, e muitas vezes com razão, mas se você olhar bem, nem tudo está tão ruim, e recentemente nada mal, e a direção do desenvolvimento é bastante positiva.

Nesta nota, quero dizer como simplesmente organizar uma biblioteca de cabeçalho em C++ no sistema CMake para obter a seguinte funcionalidade:

  1. Conjunto;
  2. Testes de execução automática;
  3. Medição de cobertura de código;
  4. Instalação;
  5. Autodocumentação;
  6. Geração de sandbox online;
  7. Análise estática.

Quem já entende as vantagens e o C-make pode simplesmente baixar modelo de projeto e comece a usá-lo.


Conteúdo

  1. Projeto por dentro
    1. Estrutura do projeto
    2. Arquivo CMake principal (./CMakeLists.txt)
      1. Informações do Projeto
      2. Opções de projeto
      3. Opções de compilação
      4. O objetivo principal
      5. Instalação
      6. Testes
      7. Documentação
      8. Caixa de areia on-line
    3. Script de teste (test/CMakeLists.txt)
      1. Teste
      2. Покрытие
    4. Script para documentação (doc/CMakeLists.txt)
    5. Script para sandbox online (online/CMakeLists.txt)
  2. Projeto fora
    1. montagem
      1. Geração
      2. montagem
    2. Opções
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Metas de montagem
      1. Por padrão
      2. testes de unidade mylib
      3. verificar
      4. cobertura
      5. doca
      6. caixa de varinhas
    4. Примеры
  3. Ferramentas
  4. Análise estática
  5. Posfácio

Projeto por dentro

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 scripts CMake, portanto eles serão discutidos em detalhes. Qualquer pessoa pode visualizar o restante dos arquivos diretamente na página do projeto modelo.

Arquivo CMake principal (./CMakeLists.txt)

Informações do Projeto

Primeiro de tudo, você precisa solicitar a versão necessária do sistema CMake. O CMake está evoluindo, as assinaturas de comandos e o comportamento em diferentes condições estão mudando. Para que o CMake entenda imediatamente o que queremos dele, precisamos registrar imediatamente nossos requisitos.

cmake_minimum_required(VERSION 3.13)

A seguir designaremos o nosso projeto, seu nome, versão, idiomas utilizados, etc. команду project).

Neste caso indicamos o idioma CXX (e isso significa C++) para que o CMake não se esforce e procure um compilador de linguagem C (por padrão, o CMake inclui duas linguagens: 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 de projeto

Forneceremos duas opções.

A primeira opção é MYLIB_TESTING — para desabilitar testes unitários. Isso pode ser necessário se tivermos certeza de que está tudo em ordem com os testes, mas quisermos apenas, por exemplo, instalar ou empacotar nosso projeto. Ou nosso projeto está incluído como um subprojeto - neste caso, o usuário do nosso projeto não tem interesse em executar nossos testes. Você não testa as dependências que usa, não é?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

Além disso, faremos uma opção separada MYLIB_COVERAGE para medir a cobertura de código por meio de testes, mas exigirá ferramentas adicionais e, portanto, precisará ser habilitado explicitamente.

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Opções de compilação

Claro, somos programadores legais, então queremos o nível máximo de diagnóstico em tempo de compilação do compilador. Nem um único rato escapará.

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
)

Também desativaremos as extensões para cumprir totalmente o padrão da linguagem C++. Eles estão habilitados por padrão no CMake.

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

O objetivo principal

Nossa biblioteca consiste apenas em arquivos de cabeçalho, o que significa que não temos nenhum esgotamento na forma de bibliotecas estáticas ou dinâmicas. Por outro lado, para utilizar externamente nossa biblioteca, ela precisa estar instalada, deve ser detectável no sistema e conectada ao seu projeto, e ao mesmo tempo esses mesmos cabeçalhos, bem como possivelmente alguns adicionais, estão anexados a ele propriedades.

Para isso, criamos uma biblioteca de interfaces.

add_library(mylib INTERFACE)

Vinculamos cabeçalhos à nossa biblioteca de interface.

O uso moderno, moderno e jovem do CMake implica que cabeçalhos, propriedades, etc. transmitido através de um único alvo. Então basta dizer target_link_libraries(target PRIVATE dependency)e todos os cabeçalhos associados ao destino dependency, estará disponível para fontes pertencentes ao destino target. E você não precisa de nenhum [target_]include_directories. Isso será demonstrado abaixo na análise Script CMake para testes unitários.

Também vale a pena prestar atenção aos chamados. выражения-генераторы: $<...>.

Este comando associa os cabeçalhos que precisamos à nossa biblioteca de interface e, se nossa biblioteca estiver conectada a qualquer destino dentro da mesma hierarquia do CMake, os cabeçalhos do diretório serão associados a ela ${CMAKE_CURRENT_SOURCE_DIR}/include, e se nossa biblioteca estiver instalada no sistema e conectada a outro projeto usando o comando find_package, então os cabeçalhos do diretório serão associados a ele include relativo ao diretório de instalação.

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

Vamos definir um padrão de linguagem. Claro, o último. Ao mesmo tempo, não apenas incluímos o padrão, mas também o estendemos a quem utilizará nossa biblioteca. Isso é conseguido devido ao fato da propriedade definida possuir uma categoria INTERFACE (Veja. comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Vamos criar um alias para nossa biblioteca. Além disso, por beleza, estará em um “namespace” especial. Isso será útil quando diferentes módulos aparecerem em nossa biblioteca e formos conectá-los independentemente uns dos outros. Como em Busta, 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 ir para o diretório include em relação ao local de instalação.

install(DIRECTORY include/mylib DESTINATION include)

A seguir, informamos ao sistema de compilação que queremos poder chamar o comando em projetos de terceiros find_package(Mylib) e conseguir um gol Mylib::mylib.

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

O próximo feitiço deve ser entendido desta forma. Quando em um projeto de terceiros chamamos o comando find_package(Mylib 1.2.3 REQUIRED), e a versão real da biblioteca instalada será incompatível com a versão 1.2.3CMake irá gerar automaticamente um erro. Ou seja, você não precisará rastrear 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 desabilitados explicitamente usando opção correspondente ou nosso projeto é um subprojeto, ou seja, está conectado a outro projeto CMake através do comando add_subdirectory, não avançamos na hierarquia e o script que descreve os comandos para gerar e executar testes simplesmente não é executado.

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

Documentação

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

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

Caixa de areia on-line

Da mesma forma, o subprojeto também não terá sandbox online.

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

Script de teste (test/CMakeLists.txt)

Teste

Em primeiro lugar, encontramos um pacote com a estrutura de teste necessária (substitua pela sua preferida).

find_package(doctest 2.3.3 REQUIRED)

Vamos criar nosso arquivo executável com testes. Normalmente adiciono diretamente no binário executável apenas o arquivo que conterá a função main.

add_executable(mylib-unit-tests test_main.cpp)

E adiciono arquivos nos quais os próprios testes são descritos posteriormente. Mas você não precisa fazer isso.

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

Conectamos dependências. Observe que vinculamos apenas os alvos do CMake necessários ao nosso binário e não chamamos o comando target_include_directories. Títulos da estrutura de teste e da nossa Mylib::mylib, bem como parâmetros de construção (no nosso caso, este é o padrão da linguagem C++) vieram junto com esses objetivos.

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

Por fim, criamos um alvo fictício, cuja “construção” equivale à execução de testes, e adicionamos esse alvo ao build padrão (o atributo é responsável por isso ALL). Isso significa que a compilação padrão aciona a execução dos testes, o que significa que nunca esqueceremos de executá-los.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Покрытие

A seguir, habilitamos a medição de cobertura de código se a opção apropriada for especificada. Não vou entrar em detalhes, pois se referem mais a uma ferramenta de medição de cobertura do que ao CMake. É importante apenas ressaltar que com base nos resultados será criada uma meta coverage, com o 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)

A seguir, verificamos se o usuário configurou a variável de idioma. Se sim, então não tocamos nele; se não, então usamos o russo. Em seguida, configuramos os arquivos do sistema Doxygen. Todas as variáveis ​​necessárias, incluindo o idioma, vão para lá durante o processo de configuração (ver. команду configure_file).

Então criamos uma meta doc, que começará a gerar documentação. Como a geração de documentação não é a maior necessidade no processo de desenvolvimento, o alvo não será habilitado por padrão; ele deverá ser lançado 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 sandbox online (online/CMakeLists.txt)

Aqui encontramos o terceiro Python e criamos um alvo wandbox, que gera uma solicitação correspondente à API do serviço caixa de varinha, e o manda embora. A resposta vem com um link para o sandbox finalizado.

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 vamos ver como usar tudo isso.

montagem

A construção deste projeto, como qualquer outro projeto no sistema de construção CMake, consiste em duas etapas:

Geração

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Se o comando acima não funcionou devido a uma versão antiga do CMake, tente omitir -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Mais sobre opções.

Construindo o projeto

cmake --build путь/к/сборочной/директории [--target target]

Mais sobre metas de montagem.

Opções

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Inclui alvo coverage, com o qual você pode começar a medir a cobertura do código por meio de testes.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

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

O teste também será desabilitado automaticamente se o projeto estiver conectado a outro projeto como um subprojeto usando o comando add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Muda o idioma da documentação que o destino gera doc para aquele dado. Para obter uma lista de idiomas disponíveis, consulte Site do sistema Doxygen.

O russo está habilitado por padrão.

Metas de montagem

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 é equivalente ao alvo all), coleta tudo o que pode e também chama o alvo check.

testes de unidade mylib

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

Compila testes unitários. Habilitado por padrão.

verificar

cmake --build путь/к/сборочной/директории --target check

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

См. также mylib-unit-tests.

cobertura

cmake --build путь/к/сборочной/директории --target coverage

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

A exaustão do revestimento ficará 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 alvo só está disponível quando a opção está habilitada MYLIB_COVERAGE.

См. также check.

doca

cmake --build путь/к/сборочной/директории --target doc

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

caixa de varinhas

cmake --build путь/к/сборочной/директории --target wandbox

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

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

O serviço é usado para isso caixa de varinha. Não sei o quão flexíveis são os seus servidores, mas penso que esta oportunidade não deve ser abusada.

Примеры

Crie o projeto em 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

Instalando um projeto sem montagem e testes preliminares

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Construir em modo de lançamento com um determinado compilador

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Gerando documentação em inglês

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

Ferramentas

  1. CMakeName 3.13

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

  2. Biblioteca de testes teste médico

    O teste pode ser desativado (veja опцию MYLIB_TESTING).

  3. Doxygen

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

  4. Intérprete de idiomas Python 3

    Para geração automática caixas de areia on-line.

Análise estática

Com o CMake e algumas boas ferramentas, você pode fornecer análises estáticas com o mínimo de esforço.

Cppverificar

CMake possui suporte integrado para uma ferramenta de análise estática Cppverificar.

Para fazer isso você precisa usar 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 sempre que o código-fonte for compilado e recompilado. Não há necessidade de fazer nada adicional.

Clam

Com a ajuda de uma ferramenta maravilhosa scan-build Você também pode executar análises estáticas rapidamente:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Aqui, diferentemente do caso do Cppcheck, você precisa executar a compilação sempre scan-build.

Posfácio

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 construção CMake para o benefício da sociedade e da saúde.

Baixar modelo de projeto

Fonte: habr.com

Adicionar um comentário