Organização do projeto C ++ (com gtest, cmake e doxygen)

123

Como sou iniciante em programação em geral, decidi começar por criar uma classe de vetor simples em C ++. No entanto, gostaria de adotar bons hábitos desde o início, em vez de tentar modificar meu fluxo de trabalho mais tarde.

Atualmente, tenho apenas dois arquivos vector3.hppe vector3.cpp. Este projeto começará lentamente a crescer (tornando-o muito mais uma biblioteca de álgebra linear geral) à medida que me familiarizar com tudo, então eu gostaria de adotar um layout de projeto "padrão" para facilitar a vida mais tarde. Então, depois de olhar em volta, encontrei duas maneiras de organizar os arquivos hpp e cpp, sendo o primeiro:

project
└── src
    ├── vector3.hpp
    └── vector3.cpp

e o segundo sendo:

project
├── inc
   └── project
       └── vector3.hpp
└── src
    └── vector3.cpp

O que você recomendaria e por quê?

Em segundo lugar, gostaria de usar o Google C ++ Testing Framework para testar o meu código por unidade, pois parece bastante fácil de usar. Você sugere empacotá-lo com meu código, por exemplo, em uma pasta inc/gtestou contrib/gtest? Se incluído, você sugere usar o fuse_gtest_files.pyscript para reduzir o número ou os arquivos ou deixá-lo como está? Se não estiver agrupado, como essa dependência é tratada?

Quando se trata de escrever testes, como eles geralmente são organizados? Eu estava pensando em ter um arquivo cpp para cada classe ( test_vector3.cpppor exemplo), mas todos compilados em um binário para que todos possam ser executados juntos facilmente?

Como a biblioteca gtest geralmente é construída usando cmake e make, eu estava pensando que faria sentido que meu projeto também fosse construído assim? Se eu decidisse usar o seguinte layout do projeto:

├── CMakeLists.txt
├── contrib
   └── gtest
       ├── gtest-all.cc
       └── gtest.h
├── docs
   └── Doxyfile
├── inc
   └── project
       └── vector3.cpp
├── src
   └── vector3.cpp
└── test
    └── test_vector3.cpp

Como seria a CMakeLists.txtaparência para poder construir apenas a biblioteca ou a biblioteca e os testes? Também vi alguns projetos que possuem buildum bindiretório e. A construção acontece no diretório de construção e os binários foram transferidos para o diretório bin? Os binários dos testes e a biblioteca morariam no mesmo local? Ou faria mais sentido estruturá-lo da seguinte maneira:

test
├── bin
├── build
└── src
    └── test_vector3.cpp

Eu também gostaria de usar doxygen para documentar meu código. É possível fazer isso rodar automaticamente com o cmake e make?

Desculpe por tantas perguntas, mas não encontrei um livro em C ++ que responda satisfatoriamente a esse tipo de pergunta.

rozzy
fonte
6
Ótima pergunta, mas não acho que seja uma boa opção para o formato de perguntas e respostas do Stack Overflow . Estou muito interessado em uma resposta, no entanto. +1 & fav
Luchian Grigore
1
Estas são muitas perguntas em grande. Que seja melhor dividi-lo em várias perguntas menores e colocar links entre si. De qualquer forma, para responder à última parte: Com o CMake, você pode optar por criar dentro e fora do diretório src (eu recomendaria fora). E sim, você pode usar o doxygen com o CMake automaticamente.
mistapink

Respostas:

84

Os sistemas de construção C ++ são um pouco de arte negra e, quanto mais antigo o projeto, mais coisas estranhas você pode encontrar, por isso não é surpreendente que surjam muitas perguntas. Vou tentar percorrer as perguntas uma por uma e mencionar algumas coisas gerais sobre a construção de bibliotecas C ++.

Separando cabeçalhos e arquivos cpp em diretórios. Isso é essencial apenas se você estiver criando um componente que deve ser usado como uma biblioteca, em oposição a um aplicativo real. Seus cabeçalhos são a base para os usuários interagirem com o que você oferece e deve ser instalado. Isso significa que eles precisam estar em um subdiretório (ninguém quer muitos cabeçalhos terminando no nível superior /usr/include/) e os cabeçalhos devem poder se incluir nessa configuração.

└── prj
    ├── include
       └── prj
           ├── header2.h
           └── header.h
    └── src
        └── x.cpp

funciona bem, porque incluir caminhos funciona e você pode usar globbing fácil para instalar destinos.

Agrupando dependências: Eu acho que isso depende muito da capacidade do sistema de compilação de localizar e configurar dependências e de quão dependente é o seu código em uma única versão. Também depende da capacidade dos seus usuários e de quão fácil é a dependência para instalar na plataforma deles. O CMake vem com um find_packagescript para o Google Test. Isso facilita muito as coisas. Eu iria com o agrupamento apenas quando necessário e evitaria o contrário.

Como criar: Evite compilações na fonte. O CMake facilita a criação de fontes e facilita muito a vida.

Suponho que você também queira usar o CTest para executar testes para o seu sistema (ele também vem com suporte embutido para o GTest). Uma decisão importante para o layout do diretório e a organização de teste será: Você acaba com subprojetos? Se assim for, você precisa de um pouco mais de trabalho ao configurar CMakeLists e deve dividir seus subprojetos em subdiretórios, cada um com a sua própria includee srcarquivos. Talvez até o próprio doxygen seja executado e produzido (a combinação de vários projetos doxygen é possível, mas não é fácil ou bonita).

Você terminará com algo assim:

└── prj
    ├── CMakeLists.txt <-- (1)
    ├── include
       └── prj
           ├── header2.hpp
           └── header.hpp
    ├── src
       ├── CMakeLists.txt <-- (2)
       └── x.cpp
    └── test
        ├── CMakeLists.txt <-- (3)
        ├── data
           └── testdata.yyy
        └── testcase.cpp

Onde

  • (1) configura dependências, especificações da plataforma e caminhos de saída
  • (2) configura a biblioteca que você irá construir
  • (3) configura os executáveis ​​e os casos de teste

Caso você tenha subcomponentes, sugiro adicionar outra hierarquia e usar a árvore acima para cada subprojeto. Então as coisas ficam complicadas, porque você precisa decidir se os subcomponentes pesquisam e configuram suas dependências ou se você faz isso no nível superior. Isso deve ser decidido caso a caso.

Doxygen: Depois que você conseguiu passar pela dança de configuração do doxygen, é trivial usar o CMake add_custom_commandpara adicionar um destino de documento.

É assim que meus projetos terminam e eu já vi alguns projetos muito semelhantes, mas é claro que isso não cura tudo.

Adendo Em algum momento, você desejará gerar um config.hpp arquivo que contenha uma definição de versão e talvez uma definição para algum identificador de controle de versão (um hash Git ou número de revisão SVN). O CMake possui módulos para automatizar a localização dessas informações e gerar arquivos. Você pode usar o CMake configure_filepara substituir variáveis ​​em um arquivo de modelo por variáveis ​​definidas dentro do CMakeLists.txt.

Se você estiver construindo bibliotecas, também precisará de uma definição de exportação para acertar a diferença entre compiladores, por exemplo, __declspecno MSVC e visibilityatributos no GCC / clang.

pmr
fonte
2
Boa resposta, mas ainda não sei ao certo por que você precisa colocar os arquivos de cabeçalho em um subdiretório adicional denominado projeto: "/prj/include/prj/foo.hpp", o que me parece redundante. Por que não apenas "/prj/include/foo.hpp"? Estou assumindo que você terá a oportunidade de alterar novamente os diretórios de instalação no momento da instalação, para obter <INSTALL_DIR> /include/prj/foo.hpp ao instalar, ou isso é difícil no CMake?
William Payne
6
@ William Isso é realmente difícil de fazer com o CPack. Além disso, como seriam as suas inclusões nos arquivos de origem? Se eles são apenas "header.hpp" em uma versão instalada "/ usr / include / prj /" precisa estar no caminho de inclusão, em vez de apenas "/ usr / include".
pmr
37

Para começar, existem alguns nomes convencionais para diretórios que você não pode ignorar, baseados na longa tradição do sistema de arquivos Unix. Esses são:

trunk
├── bin     : for all executables (applications)
├── lib     : for all other binaries (static and shared libraries (.so or .dll))
├── include : for all header files
├── src     : for source files
└── doc     : for documentation

Provavelmente, é uma boa ideia manter esse layout básico, pelo menos no nível superior.

Sobre a divisão dos arquivos de cabeçalho e arquivos de origem (cpp), ambos os esquemas são bastante comuns. No entanto, costumo preferir mantê-los juntos, é apenas mais prático nas tarefas diárias reunir os arquivos. Além disso, quando todo o código está em uma pasta de nível superior, ou seja, a trunk/src/pasta, você pode observar que todas as outras pastas (bin, lib, include, doc e talvez alguma pasta de teste) no nível superior, além de o diretório "build" para uma build fora da fonte, são todas as pastas que contêm nada além de arquivos gerados no processo de build. E, portanto, apenas é necessário fazer backup da pasta src, ou muito melhor, mantida em um sistema / servidor de controle de versão (como Git ou SVN).

E quando se trata de instalar seus arquivos de cabeçalho no sistema de destino (se você finalmente distribuir sua biblioteca), o CMake possui um comando para instalar arquivos (cria implicitamente um destino "install", para "make install") que você pode usar para colocar todos os cabeçalhos no /usr/include/diretório Eu apenas uso a seguinte macro cmake para esse fim:

# custom macro to register some headers as target for installation:
#  setup_headers("/path/to/header/something.h" "/relative/install/path")
macro(setup_headers HEADER_FILES HEADER_PATH)
  foreach(CURRENT_HEADER_FILE ${HEADER_FILES})
    install(FILES "${SRCROOT}${CURRENT_HEADER_FILE}" DESTINATION "${INCLUDEROOT}${HEADER_PATH}")
  endforeach(CURRENT_HEADER_FILE)
endmacro(setup_headers)

Onde SRCROOTestá uma variável cmake que eu defini para a pasta src e INCLUDEROOTé a variável cmake que eu configuro para onde quer que os cabeçalhos precisem ir. Claro, existem muitas outras maneiras de fazer isso, e tenho certeza de que meu caminho não é o melhor. O ponto é que não há razão para dividir os cabeçalhos e as fontes apenas porque apenas os cabeçalhos precisam ser instalados no sistema de destino, porque é muito fácil, especialmente com o CMake (ou CPack), escolher e configurar os cabeçalhos para ser instalado sem a necessidade de tê-los em um diretório separado. E é isso que tenho visto na maioria das bibliotecas.

Citação: Em segundo lugar, eu gostaria de usar o Google C ++ Testing Framework para testar o meu código por unidade, pois parece bastante fácil de usar. Você sugere empacotá-lo com meu código, por exemplo, em uma pasta "inc / gtest" ou "contrib / gtest"? Se incluído, você sugere o uso do script fuse_gtest_files.py para reduzir o número ou os arquivos ou deixá-lo como está? Se não estiver agrupado, como essa dependência é tratada?

Não agrupe dependências com sua biblioteca. Geralmente, essa é uma ideia horrível, e eu sempre odeio quando estou parada tentando construir uma biblioteca que fez isso. Deve ser o seu último recurso e cuidado com as armadilhas. Freqüentemente, as pessoas agrupam dependências em sua biblioteca porque elas visam um ambiente de desenvolvimento terrível (por exemplo, Windows) ou porque suportam apenas uma versão antiga (obsoleta) da biblioteca (dependência) em questão. A principal armadilha é que sua dependência empacotada pode colidir com versões já instaladas da mesma biblioteca / aplicativo (por exemplo, você empacotou o gtest, mas a pessoa que tenta compilar sua biblioteca já possui uma versão mais nova (ou mais antiga) do gtest já instalada e, em seguida, os dois podem entrar em conflito e dar a essa pessoa uma dor de cabeça muito desagradável). Então, como eu disse, faça por sua conta e risco, e eu diria apenas como último recurso. Pedir às pessoas que instalem algumas dependências antes de poder compilar sua biblioteca é um mal muito menor do que tentar resolver conflitos entre suas dependências agrupadas e as instalações existentes.

Citação: Quando se trata de escrever testes, como eles geralmente são organizados? Eu estava pensando em ter um arquivo cpp para cada classe (test_vector3.cpp por exemplo), mas todos compilados em um binário para que todos possam ser executados juntos facilmente?

Um arquivo cpp por classe (ou pequeno grupo coeso de classes e funções) é mais usual e prático na minha opinião. No entanto, definitivamente, não os compile todos em um binário apenas para que "todos possam ser executados juntos". Essa é uma péssima ideia. Geralmente, quando se trata de codificação, você deseja dividir as coisas o quanto for razoável. No caso de testes de unidade, você não deseja que um binário execute todos os testes, porque isso significa que qualquer pequena alteração feita em qualquer coisa da sua biblioteca provavelmente causará uma recompilação quase total desse programa de teste de unidade , e são apenas alguns minutos / horas perdidos à espera da recompilação. Basta seguir um esquema simples: 1 unidade = 1 programa de teste de unidade. Então,

Citação: Como a biblioteca gtest geralmente é construída usando cmake e make, eu estava pensando que faria sentido para o meu projeto também ser construído assim? Se eu decidisse usar o seguinte layout do projeto:

Prefiro sugerir este layout:

trunk
├── bin
├── lib
   └── project
       └── libvector3.so
       └── libvector3.a        products of installation / building
├── docs
   └── Doxyfile
├── include
   └── project
       └── vector3.hpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── src
   └── CMakeLists.txt
   └── Doxyfile.in
   └── project                 part of version-control / source-distribution
       └── CMakeLists.txt
       └── vector3.hpp
       └── vector3.cpp
       └── test
           └── test_vector3.cpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── build
└── test                        working directories for building / testing
    └── test_vector3

Algumas coisas a serem observadas aqui. Primeiro, os subdiretórios do diretório src devem espelhar os subdiretórios do diretório de inclusão, isso é apenas para manter as coisas intuitivas (tente também manter a estrutura do subdiretório razoavelmente plana (superficial), porque o aninhamento profundo de pastas geralmente é mais complicado do que qualquer outra coisa). Segundo, o diretório "include" é apenas um diretório de instalação, seu conteúdo é exatamente o que os cabeçalhos selecionam no diretório src.

Terceiro, o sistema CMake deve ser distribuído pelos subdiretórios de origem, não como um arquivo CMakeLists.txt no nível superior. Isso mantém as coisas locais, e isso é bom (no espírito de dividir as coisas em partes independentes). Se você adicionar uma nova fonte, um novo cabeçalho ou um novo programa de teste, tudo o que você precisa é editar um arquivo CMakeLists.txt pequeno e simples no subdiretório em questão, sem afetar mais nada. Isso também permite reestruturar os diretórios com facilidade (CMakeLists são locais e estão contidos nos subdiretórios que estão sendo movidos). Os CMakeLists de nível superior devem conter a maioria das configurações de nível superior, como definir diretórios de destino, comandos personalizados (ou macros) e localizar pacotes instalados no sistema. Os CMakeLists de nível inferior devem conter apenas listas simples de cabeçalhos, fontes,

Citação: como o CMakeLists.txt deveria parecer para poder criar apenas a biblioteca ou a biblioteca e os testes?

A resposta básica é que o CMake permite excluir especificamente determinados destinos de "todos" (que é criado quando você digita "make") e também pode criar pacotes específicos de destinos. Não posso fazer um tutorial do CMake aqui, mas é bastante simples descobrir você mesmo. Nesse caso específico, no entanto, a solução recomendada é, obviamente, usar o CTest, que é apenas um conjunto adicional de comandos que você pode usar nos arquivos CMakeLists para registrar um número de destinos (programas) marcados como unidades. testes. Portanto, o CMake colocará todos os testes em uma categoria especial de compilações, e é exatamente isso que você solicitou, portanto, o problema foi resolvido.

Citação: Também vi alguns projetos que têm um anúncio de construção e um diretório bin. A construção acontece no diretório de construção e os binários foram transferidos para o diretório bin? Os binários dos testes e a biblioteca morariam no mesmo local? Ou faria mais sentido estruturá-lo da seguinte maneira:

Ter um diretório de compilação fora da fonte (compilação "fora da fonte") é realmente a única coisa sensata a ser feita, é o padrão de fato atualmente. Portanto, definitivamente, tenha um diretório "build" separado, fora do diretório de origem, exatamente como o pessoal do CMake recomenda, e como todo programador que eu já conheci. Quanto ao diretório bin, bem, isso é uma convenção, e provavelmente é uma boa ideia cumpri-la, como eu disse no começo deste post.

Citação: Eu também gostaria de usar o doxygen para documentar meu código. É possível fazer isso rodar automaticamente com o cmake e make?

Sim. É mais do que possível, é incrível. Dependendo de como você deseja obter, existem várias possibilidades. O CMake possui um módulo para Doxygen (isto é, find_package(Doxygen)) que permite registrar destinos que executam o Doxygen em alguns arquivos. Se você quiser fazer coisas mais sofisticadas, como atualizar o número da versão no arquivo Doxyfile ou inserir automaticamente uma data / autor para os arquivos de origem e assim por diante, tudo isso é possível com um pouco de kung-fu do CMake. Geralmente, isso implica que você mantenha um Doxyfile de origem (por exemplo, o "Doxyfile.in" que eu coloquei no layout da pasta acima) que possui tokens a serem encontrados e substituídos pelos comandos de análise do CMake. No meu arquivo CMakeLists de nível superior , você encontrará um desses pedaços de kung-fu do CMake que faz algumas coisas sofisticadas com o cmake-doxygen juntos.

Mikael Persson
fonte
Então main.cppdeve ir para trunk/bin?
Ugnius Malūkas
17

Estruturando o projeto

Eu geralmente preferiria o seguinte:

├── CMakeLists.txt
|
├── docs/
│   └── Doxyfile
|
├── include/
│   └── project/
│       └── vector3.hpp
|
├── src/
    └── project/
        └── vector3.cpp
        └── test/
            └── test_vector3.cpp

Isso significa que você tem um conjunto muito claramente definido de arquivos de API para sua biblioteca, e a estrutura significa que os clientes da sua biblioteca

#include "project/vector3.hpp"

ao invés de menos explícito

#include "vector3.hpp"


Eu gosto da estrutura da árvore / src para coincidir com a da árvore / include, mas é realmente uma preferência pessoal. No entanto, se o seu projeto se expandir para conter subdiretórios em / include / project, geralmente ajudará a corresponder aqueles dentro da árvore / src.

Para os testes, sou a favor de mantê-los "próximos" dos arquivos que eles testam e, se você acabar com subdiretórios no / src, é um paradigma bastante fácil para outras pessoas seguirem se quiserem encontrar o código de teste de um determinado arquivo.


Teste

Em segundo lugar, eu gostaria de usar o Google C ++ Testing Framework para testar o meu código por unidade, pois parece bastante fácil de usar.

O Gtest é realmente simples de usar e é bastante abrangente em termos de suas capacidades. Ele pode ser usado com o gmock com muita facilidade para ampliar suas capacidades, mas minhas próprias experiências com o gmock foram menos favoráveis. Estou bastante preparado para aceitar que isso pode ter tudo a ver com minhas próprias falhas, mas os testes do gmock tendem a ser mais difíceis de criar e muito mais frágeis / difíceis de manter. Um prego grande no caixão gmock é que ele realmente não funciona bem com ponteiros inteligentes.

Esta é uma resposta muito trivial e subjetiva a uma grande pergunta (que provavelmente não pertence à SO)

Você sugere empacotá-lo com meu código, por exemplo, em uma pasta "inc / gtest" ou "contrib / gtest"? Se incluído, você sugere o uso do script fuse_gtest_files.py para reduzir o número ou os arquivos ou deixá-lo como está? Se não estiver agrupado, como essa dependência é tratada?

Eu prefiro usar o ExternalProject_Addmódulo do CMake . Isso evita que você tenha que manter o código-fonte gtest no seu repositório ou instalá-lo em qualquer lugar. Ele é baixado e construído na sua árvore de construção automaticamente.

Veja minha resposta sobre os detalhes aqui .

Quando se trata de escrever testes, como eles geralmente são organizados? Eu estava pensando em ter um arquivo cpp para cada classe (test_vector3.cpp por exemplo), mas todos compilados em um binário para que todos possam ser executados juntos facilmente?

Bom plano.


Construção

Sou fã do CMake, mas, como em suas perguntas relacionadas ao teste, o SO provavelmente não é o melhor lugar para pedir opiniões sobre um assunto tão subjetivo.

Como seria a aparência do CMakeLists.txt para poder criar apenas a biblioteca ou a biblioteca e os testes?

add_library(ProjectLibrary <All library sources and headers>)
add_executable(ProjectTest <All test files>)
target_link_libraries(ProjectTest ProjectLibrary)

A biblioteca aparecerá como um destino "ProjectLibrary" e a suíte de testes como um destino "ProjectTest". Ao especificar a biblioteca como uma dependência do exe de teste, a criação do exe de teste fará com que a biblioteca seja reconstruída automaticamente se estiver desatualizada.

Também vi alguns projetos que têm um anúncio de construção e um diretório bin. A construção acontece no diretório de construção e os binários foram transferidos para o diretório bin? Os binários dos testes e a biblioteca morariam no mesmo local?

O CMake recomenda compilações "fora da fonte", ou seja, você cria seu próprio diretório de compilação fora do projeto e executa o CMake a partir daí. Isso evita "poluir" sua árvore de origem com arquivos de construção e é altamente desejável se você estiver usando um vcs.

Você pode especificar que os binários sejam movidos ou copiados para um diretório diferente, uma vez criados, ou que sejam criados por padrão em outro diretório, mas geralmente não há necessidade. O CMake fornece maneiras abrangentes de instalar seu projeto, se desejado, ou facilita para outros projetos do CMake "encontrar" os arquivos relevantes do seu projeto.

Com relação ao próprio suporte do CMake para localizar e executar testes gtest , isso seria inapropriado se você criar o gtest como parte do seu projeto. O FindGtestmódulo foi realmente projetado para ser usado no caso em que o gtest foi construído separadamente fora do seu projeto.

O CMake fornece sua própria estrutura de teste (CTest) e, idealmente, todos os casos de gtest seriam adicionados como um caso de CTest.

No entanto, a GTEST_ADD_TESTSmacro fornecida por FindGtestpara permitir a fácil adição de casos gtest como casos individuais de ctest está faltando, pois não funciona para macros do gtest além de TESTe TEST_F. Valor- ou Type-parametrizados testes usando TEST_P, TYPED_TEST_Petc. não são tratados em tudo.

O problema não tem uma solução fácil que eu saiba. A maneira mais robusta de obter uma lista de casos gtest é executar o exe de teste com o sinalizador --gtest_list_tests. No entanto, isso só pode ser feito após a criação do exe, portanto o CMake não pode fazer uso disso. O que deixa você com duas opções; O CMake deve tentar analisar o código C ++ para deduzir os nomes dos testes (não triviais ao extremo, se você quiser levar em consideração todas as macros gtest, testes comentados, testes desativados) ou casos de teste adicionados manualmente ao Arquivo CMakeLists.txt.

Eu também gostaria de usar doxygen para documentar meu código. É possível fazer isso rodar automaticamente com o cmake e make?

Sim - embora eu não tenha experiência nesta frente. O CMake fornece FindDoxygenpara esse fim.

Fraser
fonte
6

Além das outras (excelentes) respostas, vou descrever uma estrutura que tenho usado para projetos de larga escala .
Não vou abordar a subquestão sobre o Doxygen, pois apenas repetiria o que é dito nas outras respostas.


Fundamentação

Para modularidade e manutenção, o projeto é organizado como um conjunto de pequenas unidades. Para maior clareza, vamos chamá-los de UnitX, com X = A, B, C, ... (mas eles podem ter qualquer nome geral). A estrutura de diretórios é organizada para refletir essa opção, com a possibilidade de agrupar unidades, se necessário.

Solução

O layout básico do diretório é o seguinte (o conteúdo das unidades é detalhado posteriormente):

project
├── CMakeLists.txt
├── UnitA
├── UnitB
├── GroupA
   └── CMakeLists.txt
   └── GroupB
       └── CMakeLists.txt
       └── UnitC
       └── UnitD
   └── UnitE

project/CMakeLists.txt pode conter o seguinte:

cmake_minimum_required(VERSION 3.0.2)
project(project)
enable_testing() # This will be necessary for testing (details below)

add_subdirectory(UnitA)
add_subdirectory(UnitB)
add_subdirectory(GroupA)

e project/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)

e project/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)

Agora, para a estrutura das diferentes unidades (tomemos, por exemplo, UnitD)

project/GroupA/GroupB/UnitD
├── README.md
├── CMakeLists.txt
├── lib
   └── CMakeLists.txt
   └── UnitD
       └── ClassA.h
       └── ClassA.cpp
       └── ClassB.h
       └── ClassB.cpp
├── test
   └── CMakeLists.txt
   └── ClassATest.cpp
   └── ClassBTest.cpp
   └── [main.cpp]

Para os diferentes componentes:

  • Eu gosto de ter source ( .cpp) e headers ( .h) na mesma pasta. Isso evita uma hierarquia de diretórios duplicada, facilita a manutenção. Para a instalação, não há problema (especialmente no CMake) apenas filtrar os arquivos de cabeçalho.
  • A função do diretório UnitDé permitir posteriormente incluir arquivos com #include <UnitD/ClassA.h>. Além disso, ao instalar esta unidade, você pode apenas copiar a estrutura de diretórios como está. Observe que você também pode organizar seus arquivos de origem em subdiretórios.
  • Gosto de um READMEarquivo para resumir o que é a unidade e especificar informações úteis sobre ela.
  • CMakeLists.txt poderia simplesmente conter:

    add_subdirectory(lib)
    add_subdirectory(test)
  • lib/CMakeLists.txt:

    project(UnitD)
    
    set(headers
        UnitD/ClassA.h
        UnitD/ClassB.h
        )
    
    set(sources
        UnitD/ClassA.cpp
        UnitD/ClassB.cpp    
        )
    
    add_library(${TARGET_NAME} STATIC ${headers} ${sources})
    
    # INSTALL_INTERFACE: folder to which you will install a directory UnitD containing the headers
    target_include_directories(UnitD
                               PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                               PUBLIC $<INSTALL_INTERFACE:include/SomeDir>
                               )
    
    target_link_libraries(UnitD
                          PUBLIC UnitA
                          PRIVATE UnitC
                          )

    Aqui, observe que não é necessário informar ao CMake que queremos os diretórios de inclusão UnitAe UnitC, como isso já foi especificado ao configurar essas unidades. Além disso, PUBLICdirá a todos os destinos que dependem UnitDque eles devem incluir automaticamente a UnitAdependência, enquanto UnitCnão será necessário ( PRIVATE).

  • test/CMakeLists.txt (veja mais abaixo, se você quiser usar o GTest):

    project(UnitDTests)
    
    add_executable(UnitDTests
                   ClassATest.cpp
                   ClassBTest.cpp
                   [main.cpp]
                   )
    
    target_link_libraries(UnitDTests
                          PUBLIC UnitD
    )
    
    add_test(
            NAME UnitDTests
            COMMAND UnitDTests
    )

Usando o GoogleTest

Para o Google Test, o mais fácil é se a fonte estiver presente em algum lugar do diretório de origem, mas você não precisará adicioná-lo lá. Eu tenho usado esse projeto para fazer o download automaticamente e envolvo seu uso em uma função para garantir que ele seja baixado apenas uma vez, apesar de termos vários destinos de teste.

Essa função CMake é a seguinte:

function(import_gtest)
  include (DownloadProject)
  if (NOT TARGET gmock_main)
    include(DownloadProject)
    download_project(PROJ                googletest
                     GIT_REPOSITORY      https://github.com/google/googletest.git
                     GIT_TAG             release-1.8.0
                     UPDATE_DISCONNECTED 1
                     )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Prevent GoogleTest from overriding our compiler/linker options when building with Visual Studio
    add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
  endif()
endfunction()

e quando quiser usá-lo em um dos meus destinos de teste, adicionarei as seguintes linhas ao CMakeLists.txt(isto é, por exemplo, acima test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)
oLen
fonte
Bom "hack" que você fez lá com Gtest e cmake! Útil! :)
Tanasis