Немедленное создание инструмента для последующего его использования в том же цикле запуска CMake

У меня есть интересная проблема курицы и яйца и потенциальное решение (см. Мой опубликованный ответ), но это решение использует CMake необычным образом. Лучше альтернативы или комментарии будут приветствоваться.

ЭТА ПРОБЛЕМА:

Простая версия проблемы может быть описана как один проект CMake со следующими характеристиками:

  1. Одной из целей сборки является исполняемый файл командной строки, который я назову mycomp, источник которого находится в mycompdir и внесение каких-либо изменений в содержимое этого каталога невозможно.
  2. Проект содержит текстовые файлы (я назову их foo.my а также bar.my) которые должны запускаться на mycomp для создания набора исходных текстов и заголовков C++ и некоторых CMakeLists.txt файлы, определяющие библиотеки, созданные из этих источников.
  3. Другие цели сборки в том же проекте должны быть связаны с библиотеками, определенными созданными CMakeLists.txt файлы. Эти другие цели также имеют источники, которые #include некоторые из сгенерированных заголовков.

Вы можете думать о mycomp как о чем-то похожем на компилятор, а текстовые файлы на шаге 2 - как некие исходные файлы. Это представляет проблему, потому что CMake нуждается в CMakeLists.txt файлы во время настройки, но mycomp недоступен до времени сборки и поэтому недоступен при первом запуске для создания CMakeLists.txt файлы достаточно рано.

NON-ОТВЕТ:

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

1 ответ

Решение

Суть проблемы в том, что mycomp должен быть доступен при запуске CMake, чтобы сгенерированный CMakeLists.txt файлы могут быть созданы, а затем извлечены с add_subdirectory(), Возможный способ достичь этого - использовать execute_process() запустить вложенный cmake-and-build из основной сборки. Этот вложенный cmake-and-build будет использовать те же самые исходные и двоичные каталоги, что и CMake-запуск верхнего уровня (если только не происходит кросс-компиляция). Общая структура основного верхнего уровня CMakeLists.txt будет что-то вроде этого:

# Usual CMakeLists.txt setup stuff goes here...

if(EARLY_BUILD)
    # This is the nested build and we will only be asked to
    # build the mycomp target (see (c) below)
    add_subdirectory(mycompdir)

    # End immediately, we don't want anything else in the nested build
    return()
endif()

# This is the main build, setup and execute the nested build
# to ensure the mycomp executable exists before continuing

# (a) When cross compiling, we cannot re-use the same binary dir
#     because the host and target are different architectures
if(CMAKE_CROSSCOMPILING)
    set(workdir "${CMAKE_BINARY_DIR}/host")
    execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${workdir}")
else()
    set(workdir "${CMAKE_BINARY_DIR}")
endif()

# (b) Nested CMake run. May need more -D... options than shown here.
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}"
                        -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
                        -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
                        -DEARLY_BUILD=ON
                        ${CMAKE_SOURCE_DIR}
               WORKING_DIRECTORY "${workdir}")

# (c) Build just mycomp in the nested build. Don't specify a --config
#     because we cannot know what config the developer will be using
#     at this point. For non-multi-config generators, we've already
#     specified CMAKE_BUILD_TYPE above in (b).
execute_process(COMMAND ${CMAKE_COMMAND} --build . --target mycomp
                WORKING_DIRECTORY "${workdir}")

# (d) We want everything from mycompdir in our main build,
#     not just the mycomp target
add_subdirectory(mycompdir)

# (e) Run mycomp on the sources to generate a CMakeLists.txt in the
#     ${CMAKE_BINARY_DIR}/foobar directory. Note that because we want
#     to support cross compiling, working out the location of the
#     executable is a bit more tricky. We cannot know whether the user
#     wants debug or release build types for multi-config generators
#     so we have to choose one. We cannot query the target properties
#     because they are only known at generate time, which is after here.
#     Best we can do is hardcode some basic logic.
if(MSVC)
    set(mycompsuffix "Debug/mycomp.exe")
elseif(CMAKE_GENERATOR STREQUAL "Xcode")
    set(mycompsuffix "Debug/mycomp")
else()
    set(mycompsuffix "mycomp")
endif()
set(mycomp_EXECUTABLE "${workdir}/mycompdir/${mycompsuffix}")
execute_process(COMMAND "${mycomp_EXECUTABLE}" -outdir foobar ${CMAKE_SOURCE_DIR}/foo.my ${CMAKE_SOURCE_DIR}/bar.my)

# (f) Now pull that generated CMakeLists.txt into the main build.
#     It will create a CMake library target called foobar.
add_subdirectory(${CMAKE_BINARY_DIR}/foobar ${CMAKE_BINARY_DIR}/foobar-build)

# (g) Another target which links to the foobar library
#     and includes headers from there
add_executable(gumby gumby.cpp)
target_link_libraries(gumby PUBLIC foobar)
target_include_directories(gumby PUBLIC foobar)

Если мы не будем повторно использовать тот же двоичный каталог в (b) и (c), который мы используем для основной сборки, мы закончим сборку mycomp дважды, чего мы явно хотим избежать. Для кросс-компиляции мы не можем избежать этого, поэтому в таких случаях мы создаем mycomp инструмент в сторону в отдельном двоичном каталоге.

Я экспериментировал с вышеупомянутым подходом, и, похоже, он действительно работает в реальном проекте, который вызвал оригинальный вопрос, по крайней мере, для генераторов файлов Unix Makefiles, Ninja, Xcode (OS X и iOS) и Visual Studio. Частью привлекательности этого подхода является то, что он требует лишь небольшого количества кода, добавляемого только на верхний уровень CMakeLists.txt файл. Тем не менее, есть некоторые замечания, которые следует сделать:

  • Если команды компилятора или компоновщика для mycomp и его источники в любом случае различаются между вложенной сборкой и основной сборкой, mycomp цель заканчивает тем, что была восстановлена ​​во второй раз в (d). Если нет различий, mycomp создается только один раз, когда не выполняется кросс-компиляция, а это именно то, что нам нужно.
  • Я не вижу простого способа передать точно такие же аргументы для вложенного вызова CMake в (b), который был передан в запуск CMake верхнего уровня (в основном проблема, описанная здесь). чтение CMakeCache.txt не вариант, так как он не будет существовать при первом вызове, и он не даст вам никаких новых или измененных аргументов из текущего выполнения в любом случае. Лучшее, что я могу сделать, - это установить те переменные CMake, которые, я думаю, потенциально будут использоваться и которые могут повлиять на команды компилятора и компоновщика. mycomp, Это можно обойти, добавляя все больше и больше переменных по мере того, как я сталкиваюсь с необходимыми, но это не идеально.
  • При повторном использовании одного и того же двоичного каталога мы полагаемся на то, что CMake не начнет записывать какие-либо свои файлы в двоичный каталог до этапа генерации (ну, по крайней мере, до завершения сборки в (c)). Похоже, что с протестированными генераторами у нас все в порядке, но я не знаю, все ли генераторы на всех платформах также следуют этому поведению (и я не могу проверить каждую комбинацию, чтобы узнать!). Это та часть, которая вызывает у меня наибольшее беспокойство. Если кто-либо может подтвердить с помощью рассуждений и / или доказательств того, что это безопасно для всех генераторов и платформ, это было бы полезно (и стоило бы возразить, если вы хотите рассмотреть это как отдельный ответ).

ОБНОВЛЕНИЕ: После использования вышеупомянутой стратегии в ряде реальных проектов с персоналом различного уровня знания CMake, можно сделать некоторые наблюдения.

  • Наличие во вложенной сборке повторного использования того же каталога сборки, что и основная сборка, может иногда приводить к проблемам. В частности, если пользователь завершает запуск CMake после завершения вложенной сборки, но до выполнения основной сборки, CMakeCache.txt файл оставлен с EARLY_BUILD установлен в ON, Затем все последующие запуски CMake действуют как вложенная сборка, поэтому основная сборка по существу теряется до CMakeCache.txt файл удален вручную. Возможно, что ошибка где-то в одном из проектов CMakeLists.txt файл также может привести к аналогичной ситуации (неподтвержденные). Выполнение вложенной сборки в стороне в своем отдельном каталоге сборки работало очень хорошо, хотя без таких проблем.

  • Вложенная сборка, вероятно, должна быть Release, а не Debug. Если мы не будем повторно использовать тот же каталог сборки, что и основная сборка (теперь то, что я бы порекомендовал), нам больше не нужно пытаться избегать компиляции одного и того же файла дважды, поэтому мы также можем сделать mycomp максимально быстрым.

  • Используйте ccache, чтобы минимизировать любые затраты, связанные с повторной сборкой нескольких файлов с разными настройками. На самом деле, мы обнаружили, что использование ccache обычно делает вложенную сборку очень быстрой, поскольку она редко изменяется по сравнению с основной сборкой.

  • Вложенная сборка, вероятно, должна иметь CMAKE_BUILD_WITH_INSTALL_RPATH установлен в FALSE на некоторых платформах, так что любые библиотеки mycomp могут быть найдены без необходимости установки переменных окружения и т. д.

Другие вопросы по тегам