
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:
- Compilação;
- Execução automática de testes;
- Medição de cobertura de código;
- Instalação;
- Auto-documentação;
- Geração de uma sandbox online;
- Análise estática.
Quem já entende de C++ e CMake pode simplesmente e começar a usá-lo.
Conteúdo
.
├── 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.cppPrincipalmente, falaremos sobre como organizar os scripts do CMake, por isso eles serão analisados em detalhes. Os outros arquivos qualquer um pode visualizar diretamente .
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 ).
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)
Consideraremos duas opções.
A primeira opção — — 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 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)
É 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()
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 , 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 .
Também vale a pena prestar atenção às chamadas .
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 , 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 ).
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. .
add_library(Mylib::mylib ALIAS mylib)
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)
Se os testes forem desativados explicitamente com ou se nosso projeto for um subprojeto, ou seja, incorporado em outro projeto CMake usando o comando , 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()
A documentação também não será gerada no caso de um subprojeto.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
Da mesma forma, as areias online do subprojeto também não existirão.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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)
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 , 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()
.
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 ).
Depois, criamos um alvo , 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 ()
Aqui encontramos o terceiro Python e criamos um alvo , que gera uma solicitação correspondente à API do serviço , 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()
Agora, vamos ver como utilizar tudo isso.
A construção deste projeto, assim como qualquer outro projeto no sistema de construção CMake, consiste em duas etapas:
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 ...]
.
cmake --build caminho\/para\/o\/diretório\/de\/build [--target target].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [outras opções ...]Ativa o alvo , com a ajuda da qual é possível iniciar a medição da cobertura de código por testes.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [outras opções ...]Permite desativar a compilação de testes de unidade e o alvo . Como resultado, a medição da cobertura de código por testes é desativada (veja ).
Além disso, os testes são automaticamente desativados se o projeto for incluído em outro projeto como um subprojeto usando o comando .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [outras opções ...]Muda o idioma da documentação que é gerada pelo alvo para o especificado. A lista de idiomas disponíveis está em .
Por padrão, o russo está habilitado.
cmake --build caminho/para/diretorio/de/build
cmake --build caminho/para/diretorio/de/build --target allSe o alvo não for especificado (o que é equivalente ao alvo all), compila tudo o que pode, além de chamar o alvo .
cmake --build caminho/para/diretorio/de/build --target mylib-unit-testsCompila os testes de unidade. Habilitado por padrão.
cmake --build caminho/para/diretorio/de/build --target checkExecuta os testes de unidade compilados (compila, se ainda não o fez). Habilitado por padrão.
Veja também .
cmake --build caminho/para/diretorio/de/build --target coverageAnalisa os testes de unidade executados (executa, se ainda não o fez) quanto à cobertura de código utilizando o programa .
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 .
Veja também .
cmake --build caminho/para/diretorio/de/build --target docInicia a geração de documentação do código utilizando o sistema .
cmake --build caminho/para/diretorio/de/build --target wandboxA 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 . Não sei até que ponto eles têm servidores robustos, mas acredito que não vale a pena abusar dessa possibilidade.
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 16Instalaçã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 installCompilaçã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 4Geraçã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
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.
Biblioteca de teste
Os testes podem ser desativados (veja ).
Para alternar o idioma em que a documentação será gerada, existe a opção .
Interpretador de linguagem de programação
Para geração automá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 .
Para isso, você deve usar a opção :
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 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/compilacaoAqui, ao contrário do caso com Cppcheck, é necessário sempre iniciar a construção através do scan-build.
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.
→
Fonte: habr.com
