CMake: как создавать внешние проекты и включать их цели

У меня есть проект A, который экспортирует статическую библиотеку в качестве цели:

install(TARGETS alib DESTINATION lib EXPORT project_a-targets)
install(EXPORT project_a-targets DESTINATION lib/alib)

Теперь я хочу использовать Проект A как внешний проект из Проекта B и включить его встроенные цели:

ExternalProject_Add(project_a
  URL ...project_a.tar.gz
  PREFIX ${CMAKE_CURRENT_BINARY_DIR}/project_a
  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)

include(${CMAKE_CURRENT_BINARY_DIR}/lib/project_a/project_a-targets.cmake)

Проблема в том, что включаемый файл еще не существует при запуске CMakeLists из проекта B.

Есть ли способ сделать включение зависимым от внешнего проекта, который создается?

Обновление: Я написал краткое руководство по CMake by Example, основанное на этой и других распространенных проблемах, с которыми я столкнулся.

Ответ 1

Думаю, вы смешиваете две разные парадигмы.

Как вы отметили, гибкий модуль ExternalProject запускает свои команды во время сборки, поэтому вы не можете напрямую использовать проект Файл импорта, поскольку он был создан только после того, как был установлен проект A.

Если вы хотите include Файл проекта Project A, вам нужно будет установить Project A вручную перед вызовом Project B CMakeLists.txt - как и любая другая сторонняя зависимость, добавленная таким образом, или через find_file/find_library/find_package.

Если вы хотите использовать ExternalProject_Add, вам нужно добавить что-то вроде следующего в ваш CMakeLists.txt:

ExternalProject_Add(project_a
  URL ...project_a.tar.gz
  PREFIX ${CMAKE_CURRENT_BINARY_DIR}/project_a
  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)

include(${CMAKE_CURRENT_BINARY_DIR}/lib/project_a/project_a-targets.cmake)

ExternalProject_Get_Property(project_a install_dir)
include_directories(${install_dir}/include)

add_dependencies(project_b_exe project_a)
target_link_libraries(project_b_exe ${install_dir}/lib/alib.lib)

Ответ 2

Этот пост имеет разумный ответ:

CMakeLists.txt.in:

cmake_minimum_required(VERSION 2.8.2)

project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
  GIT_REPOSITORY    https://github.com/google/googletest.git
  GIT_TAG           master
  SOURCE_DIR        "${CMAKE_BINARY_DIR}/googletest-src"
  BINARY_DIR        "${CMAKE_BINARY_DIR}/googletest-build"
  CONFIGURE_COMMAND ""
  BUILD_COMMAND     ""
  INSTALL_COMMAND   ""
  TEST_COMMAND      ""
)

CMakeLists.txt:

# Download and unpack googletest at configure time
configure_file(CMakeLists.txt.in
               googletest-download/CMakeLists.txt)
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" .
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download )
execute_process(COMMAND ${CMAKE_COMMAND} --build .
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download )

# Prevent GoogleTest from overriding our compiler/linker options
# when building with Visual Studio
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

# Add googletest directly to our build. This adds
# the following targets: gtest, gtest_main, gmock
# and gmock_main
add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src
                 ${CMAKE_BINARY_DIR}/googletest-build)

# The gtest/gmock targets carry header search path
# dependencies automatically when using CMake 2.8.11 or
# later. Otherwise we have to add them here ourselves.
if (CMAKE_VERSION VERSION_LESS 2.8.11)
  include_directories("${gtest_SOURCE_DIR}/include"
                      "${gmock_SOURCE_DIR}/include")
endif()

# Now simply link your own targets against gtest, gmock,
# etc. as appropriate

Однако это кажется довольно хакерским. Я хотел бы предложить альтернативное решение - использовать подмодули Git.

cd MyProject/dependencies/gtest
git submodule add https://github.com/google/googletest.git
cd googletest
git checkout release-1.8.0
cd ../../..
git add *
git commit -m "Add googletest"

Затем в MyProject/dependencies/gtest/CMakeList.txt вы можете сделать что-то вроде:

cmake_minimum_required(VERSION 3.3)

if(TARGET gtest) # To avoid diamond dependencies; may not be necessary depending on you project.
    return()
endif()

add_subdirectory("googletest")

Я еще не пробовал это широко, но это кажется чище.

Изменение: у этого подхода есть и обратная сторона: подкаталог может запускать команды install() которые вам не нужны. В этом посте есть подход к их отключению, но он глючил и не работал для меня.

Изменить 2: Если вы используете add_subdirectory("googletest" EXCLUDE_FROM_ALL) это означает, что команды install() в подкаталоге по умолчанию не используются.

Ответ 3

Вы также можете принудительно построить зависимую цель во вторичном make-процессе

См. мой ответ по теме.

Ответ 4

То, что вы можете попробовать сделать, это использовать команду cmake export внутри вашего project_a. Он работает немного иначе, чем команда install с EXPORT option в том project_a-targets.cmake при запуске cmake он создает ваш файл project_a-targets.cmake target.cmake. Сгенерированные цели импорта в файле project_a-targets.cmake изначально указывают на несуществующие библиотечные файлы в двоичном каталоге вашего проекта, которые будут сгенерированы только после выполнения команды build.

Чтобы лучше понять, о чем я говорю, просто создайте небольшой проект cmake, который создает простую библиотеку, а затем команду экспорта (код ниже не был протестирован):

add_library (project_a lib.cpp)
export (
  TARGETS 
    project_a
  FILE
    project_a-targets.cmake
)

После запуска команды cmake на вашем простом примере вы сможете найти project_a-targets.cmake внутри вашего двоичного каталога (или в одной из его дочерних папок). Осматривая файл, вы можете заметить, что в данный момент он указывает на несуществующий файл библиотеки. Только после запуска команды сборки будет библиотека.

Итак, возвращаясь к вашей проблеме, вам нужно обновить project-a CMakeLists.txt включив в него команду export. Затем вам нужно убедиться, что после обработки ExternalProject_Add он вызывает шаг настройки, который создаст project_a-targets.cmake, затем вы можете вызвать include(.../project_a-targets.cmake) который должен работать. Наконец, вам нужно будет добавить зависимость между project_b и project_a чтобы она project_a сборку project_b перед попыткой сборки project_b.

Ответ 5

cmake ExternalProject_Add действительно может использоваться, но что мне не понравилось в нем - это то, что он выполняет что-то во время сборки, непрерывного опроса и т.д. Я бы предпочел собрать проект во время фазы сборки, и ничего больше. Я пытался переопределить ExternalProject_Add в нескольких попытках, к сожалению, безуспешно.

Затем я попытался также добавить подмодуль git, но это перетаскивает весь репозиторий git, в то время как в некоторых случаях мне нужно только подмножество всего репозитория git. То, что я проверил - действительно возможно выполнить редкую проверку git, но для этого требуется отдельная функция, о которой я писал ниже.

#-----------------------------------------------------------------------------
#
# Performs sparse (partial) git checkout
#
#   into ${checkoutDir} from ${url} of ${branch}
#
# List of folders and files to pull can be specified after that.
#-----------------------------------------------------------------------------
function (SparseGitCheckout checkoutDir url branch)
    if(EXISTS ${checkoutDir})
        return()
    endif()

    message("-------------------------------------------------------------------")
    message("sparse git checkout to ${checkoutDir}...")
    message("-------------------------------------------------------------------")

    file(MAKE_DIRECTORY ${checkoutDir})

    set(cmds "git init")
    set(cmds ${cmds} "git remote add -f origin --no-tags -t master ${url}")
    set(cmds ${cmds} "git config core.sparseCheckout true")

    # This command is executed via file WRITE
    # echo <file or folder> >> .git/info/sparse-checkout")

    set(cmds ${cmds} "git pull --depth=1 origin ${branch}")

    # message("In directory: ${checkoutDir}")

    foreach( cmd ${cmds})
        message("- ${cmd}")
        string(REPLACE " " ";" cmdList ${cmd})

        #message("Outfile: ${outFile}")
        #message("Final command: ${cmdList}")

        if(pull IN_LIST cmdList)
            string (REPLACE ";" "\n" FILES "${ARGN}")
            file(WRITE ${checkoutDir}/.git/info/sparse-checkout ${FILES} )
        endif()

        execute_process(
            COMMAND ${cmdList}
            WORKING_DIRECTORY ${checkoutDir}
            RESULT_VARIABLE ret
        )

        if(NOT ret EQUAL "0")
            message("error: previous command failed, see explanation above")
            file(REMOVE_RECURSE ${checkoutDir})
            break()
        endif()
    endforeach()

endfunction()


SparseGitCheckout(${CMAKE_BINARY_DIR}/catch_197 https://github.com/catchorg/Catch2.git v1.9.7 single_include)
SparseGitCheckout(${CMAKE_BINARY_DIR}/catch_master https://github.com/catchorg/Catch2.git master single_include)

Я добавил два вызова функций ниже, чтобы проиллюстрировать, как использовать функцию.

Кто-то может не захотеть извлекать мастер/транк, так как он может быть сломан - тогда всегда можно указать конкретный тег.

Оформить заказ можно будет только один раз, пока вы не очистите папку кеша.