Как стать автором
Обновить

Комментарии 49

Закрепленные Закреплённые комментарии

Нашел небольшой баг с тем, что MYLIB_SHARED_LIBS не оказывает влияние на сборку библиотеки, т.к. используется после того, как таргет mylib определен. Пофиксил этот баг в репозитории, плюс заменил PUBLIC_HEADER на install(DIRECTORY). Для общего решение это более универсальный подход (см. комментарий от @Nipheris и мой после него).

Огромное спасибо за статью, очень интересная и с точки зрения объяснения того "как должно быть" и с точки зрения формулировки задач о которых в принципе нужно помнить, когда готовишь библиотеку на экспорт.

Хоть будет с чем сверяться когда снова буду библиотеку в cmake заворачивать.

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

В конце статьи есть ссылка на проект cmake-init, который помогает создать проект из шаблона. С ним все упрощается до ответа на несколько вопросов о типе проекта, использемом стандарте и пакетном менеджере. При этом он сразу поможет настроить CI/CD в GitHub.

Не совсем согласен с вами. Решение есть, оно достаточно типовое, так что его вполне можно загнать в какой-то генератор, например упоминавшийся уже cmake-init. Проблема однако в том, что лично мне не удалось найти его сформулированным в одном месте, пришлось ознакомиться с большим количество источников, в которых раскрывались отдельные вопросы, но не все целиком. Не понимаю, почему CMake не может обновить свой tutorial с учетом всех новых фич. Может для того, чтобы докладчиков на CppCon без хлеба не оставлять, которые из года в год рассказывают, как надо правильно делать.

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

Напишите более подробную статью про разоаботку библиотек c++. Или направьте, в каком направлении искать информацию, а то в гугле попадаются только материалы по разработке библиотек Си.

Ну может когда-нибудь) Пока посмотрите статью Ulrich Drepper, которая есть в ссылках. Она полностью раскрывает все технические детали динамических библиотек в линуксе, может это именно то, что вам нужно. Под винду плюс-минус тоже самое.

Здравствуйте! Так как только осваиваю CMake, хотел задать вопрос по своей текущей задаче.
Как можно модернизировать Ваш шаблон для библиотеки, которая в свою очередь является оберткой для другой библиотеки различных конфигураций (win/linux, x86/x64, static/shared, release/debug), которая имеет только заголовочные файлы и бинарные (dll/lib/so).
Хотелось бы раскрыть вопросы:

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

  • как правильно ссылаться (прописывать пути к include/libs) к оборачиваемой библиотеке;

  • что лучше определять в CMakePresets.json, а что в CMakeLists.txt;

  • установка разных конфигураций библиотеки.

  1. Если вы про то, что для того, чтобы пометить функцию как экспортируемую, в Linux и Windows используются разные директивы компилятора (__attribute__ и __declspec), то CMake предоставляет функцию generate_export_header, которая сгенерирует файл, в котором за макросом вида <LIBNAME>_EXPORT спрячет платформо-зависимую директиву. Этот файл надо включать в свои исходники и устанавливать вместе с вашей библиотекой. В статье есть этот вопрос освещается в разделе "Экспорт символов"

  2. В своей библиотеке вы делаете find_package(otherLib), потом target_link_libraries(myLib PUBLIC|PRIVATE otherLib). PUBLIC вы определяете, если нужно, чтобы проекты, которые будут линковаться с myLib видели заголовочные файлы otherLib (как правило, из-за того, что заголовки myLib включают заголовки otherLib), в противном случае - PRIVATE. Еще потребуется в файле конфигурации вашей библиотеки выполнить команду find_dependency(otherLib), чтобы проинициализировать зависимости для импортируемого таргета. Этот способ сработает, если разработчик otherLib все сделал правильно и предоставил файл конфигурации для своей библиотеки. Если нет, то вам надо написать свой find module для otherLib, который будет находить ее в системе и создавать для нее импортированный таргет. Этот модуль затем вы будете вероятно устанавливать вместе со своей библиотекой, чтобы использовать его в своем файле конфигурации при вызове find_dependency.

  3. В CMakeLists.txt нужно хардкодить только build requirements, т.е. опции, без которых ваш проект в принципе не соберется. Все остальное - в пресетах. Например, флаг Werror, который делает предупреждения компилятора ошибками, никогда не должен указываться в вашем CMakeLists.txt (ну или по крайней мере должна быть опции в вашем проекте, которая позволит его отключить, но лучше использовать пресет). Причины, по котором это необходимо делать, объясняются в статье. Если вкратце, любой захардкоденный флаг в вашем проекте - проблема на мейнтейнеров пакетных менеджеров, т.к. им придется патчить ваши CMakeLists.txt, когда они будут собирать вашу либу под какую-нибудь платформу, на которой захардкоденная опция невалидна.

Спасибо за разъяснение!
А если нет возможности воспользоваться find_package, т.е. обертываемая библиотека была предоставлена только заголовочными и бинарными файлами (нет ни файла конфигурации и в системе никаким образом не зарегистрирована). Какова правильная практика указать искомый путь к этим файлам? Сейчас я в пресете создаю переменную, которая хранит путь к папке обертываемой библиотеки, затем в CMakeLists через target_include_directories подключаю заголовки, target_link_directories папку с бинарными файлами и target_link_libraries имя библиотеки. Догадываюсь, что неправильно.

Я вам ответил - нужно написать find module, см. п. 2

П.2 почитал внимательно, но не сразу разобрался, что именно find module решается моя задача. Спасибо!

Обычно (если либа по известному пути) проще даже не find module а config module. Find подразумевает именно поиск через glob и другие интроспекции. Это небыстро. Но если путь известен (например, сами собрали эту третью либу через external_project), то можно просто создать imported target и положить в его свойства нужные известные пути/флаги. И вот это уже будет config.

А "правильно ссылаться" - это как раз imported target. Сейчас в ходу двоякий путь - остался как старый вариант, где модуль ставит разные переменные вроде XXX_LIBRARIES, XXX_INCLUDE_DIRS и т.д., и вы потом их используете, так и современный - когда всё нужное инкапсулировано в таргет. В последнем случае вы просто с ним линкуетесь, а вся внутренняя кухня с инклюдами, флагами и т.д. подтягивается автоматически. Плюс, в этом случае можно сразу разделить разные сборки (debug/release) в единственном таргете.

Ммм, не уверен, что полностью вас понимаю. Из своей библиотеки нужно как-то найти чужую. find_package при этом не работает, т.к. third-party либа не экспортирует файл конфигурации пакета, и не достаточна популярна, чтобы CMake сам по себе имел для нее find-модуль (как, например, для буста и опенссл). Поэтому, чтобы find_package заработал, нужно написать свой find-модуль. Что вы подразумеваете под config module я до конца не понял, т.к. в документации CMake такого понятия нет, насколько мне известно. Если вы предлагаете просто где-то руками создать импортированный таргет для third-party либы и потом без всякого find_package его использовать - то для каких-то частных случаев это может и сгодиться, но в общем случае - это костыльный подход, с которым потом вероятно вылезут проблемы. Например, когда вы свою библиотеку инсталлируете, вам придется в своем файле конфигурации как-то искать third-party либу, которая непонятно где в системе может быть установлена. Если вы напишите свой find-модуль, то можете его просто ставить вместе со своей библиотекой и использовать в файле конфигурации пакета, чтобы find_dependency отрабатывал, как надо.

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

Ну, чтобы найти - зовётся find_package.

Он пытается найти по известным путям файл FindXXX.cmake - если таковой найден, он выполняется. Если нет - файлы XXXConfig.cmake и/или xxx-config.cmake.

Скрипты findXXX лежат в самом cmake, а также конфигурируются в CMAKE_MODULE_PATH. Скрипты config ищутся тоже по общим путям, + по списку из CMAKE_PREFIX_PATH.

Можно поменять порядок поиска через CMAKE_FIND_PACKAGE_PREFER_CONFIG. Вроде всё.

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

Но обычно скрипты FindXXX написаны руками, и там может быть весьма витиеватая логика с find_path/find_library, интроспекцией всевозможных путей и запуском разных помогалок вроде mysql_config для FindMysql. Заканчивается всё стандартно вызовом find_package_handle_standard_args, и выставлением XXX_INCLUDE_DIRS и XXX_LIBRARY. Сейчас туда же дописывают создание imported target. Этот скрипт предполагается использовать "вообще" для поиска либы - т.е. его можно положить отдельно или даже запульнуть пулл-реквест в cmake.

А ConfigXXX/xxx-config - это обычно результат экспорта cmake. Там нет никаких интроспекций, всё чётко лежит по известным (относительно скрипта) путям. Чётко - создал imported target, прописал свойства - и вуаля. Непосредственно искать ничего не надо, надо всего лишь обернуть уже известное. И для предсобранных либ без cmake можно такой скрипт написать руками и положить их все рядом с папками самих либ. Всё будет так же находиться и собираться, но поскольку это не скрипты поиска, а всего лишь обёртки - их не очень верно называть FindXXX. Они именно конфиги.

Про то, как можно работать с multi-config генераторами расскажу в отдельном ответе.

Тут вообще говоря обычно используется два пути: установка разных конфигураций (static/shared, Debug/Release/..) в разные директории (тогда ничего специального делать не нужно), или же установка разных конфигураций в одну директорию, т.е. по одному CMAKE_INSTALL_PREFIX.

Во втором случае вам придется решить проблему с одинаковыми именами файлов. В качестве примера можно рассмотреть Windows. При установке обычно копируются такие файлы:

  1. Публичные заголовки библиотеки - они должны быть одинаковыми для всех конфигураций, поэтому без разницы, сколько разных конфигураций вы установите в одну директорию. Единственный момент здесь - это файл, генерируемый generate_public_header. Он имеет разный вид для static и shared, поэтому в статье я называю эти файлы по-разному для static и shared

  2. Файл версии проекта (генерируется write_basic_package_version_file) - одинаковый для разных конфигураций, можно смело перезаписывать

  3. Файл конфигурации проекта можно (и нужно) писать так, чтобы он не зависел от конфигурации сборки

  4. Файл с определением таргета библиотеки (создается install(EXPORT)) - он разный для static и shared, но НЕ зависит от типа билда (Debug, Release и т.д.). Поэтому нужно называть их по-разному, если вы планируете устанавливать статическую и динамическую версию в одну директорию. При этом install(EXPORT) кроме этих файлов генерирует еще для текущей конфигурации файл <targets-file-name>-<config>.cmake (например, mylib-static-targets-debug.cmake, mylib-static-targets-release.cmake и т.д.) Когда ваш файл конфигурации библиотеки (mylib-config.cmake) включает файл с импортируемым таргетом библиотеки (например, mylib-static-targets.cmake), последний дополнительно включает все файлы с именем mylib-static-targets-*.cmake. В этих файлах, если не вдаваться в детали, для импортируемого таргета библиотеки устанавливается свойство IMPORTED_LOCATION_<CONFIG>, в которое записывается путь до бинари библиотеки для конкретной конфигурации. В проекте, который использует вашу библиотеку, потом можно менять конфигурацию, и линковаться будет та, что нужно.

  5. Бинари библиотеки - они очевидно разные, поэтому нужно продумать какую-то схему по их переименованию. Например, я иногда использую постфиксы "" (пустой) для Release, "d" для Debug, "m" для MinSizeRel и "r" для RelWithDebInfo. Для статической версии дополнительно в начало постфикса ставится буква "s". Не надо только свою схему навязывать всем, прописывая эти постфиксы прямо в set_target_properties в CMakeLists.txt. Вместо этого можно в пресете или в командной строке использовать переменную CMAKE_<CONFIG>_POSTFIX.

установка разных конфигураций (static/shared, Debug/Release/..) в разные директории (тогда ничего специального делать не нужно)

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

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

Кстати, поддержка мультиконфигурационности в самом CMake - тоже не сахар. Она сделана по сути только для нескольких генераторов, причём прежде всего для IDE вроде Visual Studio, и сильно усложняет код скриптов кучей одинаковых переменных, но для разных конфигураций. Мне кажется гораздо правильнее реализовать такое по append-принципу "несколько прогонов cmake-скрипта -> один солюшен/проектный файл", чтобы новые конфигурации просто "дописывались" в уже сгенерированный проект. Но имеем что имеем.

Спасибо за статью! Отсутствие best practices в документации самого CMake - это, пожалуй, его главная проблема сегодня. Даже синтаксис - и тот можно стерпеть.

На винде критически важно различать debug и release версии, потому что там ещё и ABI у рантайма разный. Какой-нибудь std::string передать из либы или в либу - и это сразу вылезает. А вот разные варианты релизных - это уже на любителя. Обычно вполне достаточно одной, а остальные на неё замапить (через MAP_IMPORTED_CONFIG_<XXX>).

В разные пути ставить или нет - ну, по дефолту cmake в билде делает в разные. Причём по крайней мере для ninja и msbuild структура этих папок совпадает, что позволяет легко делать финт с тестами после кросс-компиляции (собираешь кроссом с помощью ninja-multi-config, передаёшь собранные артефакты в виртуалку с виндой, прямо 1-в-1 где собрались. В виртуалке запускаешь ctest без стадии сборки - и оно сразу работает).

Вот если инсталлировать на конечную систему - там да, проще по дефолтному пути, и тогда приходится патчить имена.

Да, вы верно говорите. В том руководстве, о котором я написал в комментарии не возникает проблем с разными версиями рантайма (библиотеки-то в итоге все с разными именами и линкуется с нужной конфигурацией/рантаймом).

Замечание про MAP_IMPORTED_CONFIG_<CONFIG> я уверен полезно для читателей, спасибо.

Огромное спасибо за статью. Давно сам хотел написать что-то подобное, потому что постоянно сталкиваюсь с библиотеками на CMake, которыми совершенно невозможно пользоваться. То как subdirectory не работает, то свои переменные выставляет сложно.

Пожалуйста, я вот раньше не вытерпел)

Спасибо. Я вроде неплохо владею cmake, но кое что новое для себя почерпнул.

Если у вас будет вдохновение и силы написать отдельную статью про CPack, то будет вообще супер. А то у нас сборка deb пакета с systemd сервисом выполняется вручну из выхлопа cmake.

Честно говоря, у меня опыта по CPack особого нет. Мне он вообще представляется довольно простым - устанавливай всякие переменные, и все дела. Вероятно я просто не в курсе о всяких подводных камнях, которые можно было бы раскрыть в статье.

В данный момент я больше задумываюсь о туториале по рецептам для Conan.

Вот за этот туториал Вам заранее ОГРОМНОЕ спасибо. )

Лучше сразу vcpkg

Не автор, но поделюсь.
Просто добавляю такие строки в конце CMakeLists.txt:


SET(CPACK_GENERATOR "DEB")
SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Имя Фамилия почта") 
SET(CPACK_DEBIAN_PACKAGE_DESCRIPTION "Описание пакета")
SET(CPACK_DEBIAN_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE armhf) # Выберите свою архитектуру
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libasan6, libubsan1")  # У вас зависимости могут отличаться.
INCLUDE(CPack)

Если у вас установка проекта через cmake --install. производится нормально, то эти же файлы запакуются в .deb пакет.
Пример c install, должно вызываться раньше cpack'а:


install(FILES main.conf DESTINATION /etc/company/)
install(FILES systemd/main.service DESTINATION /etc/systemd/system/)
install(CODE "EXECUTE_PROCESS(COMMAND ln -sf /opt/company/main/main main.lnk WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})")
install(FILES main.lnk DESTINATION /usr/bin RENAME main) 

Пути и прочее остается на ваше усмотрение.


Версию генерю немного через задницу, но пока работает(версию держу в теге гита):


string(CONCAT  GIT_VERSION  "\"" ${GIT_REPO_VERSION} "-" ${CMAKE_BUILD_TYPE} "\"")
string(REPLACE "\n" "" GIT_VERSION ${GIT_VERSION})
set(PROJECT_VERSION  ${GIT_REPO_VERSION})
string(REGEX MATCH "([0-9]+.[0-9]+.[0-9]+-[0-9]+)|([0-9]+.[0-9]+.[0-9]+)" PROJECT_VERSION ${PROJECT_VERSION})

Возможно, в пресет не стоит писать мин. версию 3.14, покуда, как вы сами говорите, поддержка появилась только в 3.19

Имхо в пресете надо все-таки указывать минимальную версию, нужную именно для сборки проекта. Я не вижу причин, по которым было бы предпочтительнее указывать версию, нужную для поддержки вашего варианта пресета (которых тоже уже 5 штук), а вот обратное в общем случае неверно. Потому что чисто теоретически, ничто не мешает IDE самой распарсить пресет и при вызове CMake вообще его не указывать, а, скажем, переменные cacheVariables просто передавать в командной строке. Тогда вам не нужен именно CMake 3.19 и выше, потому что `cmake --preset ...` просто не используется. У других авторов я находил именно такой вариант (т.е. в пресете запросто указывается именно версия, нужная для сборки, а не для поддержки самого пресета).

Отличная статья, выражаю благодарности автору. Нахожусь на этапе подготовки предложения по рефакторингу файлов сборки текущего проекта, и как вовремя ваша статья выходит..
Если будет интересна тема, напишете о кросскомпиляции, интересуют не только Linux / Windows, но и мобильные платформы, как в этом случае лучше конфигурировать зависимости: особенно когда многие было бы проще сначала собрать, и они разбросаны по кастомным путям

Признаться мне не доводилось организовывать кросс-компиляция сколько-нибудь серьезных CMake-проектов, поэтому особо делиться нечем. Очевидно, вам понадобится создать тулчейн-файл - это не должно вызвать сложностей. Общие рекомендации те же - никаких захардкоденных настроек в CMakeLists.txt. Собственно, все настройки целевой платформы в тулчейн-файле можно указать, а сам тулчейн файл для красоты добавить в пресет (см. поле toolchainFile). Тогда вы сможете вызывать что-то вроде `cmake .. --preset linux`, `cmake .. --preset android`, `cmake .. --preset win` и т.д.

По зависимостям так не смогу подсказать, мало конкретики) Могу однако порекомендовать посмотреть в сторону новой фичи Cmake (появилась в 3.24) - провайдеры зависимостей (dependency providers). Провайдер зависимости - это по сути просто функция (или, что чаще, макрос), которая перехватывает все вызовы find_package и/или FetchContent и дальше может делать с ними все, что угодно, тем более, что все параметры, с которыми вызывалась find_package/FetchContent в нее передаются. Называется она так по тому, что все-таки в основном предполагается, что делать она будет следующее: дергать какую-то внешнюю команду, которая подтянет каким-то образом зависимость.

Например, ваш провайдер при вызове функции find_package(Boost 1.77.0) может выполнить (с помощью стандартных средств CMake, например, execute_process) команду conan install boost/1.77.0@ --generator cmake_paths, а потом вызвать уже "обычный" find_package, который найдет только что установленный буст.

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

Могу рассказать об iOS опыте: как раз закончил портировать VCMI под iOS (правда, пулл реквест ещё не приняли). В процессе как раз и более-менее разобрался как надо пользоваться CMake, особенно для iOS проектов.

Для сборки зависимостей пока что создан отдельный скрипт, который выполняет условный make install каждой библиотеки в определенную папку (у меня отдельные папки для симулятора и устройства), что гарантирует стандартную структуру папок (include, lib и т.д.). После этого эта папка просто подается в переменной CMAKE_PREFIX_PATH при конфигурировании проекта — и все зависимости находятся «из коробки» (при условии, что FindXXX написаны адекватно). В принципе, никто не мешает класть зависимости и в разные папки, тогда надо каждую из них подавать в CMAKE_PREFIX_PATH как список (через точку с запятой).

Единственная проблема, с которой я столкнулся, была невозможность отыскать Boost — пришлось принудительно добавить

list(APPEND CMAKE_FIND_ROOT_PATH "${CMAKE_PREFIX_PATH}")

чтоб избавить пользователя указывать CMAKE_FIND_ROOT_PATH на ту же папку вручную. Толком я так и не понял почему Boost хочет именно ее.

Репозиторий со скриптом: https://github.com/kambala-decapitator/vcmi-ios-depends. В дальнейшем хочу попробовать перевести всё это на conan / vcpkg, сейчас как раз щупаю оба для macOS сборки на CI.

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

спасибо)

Еще один вопрос по лучшим практикам: у меня src директория содержит относительно много поддиректорий с исходниками и в каждой из них я определяю CMakeLists.txt со следующим содержимым:
INCLUDE("${CMAKE_CURRENT_LIST_DIR}/subdir/CMakeLists.txt") <- там, где надо подключить след. папку
TARGET_SOURCES("${PROJECT_NAME}" PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/file01.cpp"
"${CMAKE_CURRENT_LIST_DIR}/file01.h"
...
)
А в "верхнем" CMakeLists.txt:
...
ADD_LIBRARY("${PROJECT_NAME}" SHARED "")
...
INCLUDE("${CMAKE_CURRENT_SOURCE_DIR}/src/CMakeLists.txt")

Вы для определения списка исходников используете переменную, которая в свою очередь используется, для задач, которые Вы описали. Как лучше всего включать исходники в таргет, когда и папок много, и файлов в них?

target_sources предпочтительнее, просто он появился относительно недавно (по меркам CMake) и мало кто его ещё использует. https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/

Есть ещё холиварный вопрос - использовать ли директиву file(GLOB) или все файлы всегда перечислять в CMakeLists. Лично я пользуюсь глобом, но у него много противников.

Кстати, почему include, а не add_subdirectory? С инклудом у вас же не будет зеркалироваться структура дерева исходников в билд-дерево.

Понял, спасибо! add_subdirectory взял на вооружение, поменяю в своих проектах.

К ответу @Nipheris мне нечего добавить, согласен со всем. И статью по ссылке его обязательно почитайте.

Я file(glob) не использую, хотя какое-то время использовал. Всё-таки нет-нет, но что-то вылазило у меня с ним. Если вы используете, то обратите внимание, что относительно недавно появился в этой функции аргумент CONFIGURE_DEPENDS, который по большому счету решает проблему, за которую file(glob) особо критиковали (ну, ту, что он не переконфигурирует автоматом проект при добавлении файлов).

Изначально хотел использовать file(glob), но при очередном гуглении очередного вопроса, прочитал, что лучше использовать target_sources.

Созрел еще один вопрос вдогонку к public header файлам. Если следовать рекомендациям использования target_sources, то соблюсти структуру дерева исходников не проблема, а как быть с public header файлами? У меня достаточно много подпапок, но для них не создаются CMakeLists.txt, по крайней мере я не видел такой практики.

Раз вы задаёте этот вопрос, значит вы используете подход "отдельная директория для public хедеров", как и в этой статье. Ну т.е. когда все публичные заголовочные файлы физически перемещаются в отдельную директорию.

В их папках обычно не создаётся CMakeLists просто потому, что с ними особо нечего делать, кроме как скопировать на этапе install (лично я советую делать это и на этапе сборки, особенно если есть генерируемые хедеры, и использовать их ТОЛЬКО из build-директории, а не из source-директории, но это вопрос отдельный). Раньше копировали как могли, потому что довольно давно существующее свойство таргета PUBLIC_HEADER не сохраняет структуру директорий для публичных хедеров, и потому им проще не пользоваться вовсе.

НО! В версии 3.23 наконец приехал стандартный механизм, который реально работает для публичных хедеров - file sets. Единственный на данный момент поддерживаемый тип file set-а это как раз HEADERS. Для этого типа даже автоматически изменяется INCLUDE_DIRECTORIES таргета. Ну и самое главное - при install(TARGETS) сохраняется относительная структура файлов и директорий - как раз то, что обычно нужно для публичных хедеров.

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

С PUBLIC_HEADER действительно есть проблемы, когда у вас хедеры раскиданы по разным директориям внутри include/. Вероятно для общего решения мне стоило использовать более традиционный подход - инсталлировать хедеры через install(DIRECTORY).

По поводу копирования хедеров в билд директорию. Это позволит упростить инсталляцию, плюс в target_include_directories(PRIVATE) указывать можно будет только путь до билд-директории (если я что-то еще упускаю, то напишите). За это придется заплатить тем, что ваш проект будет переконфигурироваться перед сборкой каждый раз, когда вы вносите изменение в header файлы. А это порой может занимать приличное время (например, при определенном использовании внешних зависимостей через ExtenalProject/FetchContent).

Нашел небольшой баг с тем, что MYLIB_SHARED_LIBS не оказывает влияние на сборку библиотеки, т.к. используется после того, как таргет mylib определен. Пофиксил этот баг в репозитории, плюс заменил PUBLIC_HEADER на install(DIRECTORY). Для общего решение это более универсальный подход (см. комментарий от @Nipheris и мой после него).

Использование определенного стандарта языка является тем необходимым, которое можно включать в CMakeLists?
Если готовить cmake файлы для исполняемых файлов, то я, полагаю, требования к содержимому CMakeLists.txt будут менее жесткие? Опции/флаги компиляции/линковки можно использовать в CMakeLists или все-таки надо выносить?

Ну если ваша библиотека требует определенного стандарта, то да.

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

Еще подъехала парочка вопросов :)

  1. Best practice.
    Разрабатывается некий проект под несколько ОС. Какой генератор лучше использовать конкретно под Windows: Ninja или Visual Studio XXX (под Windows используется именно студийный компилятор)? Почему?
    Если использовать Visual Studio XXX, то что делать с генерируемыми *.sln и *.vcxproj, для чего они мне?

  2. How do it?
    Столкнулся со следующей проблемой: используется связка мой main() + SDL. Под Windows не хочу запускать консольное окно + "окошечное", хочу сразу "окошечное", поэтому гугл говорит использовать флаги "LINK_FLAGS = /ENTRY:mainCRTStartup /SUBSYSTEM:WINDOWS". И да, действительно, если я в CMakeLists.txt укажу "set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/ENTRY:mainCRTStartup /SUBSYSTEM:WINDOWS")" - все работает (но странно, что Ninja генерирует: "LINK_FLAGS = /machine:x64 /debug /INCREMENTAL /subsystem:console /ENTRY:mainCRTStartup /SUBSYSTEM:WINDOWS", т.е. и консоль и окно вместе).
    Но, если флаги "LINK_FLAGS" вынести в пресет файл в секцию с настройками винды, т.е. освободить CMakeLists.txt от лишних зависимостей, то освободиться от запуска консоли не могу :( Как можно решить эту проблему, не засоряя CMakeLists.txt?

  1. если надо чисто построить проект, то в принципе можно брать любой генератор. Visual Studio генератор пригодится, если хочется использовать Visual Studio IDE (например, для правки или отладки).

  2. Возможно, надо использовать CMAKE_EXE_LINKER_FLAGS.

В документации сказано  "does not have to have a valid generator or binaryDir ". Имхо, это переводится как "не обязан иметь валидный generator или binaryDir, а не так, как вы написали. Я еще раз дополнительно проверил - по крайней мере моя текущая версия CMake (3.22.2) корректно обрабатывает hidden пресет с установленной binaryDir.

Эххх, я, наверное, уже слишком поздно пишу, но сейчас буквально работаю с тестами и CMake. Копировать DLL библиотеки под маздаем к исполняемому файлу с тестами может быть разумно, когда она одна. А когда их несколько, то начинаются танцы с бубном, по типу этих https://gist.github.com/micahsnyder/5d98ac8548b429309ec5a35bca9366da

А вот тут https://stackoverflow.com/a/76914147 вообще рекомендуют использовать set_tests_properties и прописывать в PATH путь до библиотек. Правда, у меня почему-то не срабатывает. Не знаю, может быть, есть идеи, как это сделать для больших проектов с кучами библиотек (по типу проектов на Qt, например). Каждый раз копировать библиотеки к бинарю с тестами выглядит как ужасный костыль...

Вообще мне нравится вариант, который вы скинули с test properties. Я бы его рекомендовал вместо копирования, если бы сегодня писал статью.

Почему не работает надо разбираться, можете в личку скинуть код, я попробую помочь.

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

set_tests_properties(
  some_tests PROPERTIES ENVIRONMENT
  "PATH=$<TARGET_FILE_DIR:lib_a>\;$<TARGET_FILE_DIR:lib_b>\;"
)

Суть в том, что я с самого начала пробовал эту версию, просто забыл, что у меня есть зависимость ещё и от lib_b :) А потом пробовал с ENVIRONMENT_MODIFICATION, но оно используется для одного изменения переменных окружения, то есть я пробовал одну и ту же переменную несколько раз обновить, но применялось только последнее изменение.

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории