CMake e C++ — irmãos para sempre

CMake e C++ — irmãos para sempre

Durante o desenvolvimento, eu gosto de trocar compiladores, modos de compilação, versões de dependências, realizar análise estática, medir desempenho, coletar cobertura, gerar documentação, etc. E eu realmente gosto do CMake, porque ele me permite fazer tudo isso que eu quero.

Muitos criticam o CMake, e muitas vezes com razão, mas se você investigar, não é tão ruim assim, e recentemente tem estado bastante bom, e a direção do desenvolvimento é bastante positiva.

Neste artigo, quero explicar como é relativamente simples organizar uma biblioteca de cabeçalho na linguagem C++ dentro do sistema CMake para obter a seguinte funcionalidade:

  1. Compilação;
  2. Execução automática de testes;
  3. Medição de cobertura de código;
  4. Instalação;
  5. Auto-documentação;
  6. Geração de uma sandbox online;
  7. Análise estática.

Quem já entende de C++ e CMake pode simplesmente baixar um modelo de projeto e começar a usá-lo.


Conteúdo

  1. O projeto por dentro
    1. Estrutura do projeto
    2. Arquivo principal do CMake (./CMakeLists.txt)
      1. Informações sobre o projeto
      2. Opções do projeto
      3. Opções de compilação
      4. Objetivo principal
      5. Instalação
      6. Testes
      7. Documentação
      8. Sandbox online
    3. Script para testes (test/CMakeLists.txt)
      1. Testes
      2. Cobertura
    4. Script para documentação (doc/CMakeLists.txt)
    5. Script para sandbox online (online/CMakeLists.txt)
  2. O projeto por fora
    1. Compilação
      1. Geração
      2. Compilação
    2. Opções
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Objetivos de compilação
      1. Por padrão
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Exemplos
  3. Ferramentas
  4. Análise estática
  5. Epílogo

O 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

Principalmente, falaremos sobre como organizar os scripts do CMake, por isso eles serão analisados em detalhes. Os outros arquivos qualquer um pode visualizar diretamente na página do projeto modelo.

Arquivo principal do CMake (./CMakeLists.txt)

Informações sobre o projeto

Em primeiro lugar, precisamos solicitar a versão adequada do sistema CMake. O CMake está em desenvolvimento, as assinaturas dos comandos e o comportamento em diferentes condições mudam. Para que o CMake compreenda imediatamente o que queremos dele, precisamos estabelecer nossos requisitos desde o início.

cmake_minimum_required(VERSION 3.13)

Em seguida, vamos identificar nosso projeto, seu nome, versão, idiomas utilizados e outras informações (veja o comando project).

Neste caso, especificamos o idioma CXX (o que significa C++), para que o CMake não tenha problemas em encontrar o compilador da linguagem C (por padrão, o CMake inclui duas linguagens: C e C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Aqui também podemos verificar imediatamente se nosso projeto está incluído como um subprojeto em outro projeto. Isso será muito útil posteriormente.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opções do projeto

Consideraremos duas opções.

A primeira opção — MYLIB_TESTING — para desativar os testes de módulo. Isso pode ser necessário se estivermos confiantes de que os testes estão funcionando corretamente, e queremos, por exemplo, apenas instalar ou empacotar nosso projeto. Ou nosso projeto está incluído como um subprojeto — nesse caso, o usuário do nosso projeto não estará interessado em executar nossos testes. Você não testa as dependências que usa, certo?

option(MYLIB_TESTING "Habilitar testes de módulo" ON)

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

option(MYLIB_COVERAGE "Habilitar medição de cobertura de código pelos testes" OFF)

Opções de compilação

É claro que somos programadores C++ excepcionais, portanto queremos obter o nível máximo de diagnósticos do compilador durante a compilação. Nenhum erro passará despercebido.

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
)

Desativaremos também as extensões, para estar completamente em conformidade com o padrão da linguagem C++. Por padrão, elas estão ativadas no CMake.

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

Objetivo principal

Nossa biblioteca consiste apenas de arquivos de cabeçalho, o que significa que não temos nenhuma saída na forma de bibliotecas estáticas ou dinâmicas. Por outro lado, para usar nossa biblioteca externamente, ela precisa ser instalada, deve estar disponível no sistema e ser vinculada ao seu projeto, e os respectivos cabeçalhos, além de algumas propriedades adicionais, devem também ser vinculados.

Para isso, criamos uma biblioteca de interface.

add_library(mylib INTERFACE)

Vinculamos os cabeçalhos à nossa biblioteca de interface.

O uso moderno e contemporâneo do CMake implica que os cabeçalhos, propriedades, etc., são passados através de um único alvo. Assim, é suficiente dizer target_link_libraries(target PRIVATE dependency), e todos os cabeçalhos associados ao alvo dependência, estarão disponíveis para as fontes pertencentes ao alvo alvo. E não são necessárias [target_]include_directories. Isso será demonstrado abaixo ao analisar o script CMake para testes modulares.

Também vale a pena prestar atenção às chamadas expressões geradoras: $.

Este comando associa os cabeçalhos necessários à nossa biblioteca de interface, sendo que, caso nossa biblioteca seja conectada a algum alvo dentro de uma mesma hierarquia CMake, os cabeçalhos do diretório ${CMAKE_CURRENT_SOURCE_DIR}/include, e se nossa biblioteca estiver instalada no sistema e conectada em outro projeto com o comando find_package, então os cabeçalhos serão associados ao diretório include em relação ao diretório de instalação.

target_include_directories(mylib INTERFACE
    $
    $
)

Definimos o padrão da linguagem. Claro, o mais recente. Isso não apenas ativa o padrão, mas também o estende para aqueles que usarão nossa biblioteca. Isso é alcançado porque a propriedade definida pertence à categoria INTERFACE (veja o comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Criamos um pseudônimo para nossa biblioteca. Para isso, ele usará um 'espaço de nomes' especial. Isso será útil quando nossa biblioteca tiver diferentes módulos, permitindo conectá-los de forma independente. Assim como no Boost, por exemplo.

add_library(Mylib::mylib ALIAS mylib)

Instalação

Instalando nossos cabeçalhos no sistema. Aqui é simples. Informamos que a pasta com todos os cabeçalhos deve ser colocada no diretório include em relação ao local de instalação.

install(DIRECTORY include/mylib DESTINATION include)

Em seguida, informamos ao sistema de build que desejamos ter a possibilidade de chamar o comando find_package(Mylib) e obter o alvo Mylib::mylib.

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

A próxima invocação deve ser entendida assim. Quando em um projeto externo chamamos o comando find_package(Mylib 1.2.3 REQUIRED), e se a versão real da biblioteca instalada for incompatível com a versão 1.2.3, o CMake gerará automaticamente um erro. Ou seja, não será necessário acompanhar 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 com a opção correspondente ou se nosso projeto for um subprojeto, ou seja, incorporado em outro projeto CMake usando o comando add_subdirectory, não avançamos mais na hierarquia, e o script que descreve os comandos para gerar e executar os testes simplesmente não é executado.

if(NOT MYLIB_TESTING)
    message(STATUS "Testando o projeto Mylib desativado")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib não está sendo testado no modo submódulo")
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()

Sandbox online

Da mesma forma, as areias online do subprojeto também não existirão.

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

Script para testes (test/CMakeLists.txt)

Testes

Primeiro, encontramos o pacote com o framework de teste desejado (substitua pelo seu favorito).

find_package(doctest 2.3.3 REQUIRED)

Criamos nosso executável com os testes. Normalmente, adiciono apenas o arquivo que contém a função main.

add_executable(mylib-unit-tests test_main.cpp)

E os arquivos que descrevem os próprios testes são adicionados posteriormente. Mas não é necessário fazer assim.

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

Conectamos as dependências. Note que nosso executável está apontado apenas para os alvos CMake necessários, e não chamamos o comando target_include_directories. Os cabeçalhos do framework de teste e da nossa Mylib::mylib, assim como os parâmetros de compilação (no nosso caso, este é o padrão da linguagem C++) vieram juntos com esses alvos.

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

Finalmente, criamos um alvo fictício, cuja "construção" é equivalente à execução dos testes, e adicionamos esse alvo à construção padrão (o que é controlado pelo atributo ALL). Isso significa que a construção padrão inicia a execução 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 de cobertura de código se a opção correspondente estiver definida. Não entrarei em detalhes, pois eles se referem mais à ferramenta de medição de cobertura do que ao CMake. O importante é observar que, com os resultados, será criado um alvo coverage, que facilita a execução da medição de cobertura.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Medição da cobertura de código por testes está ativada")

    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 "O programa gcovr é necessário para a medição da cobertura de código")
endif()

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

Doxygen encontrado.

find_package(Doxygen)

A seguir, verificamos se o usuário definiu a variável de idioma. Se sim, não alteramos, se não, usamos o russo. Depois, configuramos os arquivos do sistema Doxygen. Todas as variáveis necessárias, incluindo o idioma, estão incluídas nesse processo de configuração (veja o comando configure_file).

Depois, criamos um alvo doc, que irá iniciar a geração da documentação. Como a geração de documentação não é a maior prioridade durante o desenvolvimento, este alvo não será habilitado por padrão e terá que ser iniciado explicitamente.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russo)
    endif()
    message(STATUS "A documentação Doxygen será gerada em ${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 Wandbox, e a envia. Em resposta, recebemos um link para a sandbox pronta.

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 "Um interpretador da linguagem de programação Python versão 3 é necessário para criar a sandbox online")
endif()

O projeto por fora

Agora, vamos ver como utilizar tudo isso.

Compilação

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

Geração

cmake -S caminho\/para\/os\/fontes -B caminho\/para\/o\/diretório\/de\/build [opções ...]

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

cmake caminho\/para\/os\/fontes -B caminho\/para\/o\/diretório\/de\/build [opções ...]

Mais informações sobre opções.

Construção do projeto

cmake --build caminho\/para\/o\/diretório\/de\/build [--target target]

Mais informações sobre alvos de construção.

Opções

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [outras opções ...]

Ativa o alvo coverage, com a ajuda da qual é possível iniciar a medição da cobertura de código por testes.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [outras opções ...]

Permite desativar a compilação de testes de unidade e o alvo check. Como resultado, a medição da cobertura de código por testes é desativada (veja MYLIB_COVERAGE).

Além disso, os testes são automaticamente desativados se o projeto for incluído em outro projeto como um subprojeto usando o comando add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [outras opções ...]

Muda o idioma da documentação que é gerada pelo alvo doc para o especificado. A lista de idiomas disponíveis está em no site do sistema Doxygen.

Por padrão, o russo está habilitado.

Objetivos de compilação

Por padrão

cmake --build caminho/para/diretorio/de/build
cmake --build caminho/para/diretorio/de/build --target all

Se o alvo não for especificado (o que é equivalente ao alvo all), compila tudo o que pode, além de chamar o alvo check.

mylib-unit-tests

cmake --build caminho/para/diretorio/de/build --target mylib-unit-tests

Compila os testes de unidade. Habilitado por padrão.

check

cmake --build caminho/para/diretorio/de/build --target check

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

Veja também mylib-unit-tests.

coverage

cmake --build caminho/para/diretorio/de/build --target coverage

Analisa os testes de unidade executados (executa, se ainda não o fez) quanto à cobertura de código utilizando o programa gcovr.

A saída da cobertura parecerá mais ou menos assim:

------------------------------------------------------------------------------
                           Relatório de Cobertura de Código GCC
Diretório: /caminho/para/cmakecpptemplate/include/
------------------------------------------------------------------------------
Arquivo                                       Linhas    Exec  Cobrir   Faltando
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

O alvo está disponível apenas quando a opção está habilitada MYLIB_COVERAGE.

Veja também check.

doc

cmake --build caminho/para/diretorio/de/build --target doc

Inicia a geração de documentação do código utilizando o sistema Doxygen.

wandbox

cmake --build caminho/para/diretorio/de/build --target wandbox

A resposta do serviço parecerá mais ou menos assim:

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

Para isso, é utilizado o serviço Wandbox. Não sei até que ponto eles têm servidores robustos, mas acredito que não vale a pena abusar dessa possibilidade.

Exemplos

Compilação do projeto em modo de depuração com medição de cobertura

cmake -S caminho/para/fontes -B caminho/para/diretorio/de/build -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build caminho/para/diretorio/de/build --target coverage --parallel 16

Instalação do projeto sem compilação ou teste prévio

cmake -S caminho/para/fontes -B caminho/para/diretorio/da/compilacao -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=caminho/para/o/diretorio/de/instalacao
cmake --build caminho/para/diretorio/da/compilacao --target install

Compilação em modo release com o compilador especificado

cmake -S caminho/para/fontes -B caminho/para/diretorio/da/compilacao -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=caminho/para/o/diretorio/onde/as/dependencias/estao/instaladas
cmake --build caminho/para/diretorio/da/compilacao --parallel 4

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

cmake -S caminho/para/fontes -B caminho/para/diretorio/da/compilacao -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build caminho/para/diretorio/da/compilacao --target doc

Ferramentas

  1. CMake 3.13

    Na verdade, a versão do CMake 3.13 é necessária apenas para executar alguns comandos de console descritos nesta documentação. 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 teste doctest

    Os testes podem ser desativados (veja opção MYLIB_TESTING).

  3. Doxygen

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

  4. Interpretador de linguagem de programação Python 3

    Para geração automática sandbox online.

Análise estática

Com o CMake e algumas boas ferramentas, é possível garantir análise estática com o mínimo de esforço.

Cppcheck

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

Para isso, você deve usar a opção CMAKE_CXX_CPPCHECK:

cmake -S caminho/para/fontes -B caminho/para/diretorio/da/compilacao -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-I caminho/para/fontes/include"

Depois disso, a análise estática será executada automaticamente sempre que houver compilação e recompilação do código-fonte. Não é necessário fazer nada adicional.

Clang

Com a maravilhosa ferramenta scan-build também é possível executar a análise estática rapidamente:

scan-build cmake -S caminho/para/fontes -B caminho/para/diretorio/da/compilacao -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build caminho/para/diretorio/da/compilacao

Aqui, ao contrário do caso com Cppcheck, é necessário sempre iniciar a construção através do scan-build.

Epílogo

O CMake é um sistema muito poderoso e flexível, permitindo implementar funcionalidades de todas as formas e estilos. E, embora a sintaxe possa por vezes deixar a desejar, não é tão assustador quanto parece. Use o sistema de construção CMake para o bem da sociedade e para o seu próprio benefício.

→ Baixar o modelo de projeto

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster