Самый простой, но полный пример CMake

Каким-то образом я полностью смущен тем, как работает CMake. Каждый раз, когда я думаю, что становлюсь ближе к пониманию того, как должен быть написан CMake, он исчезает в следующем примере, который я прочитал. Все, что я хочу знать, это то, как я должен структурировать свой проект, чтобы мой CMake требовал минимального сопровождения в будущем. Например, я не хочу обновлять свой CMakeList.txt при добавлении новой папки в мое дерево src, которая работает точно так же, как и все остальные папки src.

Вот как я представляю структуру моего проекта, но, пожалуйста, это только пример. Если рекомендуемый способ отличается, скажите, пожалуйста, и скажите, как это сделать.

myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]

Кстати, важно, чтобы моя программа знала, где находятся ресурсы. Я хотел бы знать рекомендуемый способ управления ресурсами. Я не хочу получать доступ к своим ресурсам с помощью "../resources/file.png"

4 ответа

Решение

После некоторых исследований у меня теперь есть собственная версия самого простого, но полного примера cmake. Вот оно, и оно пытается охватить большинство основ, включая ресурсы и упаковку.

одна вещь, которую он делает нестандартной - это обработка ресурсов. По умолчанию cmake хочет поместить их в /usr/share/, /usr/local/share/ и что-то эквивалентное в windows. Я хотел иметь простой zip/tar.gz, который вы можете извлечь в любом месте и запустить. Поэтому ресурсы загружаются относительно исполняемого файла.

Основное правило для понимания команд cmake - следующий синтаксис:<function-name>(<arg1> [<arg2> ...]) без запятой или точки с запятой. Каждый аргумент является строкой. foobar(3.0) а также foobar("3.0") та же. Вы можете установить списки / переменные с set(args arg1 arg2), С этой переменной установлено foobar(${args}) а также foobar(arg1 arg2) фактически одинаковы. Несуществующая переменная эквивалентна пустому списку. Внутренне список - это просто строка с точкой с запятой для разделения элементов. Поэтому список только с одним элементом по определению является именно этим элементом, бокс не имеет места. Переменные являются глобальными. Встроенные функции предлагают некоторую форму именованных аргументов, потому что они ожидают некоторые идентификаторы, такие как PUBLIC или же DESTINATION в их списке аргументов, чтобы сгруппировать аргументы. Но это не языковая функция, эти идентификаторы также являются строками и анализируются реализацией функции.

вы можете клонировать все из GitHub

cmake_minimum_required(VERSION 3.0)
project(example_project)

###############################################################################
## file globbing ##############################################################
###############################################################################

# these instructions search the directory tree when cmake is
# invoked and put all files that match the pattern in the variables 
# `sources` and `data`
file(GLOB_RECURSE sources      src/main/*.cpp src/main/*.h)
file(GLOB_RECURSE sources_test src/test/*.cpp)
file(GLOB_RECURSE data resources/*)
# you can use set(sources src/main.cpp) etc if you don't want to
# use globing to find files automatically

###############################################################################
## target definitions #########################################################
###############################################################################

# add the data to the target, so it becomes visible in some IDE
add_executable(example ${sources} ${data})

# just for example add some compiler flags
target_compile_options(example PUBLIC -std=c++1y -Wall -Wfloat-conversion)

# this lets me include files relative to the root src dir with a <> pair
target_include_directories(example PUBLIC src/main)

# this copies all resource files in the build directory
# we need this, because we want to work with paths relative to the executable
file(COPY ${data} DESTINATION resources)

###############################################################################
## dependencies ###############################################################
###############################################################################

# this defines the variables Boost_LIBRARIES that contain all library names
# that we need to link to
find_package(Boost 1.36.0 COMPONENTS filesystem system REQUIRED)

target_link_libraries(example PUBLIC
  ${Boost_LIBRARIES}
  # here you can add any library dependencies
)

###############################################################################
## testing ####################################################################
###############################################################################

# this is for our testing framework
# we don't add REQUIRED because it's just for testing
find_package(GTest)

if(GTEST_FOUND)
  add_executable(unit_tests ${sources_test} ${sources})

  # we add this define to prevent collision with the main
  # this might be better solved by not adding the source with the main to the
  # testing target
  target_compile_definitions(unit_tests PUBLIC UNIT_TESTS)

  # this allows us to use our executable as a link library
  # therefore we can inherit all compiler options and library dependencies
  set_target_properties(example PROPERTIES ENABLE_EXPORTS on)

  target_link_libraries(unit_tests PUBLIC
    ${GTEST_BOTH_LIBRARIES}
    example
  )

  target_include_directories(unit_tests PUBLIC
    ${GTEST_INCLUDE_DIRS} # doesn't do anything on Linux
  )
endif()

###############################################################################
## packaging ##################################################################
###############################################################################

# all install commands get the same destination. this allows us to use paths
# relative to the executable.
install(TARGETS example DESTINATION example_destination)
# this is basically a repeat of the file copy instruction that copies the
# resources in the build directory, but here we tell cmake that we want it
# in the package
install(DIRECTORY resources DESTINATION example_destination)

# now comes everything we need, to create a package
# there are a lot more variables you can set, and some
# you need to set for some package types, but we want to
# be minimal here
set(CPACK_PACKAGE_NAME "MyExample")
set(CPACK_PACKAGE_VERSION "1.0.0")

# we don't want to split our program up into several things
set(CPACK_MONOLITHIC_INSTALL 1)

# This must be last
include(CPack)

Самый простой, но полный пример можно найти в учебнике по cmake:

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)

Для вашего примера проекта вы можете иметь:

cmake_minimum_required (VERSION 2.6)
project (MyProject)
add_executable(myexec src/module1/module1.cpp src/module2/module2.cpp src/main.cpp)
add_executable(mytest test1.cpp)

Для вашего дополнительного вопроса, один из способов снова в учебнике: создайте настраиваемый заголовочный файл, который вы включаете в свой код. Для этого сделайте файл configuration.h.in со следующим содержанием:

#define RESOURCES_PATH "@RESOURCES_PATH@"

Тогда в вашем CMakeLists.txt добавлять:

set(RESOURCES_PATH "${PROJECT_SOURCE_DIR}/resources/"
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/configuration.h.in"
  "${PROJECT_BINARY_DIR}/configuration.h"
)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

Наконец, где вам нужен путь в вашем коде, вы можете сделать:

#include "configuration.h"

...

string resourcePath = string(RESOURCE_PATH) + "file.png";

Примечание. Этот ответ длинный, потому что невинно выглядящий вопрос и множество тем, которые он охватывает, обманчиво широки, а целевая аудитория вопроса предназначена для новичков (людей, плохо знакомых с CMake). Поверьте мне, этот ответ мог бы быть намного длиннее, чем он есть.

Что такое CMake

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

CMake — это генератор системы сборки. Вы пишете конфигурацию для описания системы сборки (проекта и его целей сборки и того, как они должны быть собраны) (и, возможно, , и упакованы ). Вы даете программе CMake эту конфигурацию и сообщаете ей, какую систему сборки генерировать, и она генерирует ее ( при условии, что эта система сборки поддерживается ). Такие поддерживаемые системы сборки включают (но не ограничиваются ими): Ninja, Unix Makefiles, решения Visual Studio и XCode.

Система сборки — это то, что понимает (потому что вы указываете ей), как должен быть собран ваш проект — какие исходные файлы у него есть, и как эти исходные файлы должны быть скомпилированы в объектные файлы, и как эти объектные файлы должны быть связаны вместе в исполняемые файлы или динамические/общие или статические библиотеки.

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

На самом деле CMake поддерживает другие языки программирования и их системы сборки, а не только C и C++, но поскольку вы просто спрашиваете о C++, я не буду об этом говорить.

Уловки, чтобы избежать CMake при использовании CMake (не пытайтесь дома, дети)

Все, что я хочу знать, это то, как мне структурировать мой проект, чтобы мой CMake требовал минимального обслуживания в будущем. Например, я не хочу обновлять свой CMakeList.txt, когда добавляю новую папку в свое дерево src, которое работает точно так же, как и все другие папки src.

Вопреки тому, что вы думаете, небольшая необходимость изменять файлы CMakeLists.txt всякий раз, когда вы добавляете новые исходные файлы, — это очень небольшая плата за все преимущества, которые может предоставить CMake, и попытка обойти эту стоимость имеет свои собственные затраты. которые становятся проблемами в масштабе. Вот почему эти методы обхода (а именно,), которые часто используют люди, не рекомендуются для использования сопровождающими CMake и различными давними пользователями CMake на Stack Overflow, как показано здесь и здесь .

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

Базовая конфигурация CMake

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

       myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]

Если вы ищете соглашение, которое можно использовать для макета файловой системы проекта, хорошо описанной спецификацией макета является The Pitchfork Layout Convention (PFL) , которая была написана на основе соглашений, появившихся в сообществе C++ с течением времени.

Вот как может выглядеть этот макет проекта в соответствии со спецификацией PFL с разделенными заголовками и раздельными тестами :

      myProject/
  CMakeLists.txt
  libs/
    module1/
      CMakeLists.txt
      include/myProject_module1/
        module1.h
      src/myProject_module1/
        module1.cpp
      tests/
        CMakeLists.txt
        test1.cpp
      data/
        file.png
    module2/
      CMakeLists.txt
      src/module2
        main.cpp
      [...]
  build/

Примечание: это не обязательно- это может быть просто, но добавивделаетдля каждого модуля "пространство имен", так что два модуля (даже если один из отдельного проекта) могут иметь файлы заголовков с одним и тем же именем, и что все эти заголовки могут быть включены в один исходный файл без конфликтов или двусмысленностей, например:

      #include <myProject_module1/foo.h>
#include <myProject_module2/foo.h>
#include <yourProject_module1/foo.h>
// Look, ma! No clashing or ambiguities!

Поскольку вы разрешили это в своем вопросе, для остальных примеров кода конфигурации я буду использовать приведенный выше макет PFL.

мой проект/CMakeLists.txt:

      cmake_minimum_required(VERSION 3.25)
# ^choose a CMake version to support (its own can of worms)
# see https://alexreinking.com/blog/how-to-use-cmake-without-the-agonizing-pain-part-1.html
project(example_project
  VERSION 0.1.0 # https://semver.org/spec/v0.1.0.html
  DESCRIPTION "a simple CMake example project"
  # HOMEPAGE_URL ""
  LANGUAGES CXX
)
if(EXAMPLE_PROJECT_BUILD_TESTING)
  enable_testing()
  # or alternatively, `include(CTest)`, if you want to use CDash
  # https://cmake.org/cmake/help/book/mastering-cmake/chapter/CDash.html
endif()
add_subdirectory(libs/module1)
add_subdirectory(libs/module2)
# ^I generally order these from lower to higher abstraction levels.
# Ex. if module1 uses module2, then add_subdirectory it _after_ module2.
# That allows doing target_link_libraries inside moduleN/CmakeLists.txt
# instead of here (although that's equally fine. a matter of preference).

В этой команде вы указываете, какая версия CMake требуется для анализа и запуска вашей конфигурации CMake. В команде вы объявляете основную информацию о проекте, команда включает тестирование для текущего каталога и ниже, и каждая команда изменяет «текущий каталог» , создает новый подкаталог «область» и анализирует файл CMakeLists.txt, найденный в этом путь.

Вот документы для cmake_minimum_required(), project(), enable_testing()и add_subdirectory().

myProject/libs/module1/CMakeLists.txt (и аналогичный для модуля2):

      # if module1 is a library, use add_library() instead
add_executable(module1
  src/module1.cpp
)
target_compile_features(okiidoku PUBLIC cxx_std_20) # or whatever language standard you are using
target_include_directories(module1 PUBLIC include)
if(EXAMPLE_PROJECT_BUILD_TESTING)
  add_subdirectory(tests)
endif()

Команда — это то, как вы объявляете новую исполняемую цель для сборки. Это похоже на то, как вы объявляете цель библиотеки для сборки, где библиотека может быть связана с помощью команды.

С помощью этой команды вы сообщаете CMake, какой флаг передать компилятору для выбора используемого стандарта языка C++, а с помощью этой команды вы сообщаете CMake, какие включаемые каталоги указывать при компиляции файлов реализации. означает, что самой цели потребуется эта включаемая директория для ееs, а также другие зависимые цели, которые ссылаются на эту цель. Если зависимым целям это не нужно, используйте . Если это нужно только зависимым целям, используйте . , , иактуальны для библиотечных целей, но я не знаю, используют ли они какие-либо исполняемые цели (поскольку я уверен, что ничто никогда не зависит от исполняемых файлов с точки зрения компоновки), поэтому либоилидолжен работать при указании включаемых каталогов для исполняемых целей.

Вот документы для add_library(), add_executable(), target_compile_features() target_include_directories(), и target_link_libraries().

Если вы хотите узнать больше о какой-либо команде CMake (перечисленной в), просто делать, или гугл.

Что касается папки тестов, вы можете использовать поддержку тестирования CMake без использования каких-либо библиотек тестирования платформ C++ или использовать ее с библиотекой тестирования или платформой, поддерживающей CMake. Чтобы узнать больше о CMake и тестировании в целом, прочитайте протестированыглаву в книге Mastering CMake . Слишком много материала, чтобы охватить его в виде ответа на переполнение стека.

Установка также сама по себе может быть червяком (подробнее об этом позже), поэтому, поскольку вопрос об этом не задавался, я думаю, что лучше не включать пост с ответом, чтобы избежать очень длинного поста и расползания области. Опять же, установленысм. посвященную главу в книге Mastering CMake . Одна вещь , на которую следует обратить особое внимание при установке , -- убедиться , что вы делаете установочный пакет перемещаемым .

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

Параметры/флаги компиляции также являются собственной проблемой и лучше освещаются в отдельных сообщениях с вопросами и ответами.

Если вы хотите начать использовать зависимости, предлагаю прочитать официальное руководство «Использование зависимостей» .

Если у вас действительно много исходных файлов, и ваш файл myProject/src/module1/CMakeLists.txt начинает становиться громоздким из-за всех строк/, то вы можете выделить это в отдельный файл, используя target_sources()и либо другой файл CmakeLists.txt в подкаталоге, включенном черезили файл sources.cmake в том же подкаталоге, включенный через include().

Чтобы сгенерировать систему сборки, как показано на диаграмме дерева каталогов, измените текущий каталог наа затем запустить, гделюбые другие аргументы конфигурации, которые вы хотите использовать .

Как всегда, справочная документация по CMake может быть весьма полезной, но первые пару раз ее сложно просмотреть, так как она не предназначена для обучения новичков. Если вы хотите узнать больше о том, как использовать CMake, попробуйте официальное руководство по CMake и прочтите соответствующие главы в книге Mastering CMake . Если вы действительно хотите погрузиться глубже, ознакомьтесь с «Professional CMake» , написанным Крейгом Скоттом (одним из сопровождающих CMake). Это стоит денег, но, прочитав образец главы, оглавление и другие сообщения в блоге и предложения Крейга о CMake GitLab, я поверил в ее ценность, и новые издания книги не требуют дополнительных затрат.

Трюки для файлов ресурсов

Кстати, важно, чтобы моя программа знала, где находятся ресурсы. Я хотел бы знать рекомендуемый способ управления ресурсами. Я не хочу получать доступ к своим ресурсам с помощью "../resources/file.png"

Этот ответ написан при условии, что вы выбрали CMake, чтобы использовать его для всех его целей - кросс-платформенных, гибких сборок инструментальной цепочки, для которых не все используют CMake (и это нормально).

Это его собственная банка червей. Одна сложная часть — это сравнение размещения файловой системы между папкой сборки и папкой установки. Папка сборки — это место, где собираются двоичные файлы и другие компоненты, такие как объектные файлы, а папка установки — это любое место, куда вы устанавливаете эти встроенные двоичные файлы. Они могут иметь очень разные структуры файловой системы, где макет папки сборки зависит от CMake, и по умолчанию он делает разумные вещи, но расположение вещей в папке установки в значительной степени зависит от того, как выхотите, чтобы он был настроен. Это может отличаться от того, что CMake делает в папке сборки, и вы, вероятно, захотите иметь возможность запускать и тестировать свои двоичные файлы как из папки сборки, так и из папки установки. Таким образом, вам нужно найти способ поддержки ваших двоичных файлов, находящих ваши файлы ресурсов как там, где они находятся «время разработки» (когда вы запускаете из папки сборки), так и после установки (когда вы или ваши пользователи запускаете установленный двоичные файлы).

На разных платформах также существуют разные соглашения о том, где размещать файлы ресурсов для установки. Существует соглашение, определенное GNU Coding Standards , с которым CMake немного интегрируется / поддерживает , но, конечно, у Microsoft Windows есть другая вещь си, а у MacOS другая штука с пакетами приложений и. Все может быть не так просто, как вы думали. Но я мало что знаю об этом (и могу ошибаться), но мне кажется, что это достаточно большая тема, чтобы иметь свой собственный вопрос или несколько своих вопросов здесь, в Stack Overflow.

Для других связанных битов CMake см.:

В качестве примеров недостатков с простыми/наивными подходами ответ sgvd работает для каталога сборки, но не будет работать везде, где установлен проект, и даже если он каким-то образом работает на машине сборщика, тот факт, что он использует Абсолютный путь делает его работу маловероятной при распространении на другие машины или платформы с другими соглашениями.

Если вы используете CMake, но хотите поддерживать только определенную платформу, считайте, что вам повезло, и найдите или напишите вопрос здесь, в Stack Overflow, о том, как это сделать.

См. также этот связанный с этим вопрос (на который на момент написания этой статьи до сих пор нет ответов): Как указать пути к активам, которые работают в сборках .

Отослать

Добро пожаловать в мир CMake! Просто подождите, пока вы не перейдете к выражениям генератора ! Тогда ты действительно начнешь веселиться :D

Здесь я пишу самый простой, но полный пример файла CMakeLists.txt.

Исходный код

  1. Учебники от hello world до кросс-платформенных Android/iOS/Web/Desktop.
  2. Для каждой платформы я выпустил образец приложения.
  3. Структура файла 08-cross_platform проверена моей работой
  4. Это может быть не идеально, но полезно и лучше всего для одной команды

После этого я предложил документ для подробностей.

Если у вас есть вопросы, вы можете связаться со мной, и я хотел бы это объяснить.

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