CMake y búsqueda de otros proyectos y sus dependencias

76

Imagine el siguiente escenario: el Proyecto A es una biblioteca compartida que tiene varias dependencias (LibA, LibB y LibC). El proyecto B es un ejecutable que depende del proyecto A y, por lo tanto, también requiere todas las dependencias del proyecto A para poder construir.

Además, ambos proyectos se construyen usando CMake, y el Proyecto A no debería necesitar ser instalado (a través del destino 'instalar') para que el Proyecto B lo use, ya que esto puede convertirse en una molestia para los desarrolladores.

¿Cuál es la mejor manera de resolver estas dependencias usando CMake? La solución ideal sería lo más simple posible (aunque no más simple) y requeriría un mantenimiento mínimo.

blockchaindev
fuente
2
Para la prosperidad futura: cmake.org/Wiki/CMake/Tutorials/…
blockchaindev
1
Ese tutorial no parece explicar cómo manejar la exportación de dependencias de bibliotecas externas. La única biblioteca vinculada es una construida por el proyecto. Necesito saber cómo decirle al Proyecto B que el Proyecto A requiere varias bibliotecas externas y, por lo tanto, estas deben agregarse al paso de enlace del Proyecto B.
Ben Farmer
En realidad, deberías probar el subsistema Linux o Linux si eres un tipo de PC. Lo mejor de esta plataforma es que Linux instalará todas las dependencias por ti. O mejor aún, sugiere qué dependencias le faltan y proporciona Sudo apt-get install mydependencies, cómo instalar. Realmente fácil.
Juniar
@Juniar, eso simplifica y optimiza mucho las cosas, estoy de acuerdo. Pero hace que la implementación de software sea una pesadilla. Preferiría tener todo en un paquete para mi software e implementarlo todo junto (incluso duplicando parcialmente algunas bibliotecas). Sin mencionar los problemas de mantenimiento. Cada caja tendrá un conjunto único de bibliotecas (hasta cierto punto).
OpalApps
@OpalApps, las dependencias pueden instalarse en diferentes rutas y directorios, sin embargo, aún puede agregar estas dependencias en tiempo de compilación, o configurar / incluir rutas externas. No se instalarán todos en una ruta. Verdadero, sin embargo, "sudo apt-get install" se instala en directorios específicos, simplemente cámbielos.
Juniar

Respuestas:

147

Fácil. Aquí está el ejemplo de la parte superior de mi cabeza:

El nivel superior CMakeLists.txt:

cmake_minimum_required(VERSION 2.8.10)

# You can tweak some common (for all subprojects) stuff here. For example:

set(CMAKE_DISABLE_IN_SOURCE_BUILD ON)
set(CMAKE_DISABLE_SOURCE_CHANGES  ON)

if ("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
  message(SEND_ERROR "In-source builds are not allowed.")
endif ()

set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_COLOR_MAKEFILE   ON)

# Remove 'lib' prefix for shared libraries on Windows
if (WIN32)
  set(CMAKE_SHARED_LIBRARY_PREFIX "")
endif ()

# When done tweaking common stuff, configure the components (subprojects).
# NOTE: The order matters! The most independent ones should go first.
add_subdirectory(components/B) # B is a static library (depends on Boost)
add_subdirectory(components/C) # C is a shared library (depends on B and external XXX)
add_subdirectory(components/A) # A is a shared library (depends on C and B)

add_subdirectory(components/Executable) # Executable (depends on A and C)

CMakeLists.txten components/B:

cmake_minimum_required(VERSION 2.8.10)

project(B C CXX)

find_package(Boost
             1.50.0
             REQUIRED)

file(GLOB CPP_FILES source/*.cpp)

include_directories(${Boost_INCLUDE_DIRS})

add_library(${PROJECT_NAME} STATIC ${CPP_FILES})

# Required on Unix OS family to be able to be linked into shared libraries.
set_target_properties(${PROJECT_NAME}
                      PROPERTIES POSITION_INDEPENDENT_CODE ON)

target_link_libraries(${PROJECT_NAME})

# Expose B's public includes (including Boost transitively) to other
# subprojects through cache variable.
set(${PROJECT_NAME}_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/include
                                 ${Boost_INCLUDE_DIRS}
    CACHE INTERNAL "${PROJECT_NAME}: Include Directories" FORCE)

CMakeLists.txten components/C:

cmake_minimum_required(VERSION 2.8.10)

project(C C CXX)

find_package(XXX REQUIRED)

file(GLOB CPP_FILES source/*.cpp)

add_definitions(${XXX_DEFINITIONS})

# NOTE: Boost's includes are transitively added through B_INCLUDE_DIRS.
include_directories(${B_INCLUDE_DIRS}
                    ${XXX_INCLUDE_DIRS})

add_library(${PROJECT_NAME} SHARED ${CPP_FILES})

target_link_libraries(${PROJECT_NAME} B
                                      ${XXX_LIBRARIES})

# Expose C's definitions (in this case only the ones of XXX transitively)
# to other subprojects through cache variable.
set(${PROJECT_NAME}_DEFINITIONS ${XXX_DEFINITIONS}
    CACHE INTERNAL "${PROJECT_NAME}: Definitions" FORCE)

# Expose C's public includes (including the ones of C's dependencies transitively)
# to other subprojects through cache variable.
set(${PROJECT_NAME}_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/include
                                 ${B_INCLUDE_DIRS}
                                 ${XXX_INCLUDE_DIRS}
    CACHE INTERNAL "${PROJECT_NAME}: Include Directories" FORCE)

CMakeLists.txten components/A:

cmake_minimum_required(VERSION 2.8.10)

project(A C CXX)

file(GLOB CPP_FILES source/*.cpp)

# XXX's definitions are transitively added through C_DEFINITIONS.
add_definitions(${C_DEFINITIONS})

# NOTE: B's and Boost's includes are transitively added through C_INCLUDE_DIRS.
include_directories(${C_INCLUDE_DIRS})

add_library(${PROJECT_NAME} SHARED ${CPP_FILES})

# You could need `${XXX_LIBRARIES}` here too, in case if the dependency 
# of A on C is not purely transitive in terms of XXX, but A explicitly requires
# some additional symbols from XXX. However, in this example, I assumed that 
# this is not the case, therefore A is only linked against B and C.
target_link_libraries(${PROJECT_NAME} B
                                      C)

# Expose A's definitions (in this case only the ones of C transitively)
# to other subprojects through cache variable.
set(${PROJECT_NAME}_DEFINITIONS ${C_DEFINITIONS}
    CACHE INTERNAL "${PROJECT_NAME}: Definitions" FORCE)

# Expose A's public includes (including the ones of A's dependencies
# transitively) to other subprojects through cache variable.
set(${PROJECT_NAME}_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/include
                                 ${C_INCLUDE_DIRS}
    CACHE INTERNAL "${PROJECT_NAME}: Include Directories" FORCE)

CMakeLists.txten components/Executable:

cmake_minimum_required(VERSION 2.8.10)

project(Executable C CXX)

file(GLOB CPP_FILES source/*.cpp)

add_definitions(${A_DEFINITIONS})

include_directories(${A_INCLUDE_DIRS})

add_executable(${PROJECT_NAME} ${CPP_FILES})

target_link_libraries(${PROJECT_NAME} A C)

Para que quede claro, aquí está la estructura de árbol de origen correspondiente:

Root of the project
├───components
│   ├───Executable
│   │   ├───resource
│   │   │   └───icons
│   │   ├───source
|   |   └───CMakeLists.txt
│   ├───A
│   │   ├───include
│   │   │   └───A
│   │   ├───source
|   |   └───CMakeLists.txt
│   ├───B
│   │   ├───include
│   │   │   └───B
│   │   ├───source
|   |   └───CMakeLists.txt
│   └───C
│       ├───include
│       │   └───C
│       ├───source
|       └───CMakeLists.txt
└───CMakeLists.txt

Hay muchos puntos en los que esto podría ajustarse / personalizarse o cambiarse para satisfacer ciertas necesidades, pero esto al menos debería ayudarlo a comenzar.

NOTA: He empleado con éxito esta estructura en varios proyectos de tamaño mediano y grande.

Alexander Shukaev
fuente
15
¡Eres una F **** STAR! realmente me sacaste de un enorme dolor de cabeza que duró casi un día. Muchas gracias 1
rsacchettini
2
Tengo curiosidad, ¿se compilaría si invocara Cmake desde el directorio "Ejecutable"? ¿O debo compilar desde la raíz del proyecto en todo momento?
Ahmet Ipkin
1
Creo que tiene el pequeño inconveniente de que define en cada proyecto los directorios de inclusión dos veces (una vez para set(...INCLUDE_DIRSy una vez para include_directories()), que encuentro difícil de mantener (siempre recordando agregar una nueva dependencia de inclusión en dos lugares). Podrías interrogarlos con get_property(...PROPERTY INCLUDE_DIRECTORIES).
pseyfert
2
Esto es realmente genial, pero ¿cómo se puede lograr lo mismo cuando A, B y C son proyectos completamente separados? Es decir, quiero compilar A y exportarlo para tener su propio archivo ProjectConfig.cmake, y luego en B usar find_package para encontrar A en el sistema, obteniendo de alguna manera una lista de todas las bibliotecas de las que A depende para que puedan vincularse cuando edificio B.
Ben Farmer
2
@Ben Farmer, este es un tema más complicado que merece un gran artículo para explicarlo correctamente. Nunca he tenido la paciencia suficiente para resumirlo aquí. De hecho, eso es lo que hago para administrar mis proyectos, ya que este es en realidad el uso y la intención (profesional) final de CMake. Para administrar todo eso, tengo mi propio marco CMake que maneja muchas cosas detrás de escena. Como ejemplo, podría intentar construir uno de mis proyectos de juguete C ++ Hacks o C ++ Firewall .
Alexander Shukaev
15

Alexander Shukaev tuvo un gran comienzo, pero hay varias cosas que podrían hacerse mejor:

  1. No utilice include_directories. Por lo menos, use target_include_directories. Sin embargo, probablemente ni siquiera necesite hacer eso si usa los destinos importados.
  2. Utilice los objetivos importados. Ejemplo de impulso:

    find_package(Boost 1.56 REQUIRED COMPONENTS
                 date_time filesystem iostreams)
    add_executable(foo foo.cc)
    target_link_libraries(foo
      PRIVATE
        Boost::date_time
        Boost::filesystem
        Boost::iostreams
    )
    

    Esto se encarga de los directorios de inclusión, bibliotecas, etc. Si usó Boost en sus encabezados en B, entonces en lugar de PRIVATE, use PUBLIC, y estas dependencias se agregarán transitivamente a lo que dependa de B.

  3. No utilice el archivo globular (a menos que utilice 3.12). Hasta hace muy poco, el almacenamiento global de archivos solo funcionaba durante el tiempo de configuración, por lo que si agrega archivos y compila, no podrá detectar los cambios hasta que regenere explícitamente el proyecto. Sin embargo, si enumera los archivos directamente e intenta compilar, debería reconocer que la configuración está desactualizada y regenerarse automáticamente en el paso de compilación.

Hay una buena charla aquí (YouTube): C ++ Now 2017: Daniel Pfeifer "Effective CMake"

Lo que cubre una idea de administrador de paquetes que permite que su CMake de nivel raíz funcione con find_packageOR subdirectory, sin embargo, he estado tratando de adoptar la ideología de esto y estoy teniendo grandes problemas find_packagepara usarlo para todo y tener una estructura de directorio como la suya.

johnb003
fuente