Organización de proyectos C ++ (con gtest, cmake y doxygen)

123

Soy nuevo en la programación en general, así que decidí comenzar haciendo una clase vectorial simple en C ++. Sin embargo, me gustaría adquirir buenos hábitos desde el principio en lugar de intentar modificar mi flujo de trabajo más adelante.

Actualmente tengo solo dos archivos vector3.hppy vector3.cpp. Este proyecto comenzará a crecer lentamente (haciéndolo mucho más una biblioteca de álgebra lineal general) a medida que me familiarice con todo, así que me gustaría adoptar un diseño de proyecto "estándar" para hacer la vida más fácil más adelante. Entonces, después de mirar alrededor, encontré dos formas de organizar los archivos hpp y cpp, la primera es:

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

y el segundo siendo:

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

¿Cuál recomendarías y por qué?

En segundo lugar, me gustaría utilizar Google C ++ Testing Framework para realizar pruebas unitarias de mi código, ya que parece bastante fácil de usar. ¿Sugieres empaquetar esto con mi código, por ejemplo, en una carpeta inc/gtesto contrib/gtest? Si está incluido, ¿sugiere usar el fuse_gtest_files.pyscript para reducir el número de archivos o dejarlo como está? Si no está incluido, ¿cómo se maneja esta dependencia?

Cuando se trata de pruebas de redacción, ¿cómo se organizan en general? Estaba pensando en tener un archivo cpp para cada clase ( test_vector3.cpppor ejemplo) pero todos compilados en un binario para que todos puedan ejecutarse juntos fácilmente.

Dado que la biblioteca gtest generalmente se construye usando cmake y make, estaba pensando que tendría sentido que mi proyecto también se construyera así. Si decidiera usar el siguiente diseño de proyecto:

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

¿Cómo debería CMakeLists.txtverse para poder construir solo la biblioteca o la biblioteca y las pruebas? También he visto bastantes proyectos que tienen buildun bindirectorio y un directorio. ¿La compilación ocurre en el directorio de compilación y luego los binarios se trasladan al directorio bin? ¿Los binarios de las pruebas y la biblioteca vivirían en el mismo lugar? ¿O tendría más sentido estructurarlo de la siguiente manera:

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

También me gustaría usar doxygen para documentar mi código. ¿Es posible hacer que esto se ejecute automáticamente con cmake y make?

Perdón por tantas preguntas, pero no he encontrado un libro sobre C ++ que responda satisfactoriamente a este tipo de preguntas.

rozzy
fuente
6
Gran pregunta, pero no creo que sea una buena opción para el formato de preguntas y respuestas de Stack Overflow . Sin embargo, estoy muy interesado en una respuesta. +1 & fav
Luchian Grigore
1
Estas son muchas preguntas en enorme. Ojalá sea mejor dividirlo en varias preguntas más pequeñas y colocar enlaces entre sí. De todos modos, para responder a la última parte: con CMake puede elegir compilar dentro y fuera de su directorio src (lo recomendaría fuera). Y sí, puede usar doxygen con CMake automáticamente.
wrongpink

Respuestas:

84

Los sistemas de compilación C ++ son un poco arte negro y cuanto más antiguo es el proyecto, más cosas raras se pueden encontrar, por lo que no es sorprendente que surjan muchas preguntas. Intentaré analizar las preguntas una por una y mencionar algunos aspectos generales relacionados con la creación de bibliotecas C ++.

Separación de encabezados y archivos cpp en directorios. Esto solo es esencial si está creando un componente que se supone que debe usarse como una biblioteca en lugar de una aplicación real. Tus encabezados son la base para que los usuarios interactúen con lo que ofreces y deben estar instalados. Esto significa que tienen que estar en un subdirectorio (nadie quiere que muchos encabezados terminen en el nivel superior /usr/include/) y sus encabezados deben poder incluirse a sí mismos con dicha configuración.

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

funciona bien, porque las rutas de inclusión funcionan y puede utilizar el globbing fácil para instalar destinos.

Agrupar dependencias: creo que esto depende en gran medida de la capacidad del sistema de compilación para localizar y configurar dependencias y de qué tan dependiente es su código de una sola versión. También depende de la capacidad de los usuarios y de la facilidad de instalación de la dependencia en su plataforma. CMake viene con un find_packagescript para Google Test. Esto facilita mucho las cosas. Yo optaría por empaquetar solo cuando fuera necesario y lo evitaría de otra manera.

Cómo compilar: evite las compilaciones en origen. CMake facilita las compilaciones de código fuente y hace la vida mucho más fácil.

Supongo que también desea utilizar CTest para ejecutar pruebas para su sistema (también viene con soporte integrado para GTest). Una decisión importante para el diseño del directorio y la organización de la prueba será: ¿Termina con subproyectos? Si es así, necesita más trabajo al configurar CMakeLists y debe dividir sus subproyectos en subdirectorios, cada uno con sus propios archivos includey src. Tal vez incluso sus propias ejecuciones y salidas de doxygen (es posible combinar varios proyectos de doxygen, pero no es fácil ni bonito).

Terminarás con algo como esto:

└── 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

dónde

  • (1) configura dependencias, especificaciones de plataforma y rutas de salida
  • (2) configura la biblioteca que va a construir
  • (3) configura los ejecutables de prueba y los casos de prueba

En caso de que tenga subcomponentes, sugeriría agregar otra jerarquía y usar el árbol de arriba para cada subproyecto. Entonces las cosas se ponen complicadas, porque necesita decidir si los subcomponentes buscan y configuran sus dependencias o si lo hace en el nivel superior. Esto debe decidirse caso por caso.

Doxygen: después de que lograste pasar por el baile de configuración de doxygen, es trivial usar CMake add_custom_commandpara agregar un objetivo de documento.

Así es como terminan mis proyectos y he visto algunos proyectos muy similares, pero por supuesto esto no lo cura todo.

Anexo En algún momento, querrá generar un config.hpp archivo que contenga una definición de versión y tal vez una definición de algún identificador de control de versión (un hash de Git o un número de revisión SVN). CMake tiene módulos para automatizar la búsqueda de esa información y generar archivos. Puede usar CMake configure_filepara reemplazar variables en un archivo de plantilla con variables definidas dentro de CMakeLists.txt.

Si está creando bibliotecas, también necesitará una definición de exportación para diferenciar correctamente los compiladores, por ejemplo, __declspecen MSVC y los visibilityatributos en GCC / clang.

pmr
fuente
2
Buena respuesta, pero todavía no tengo claro por qué necesita poner sus archivos de encabezado en un subdirectorio adicional llamado proyecto: "/prj/include/prj/foo.hpp", que me parece redundante. ¿Por qué no solo "/prj/include/foo.hpp"? Supongo que tendrá la oportunidad de volver a configurar los directorios de instalación en el momento de la instalación, por lo que obtendrá <INSTALL_DIR> /include/prj/foo.hpp cuando lo instale, ¿o es tan difícil en CMake?
William Payne
6
@William Eso es realmente difícil de hacer con CPack. Además, ¿cómo se verían sus archivos de origen incluidos dentro? Si son solo "header.hpp" en una versión instalada, "/ usr / include / prj /" debe estar en la ruta de inclusión en lugar de solo "/ usr / include".
pmr
37

Para empezar, existen algunos nombres convencionales para directorios que no puede ignorar, estos se basan en la larga tradición del sistema de archivos Unix. Estos son:

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

Probablemente sea una buena idea ceñirse a este diseño básico, al menos en el nivel superior.

Acerca de dividir los archivos de encabezado y los archivos de origen (cpp), ambos esquemas son bastante comunes. Sin embargo, tiendo a preferir mantenerlos juntos, simplemente es más práctico en las tareas del día a día tener los archivos juntos. Además, cuando todo el código está en una carpeta de nivel superior, es decir, la trunk/src/carpeta, puede observar que todas las demás carpetas (bin, lib, include, doc y tal vez alguna carpeta de prueba) en el nivel superior, además de el directorio de "compilación" para una compilación fuera de la fuente, son todas las carpetas que contienen nada más que archivos que se generan en el proceso de compilación. Y, por lo tanto, solo se debe hacer una copia de seguridad de la carpeta src, o mucho mejor, mantenerla en un sistema / servidor de control de versiones (como Git o SVN).

Y cuando se trata de instalar sus archivos de encabezado en el sistema de destino (si desea distribuir eventualmente su biblioteca), bueno, CMake tiene un comando para instalar archivos (crea implícitamente un destino de "instalación", para hacer "hacer instalar") que puede usar para poner todos los encabezados en el /usr/include/directorio. Solo uso la siguiente macro cmake para este propósito:

# 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)

¿Dónde SRCROOTestá una variable cmake que configuré en la carpeta src, y INCLUDEROOTes la variable cmake que configuro donde deben ir los encabezados? Por supuesto, hay muchas otras formas de hacer esto, y estoy seguro de que la mía no es la mejor. El punto es que no hay razón para dividir los encabezados y las fuentes solo porque solo los encabezados deben instalarse en el sistema de destino, porque es muy fácil, especialmente con CMake (o CPack), seleccionar y configurar los encabezados para instalarse sin tener que tenerlos en un directorio aparte. Y esto es lo que he visto en la mayoría de las bibliotecas.

Cita: En segundo lugar, me gustaría utilizar Google C ++ Testing Framework para realizar pruebas unitarias de mi código, ya que parece bastante fácil de usar. ¿Sugieres incluir esto con mi código, por ejemplo, en una carpeta "inc / gtest" o "contrib / gtest"? Si está incluido, ¿sugiere usar el script fuse_gtest_files.py para reducir el número de archivos, o dejarlo como está? Si no está incluido, ¿cómo se maneja esta dependencia?

No agrupe dependencias con su biblioteca. Esta es generalmente una idea bastante horrible, y siempre odio cuando estoy atascado tratando de construir una biblioteca que lo haga. Debe ser su último recurso y tenga cuidado con las trampas. A menudo, las personas agrupan dependencias con su biblioteca ya sea porque se dirigen a un entorno de desarrollo terrible (por ejemplo, Windows) o porque solo admiten una versión antigua (obsoleta) de la biblioteca (dependencia) en cuestión. El problema principal es que su dependencia empaquetada podría chocar con versiones ya instaladas de la misma biblioteca / aplicación (por ejemplo, usted empaquetó gtest, pero la persona que intenta construir su biblioteca ya tiene una versión más nueva (o anterior) de gtest ya instalada, entonces los dos podrían chocar y darle a esa persona un dolor de cabeza muy desagradable). Entonces, como dije, hazlo bajo tu propio riesgo, y yo diría que solo como último recurso. Pedirle a la gente que instale algunas dependencias antes de poder compilar su biblioteca es un mal mucho menor que tratar de resolver los conflictos entre las dependencias agrupadas y las instalaciones existentes.

Cita: Cuando se trata de pruebas de redacción, ¿cómo se organizan en general? Estaba pensando en tener un archivo cpp para cada clase (test_vector3.cpp por ejemplo) pero todos compilados en un binario para que todos puedan ejecutarse juntos fácilmente.

Un archivo cpp por clase (o un pequeño grupo cohesivo de clases y funciones) es más habitual y práctico en mi opinión. Sin embargo, definitivamente, no los compile todos en un binario sólo para que "se puedan ejecutar todos juntos". Esa es una muy mala idea. Generalmente, cuando se trata de codificación, desea dividir las cosas tanto como sea razonable para hacerlo. En el caso de las pruebas unitarias, no desea que un binario ejecute todas las pruebas, porque eso significa que cualquier pequeño cambio que realice en cualquier cosa en su biblioteca probablemente provocará una recompilación casi total de ese programa de prueba unitaria. , y eso es solo minutos / horas perdidas esperando la recompilación. Siga un esquema simple: 1 unidad = 1 programa de prueba de unidad. Luego,

Cita: Dado que la biblioteca gtest generalmente se construye usando cmake y make, estaba pensando que tendría sentido que mi proyecto también se construyera así. Si decidiera usar el siguiente diseño de proyecto:

Preferiría sugerir este diseño:

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

Algunas cosas para notar aquí. Primero, los subdirectorios de su directorio src deben reflejar los subdirectorios de su directorio de inclusión, esto es solo para mantener las cosas intuitivas (también, intente mantener la estructura de su subdirectorio razonablemente plana (superficial), porque la anidación profunda de carpetas es a menudo más complicado que cualquier otra cosa). En segundo lugar, el directorio "incluir" es solo un directorio de instalación, su contenido son solo los encabezados que se seleccionan del directorio src.

En tercer lugar, el sistema CMake está diseñado para distribuirse en los subdirectorios de origen, no como un archivo CMakeLists.txt en el nivel superior. Esto mantiene las cosas locales, y eso es bueno (con el espíritu de dividir las cosas en partes independientes). Si agrega una nueva fuente, un nuevo encabezado o un nuevo programa de prueba, todo lo que necesita es editar un archivo CMakeLists.txt pequeño y simple en el subdirectorio en cuestión, sin afectar nada más. Esto también le permite reestructurar los directorios con facilidad (las CMakeLists son locales y están contenidas en los subdirectorios que se están moviendo). Las CMakeLists de nivel superior deben contener la mayoría de las configuraciones de nivel superior, como la configuración de directorios de destino, comandos personalizados (o macros) y búsqueda de paquetes instalados en el sistema. Las CMakeLists de nivel inferior deben contener solo listas simples de encabezados, fuentes,

Cita: ¿Cómo debería verse CMakeLists.txt para que pueda construir solo la biblioteca o la biblioteca y las pruebas?

La respuesta básica es que CMake le permite excluir específicamente ciertos objetivos de "todos" (que es lo que se crea cuando escribe "hacer"), y también puede crear paquetes específicos de objetivos. No puedo hacer un tutorial de CMake aquí, pero es bastante sencillo descubrirlo usted mismo. En este caso específico, sin embargo, la solución recomendada es, por supuesto, usar CTest, que es solo un conjunto adicional de comandos que puede usar en los archivos CMakeLists para registrar una cantidad de objetivos (programas) que están marcados como unidad- pruebas. Entonces, CMake pondrá todas las pruebas en una categoría especial de compilaciones, y eso es exactamente lo que solicitó, entonces, problema resuelto.

Cita: También he visto bastantes proyectos que tienen un anuncio de compilación en un directorio bin. ¿La compilación ocurre en el directorio de compilación y luego los binarios se trasladan al directorio bin? ¿Los binarios de las pruebas y la biblioteca vivirían en el mismo lugar? ¿O tendría más sentido estructurarlo de la siguiente manera:

Tener un directorio de compilación fuera de la fuente (compilación "fuera de la fuente") es realmente la única cosa sensata que se puede hacer, es el estándar de facto en estos días. Entonces, definitivamente, tenga un directorio de "compilación" separado, fuera del directorio de origen, tal como lo recomienda la gente de CMake, y como lo hacen todos los programadores que he conocido. En cuanto al directorio bin, bueno, eso es una convención, y probablemente sea una buena idea ceñirse a él, como dije al principio de esta publicación.

Cita: También me gustaría usar doxygen para documentar mi código. ¿Es posible hacer que esto se ejecute automáticamente con cmake y make?

Si. Es más que posible, es asombroso. Dependiendo de lo elegante que quieras ponerte, hay varias posibilidades. CMake tiene un módulo para Doxygen (es decir, find_package(Doxygen)) que le permite registrar objetivos que ejecutarán Doxygen en algunos archivos. Si desea hacer cosas más sofisticadas, como actualizar el número de versión en el Doxyfile, o ingresar automáticamente un sello de fecha / autor para los archivos de origen, etc., todo es posible con un poco de CMake kung-fu. Generalmente, hacer esto implicará que mantengas un Doxyfile de origen (por ejemplo, el "Doxyfile.in" que puse en el diseño de la carpeta anterior) que tiene tokens para ser encontrados y reemplazados por los comandos de análisis de CMake. En mi archivo CMakeLists de nivel superior , encontrará una pieza de CMake kung-fu que hace algunas cosas elegantes con cmake-doxygen juntos.

Mikael Persson
fuente
Entonces main.cpp, ¿ debería ir a trunk/bin?
Ugnius Malūkas
17

Estructurando el proyecto

En general, favorecería lo siguiente:

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

Esto significa que tiene un conjunto de archivos API claramente definido para su biblioteca, y la estructura significa que los clientes de su biblioteca lo harían

#include "project/vector3.hpp"

en lugar de lo menos explícito

#include "vector3.hpp"


Me gusta que la estructura del árbol / src coincida con la del árbol / include, pero esa es una preferencia personal. Sin embargo, si su proyecto se expande para contener subdirectorios dentro de / include / project, generalmente sería útil hacer coincidir los que están dentro del árbol / src.

Para las pruebas, prefiero mantenerlos "cerca" de los archivos que prueban, y si terminas con subdirectorios dentro de / src, es un paradigma bastante fácil de seguir para otros si quieren encontrar el código de prueba de un archivo dado.


Pruebas

En segundo lugar, me gustaría utilizar Google C ++ Testing Framework para realizar pruebas unitarias de mi código, ya que parece bastante fácil de usar.

Gtest es realmente fácil de usar y es bastante completo en términos de sus capacidades. Se puede usar junto con gmock muy fácilmente para ampliar sus capacidades, pero mis propias experiencias con gmock han sido menos favorables. Estoy bastante preparado para aceptar que esto puede deberse a mis propias deficiencias, pero las pruebas gmock tienden a ser más difíciles de crear y mucho más frágiles / difíciles de mantener. Un gran clavo en el ataúd de gmock es que realmente no funciona bien con punteros inteligentes.

Esta es una respuesta muy trivial y subjetiva a una gran pregunta (que probablemente no pertenezca realmente a SO)

¿Sugieres incluir esto con mi código, por ejemplo, en una carpeta "inc / gtest" o "contrib / gtest"? Si está incluido, ¿sugiere usar el script fuse_gtest_files.py para reducir el número de archivos, o dejarlo como está? Si no está incluido, ¿cómo se maneja esta dependencia?

Prefiero usar el ExternalProject_Addmódulo de CMake . Esto le evita tener que mantener el código fuente de gtest en su repositorio o instalarlo en cualquier lugar. Se descarga y se integra en su árbol de construcción automáticamente.

Vea mi respuesta que trata con los detalles aquí .

Cuando se trata de pruebas de redacción, ¿cómo se organizan en general? Estaba pensando en tener un archivo cpp para cada clase (test_vector3.cpp por ejemplo) pero todos compilados en un binario para que todos puedan ejecutarse juntos fácilmente.

Buen plan.


edificio

Soy fanático de CMake, pero al igual que con sus preguntas relacionadas con las pruebas, SO probablemente no sea el mejor lugar para pedir opiniones sobre un tema tan subjetivo.

¿Cómo debería verse CMakeLists.txt para que pueda construir solo la biblioteca o la biblioteca y las pruebas?

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

La biblioteca aparecerá como un "ProjectLibrary" de destino y el conjunto de pruebas como un "ProjectTest" de destino. Al especificar la biblioteca como una dependencia del exe de prueba, la creación del exe de prueba hará que la biblioteca se reconstruya automáticamente si está desactualizada.

También he visto bastantes proyectos que tienen un anuncio de compilación en un directorio bin. ¿La compilación ocurre en el directorio de compilación y luego los binarios se trasladan al directorio bin? ¿Los binarios de las pruebas y la biblioteca vivirían en el mismo lugar?

CMake recomienda compilaciones "fuera de origen", es decir, crea su propio directorio de compilación fuera del proyecto y ejecuta CMake desde allí. Esto evita "contaminar" su árbol de fuentes con archivos de compilación, y es muy conveniente si está utilizando un vcs.

Usted puede especificar que los binarios se mueven o copian a un directorio diferente, una vez construido, o que se crean por defecto en otro directorio, pero por lo general hay ninguna necesidad. CMake proporciona formas completas de instalar su proyecto si lo desea, o facilitar que otros proyectos de CMake "encuentren" los archivos relevantes de su proyecto.

Con respecto al soporte propio de CMake para encontrar y ejecutar pruebas de gtest , esto sería en gran medida inapropiado si crea gtest como parte de su proyecto. El FindGtestmódulo está realmente diseñado para usarse en el caso de que gtest se haya construido por separado fuera de su proyecto.

CMake proporciona su propio marco de prueba (CTest) e, idealmente, cada caso de gtest se agregaría como un caso de CTest.

Sin embargo, la GTEST_ADD_TESTSmacro proporcionada por FindGtestpara permitir la adición fácil de casos de gtest como casos de ctest individuales es algo deficiente, ya que no funciona para macros de gtest que no sean TESTy TEST_F. De Valor o parametrizados-Tipo pruebas usando TEST_P, TYPED_TEST_P, etc, no se manejan en absoluto.

El problema no tiene una solución fácil que yo sepa. La forma más sólida de obtener una lista de casos de gtest es ejecutar el exe de prueba con la bandera --gtest_list_tests. Sin embargo, esto solo se puede hacer una vez que se crea el exe, por lo que CMake no puede hacer uso de esto. Lo que te deja con dos opciones; CMake debe tratar de analizar el código C ++ para deducir los nombres de las pruebas (no trivial en el extremo si desea tener en cuenta todas las macros de gtest, las pruebas comentadas, las pruebas deshabilitadas), o los casos de prueba se agregan manualmente al Archivo CMakeLists.txt.

También me gustaría usar doxygen para documentar mi código. ¿Es posible hacer que esto se ejecute automáticamente con cmake y make?

Sí, aunque no tengo experiencia en este frente. CMake proporciona FindDoxygeneste propósito.

Fraser
fuente
6

Además de las otras (excelentes) respuestas, voy a describir una estructura que he estado usando para proyectos relativamente a gran escala .
No voy a abordar la subpregunta sobre Doxygen, ya que solo repetiría lo que se dice en las otras respuestas.


Razón fundamental

Por modularidad y mantenibilidad, el proyecto está organizado como un conjunto de pequeñas unidades. Para mayor claridad, llamémoslos UnitX, con X = A, B, C, ... (pero pueden tener cualquier nombre general). Luego, la estructura del directorio se organiza para reflejar esta elección, con la posibilidad de agrupar unidades si es necesario.

Solución

El diseño básico del directorio es el siguiente (el contenido de las unidades se detalla más adelante):

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

project/CMakeLists.txt podría contener lo siguiente:

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)

y project/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)

y project/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)

Ahora a la estructura de las diferentes unidades (tomemos, como ejemplo, 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]

A los diferentes componentes:

  • Me gusta tener source ( .cpp) y headers ( .h) en la misma carpeta. Esto evita una jerarquía de directorios duplicada y facilita el mantenimiento. Para la instalación, no es un problema (especialmente con CMake) filtrar los archivos de encabezado.
  • La función del directorio UnitDes permitir más adelante incluir archivos con #include <UnitD/ClassA.h>. Además, al instalar esta unidad, puede copiar la estructura del directorio tal como está. Tenga en cuenta que también puede organizar sus archivos de origen en subdirectorios.
  • Me gusta un READMEarchivo para resumir de qué se trata la unidad y especificar información útil sobre ella.
  • CMakeLists.txt podría simplemente contener:

    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
                          )

    Aquí, tenga en cuenta que no es necesario decirle a CMake que queremos los directorios de inclusión para UnitAy UnitC, ya que esto ya se especificó al configurar esas unidades. Además, PUBLICle dirá a todos los objetivos que dependen de UnitDque deben incluir automáticamente la UnitAdependencia, mientras UnitCque no será necesario entonces ( PRIVATE).

  • test/CMakeLists.txt (vea más abajo si desea utilizar GTest para ello):

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

Usando GoogleTest

Para Google Test, lo más fácil es si su fuente está presente en algún lugar de su directorio de fuentes, pero no tiene que agregarla allí usted mismo. He estado usando este proyecto para descargarlo automáticamente, y envuelvo su uso en una función para asegurarme de que se descargue solo una vez, aunque tenemos varios objetivos de prueba.

Esta función de CMake es la siguiente:

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()

y luego, cuando quiera usarlo dentro de uno de mis objetivos de prueba, agregaré las siguientes líneas al CMakeLists.txt(esto es para el ejemplo anterior test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)
oLen
fuente
¡Buen "truco" que hiciste allí con Gtest y cmake! ¡Útil! :)
Tanasis