Введение
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Запуск CMake
Ниже приведены примеры использования языка CMake, по которым Вам следует попрактиковаться. Экспериментируйте с исходным кодом, меняя существующие команды и добавляя новые. Чтобы запустить данные примеры, установите CMake с официального сайта.
Принцип работы
Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make). Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает.
Система сборки CMake принимает на вход файл CMakeLists.txt
с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.
Сгенерированные файлы будут содержать конкретные названия системных утилит, директорий и компиляторов, в то время как команды CMake орудуют лишь абстрактным понятием компилятора и не привязаны к платформенно зависимым инструментам, сильно различающихся на разных операционных системах.
Проверка версии CMake
Команда cmake_minimum_required
проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой. Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла:
# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
Как подметили в комментариях, команда cmake_minimum_required
выставляет все флаги совместимости (смотреть cmake_policy
). Некоторые разработчики намеренно выставляют низкую версию CMake, а затем корректируют функционал вручную. Это позволяет одновременно поддерживать древние версии CMake и местами использовать новые возможности.
Оформление проекта
В начале любого CMakeLists.txt
следует задать характеристики проекта командой project
для лучшего оформления интегрированными средами и прочими инструментами разработки.
# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
Стоит отметить, что если ключевое слово LANGUAGES
опущено, то по умолчанию задаются языки C CXX
. Вы также можете отключить указание любых языков путём написания ключевого слова NONE
в качестве списка языков или просто оставить пустой список.
Запуск скриптовых файлов
Команда include
заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include
языков C/C++. Этот пример запускает скриптовый файл MyCMakeScript.cmake
описанной командой:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
# Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake)
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
В данном примере, первое сообщение уведомит о том, что переменная TEST_VARIABLE
ещё не определена, однако если скрипт MyCMakeScript.cmake
определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной. Таким образом, скриптовый файл, включаемый командой include
, не создаёт собственной области видимости, о чём упомянули в комментариях к предыдущей статье.
Компиляция исполняемых файлов
Команда add_executable
компилирует исполняемый файл с заданным именем из списка исходников. Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe
или просто <ExecutableName>
). Типичный пример вызова данной команды:
# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
Компиляция библиотек
Команда add_library
компилирует библиотеку с указанным видом и именем из исходников. Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a
или <LibraryName>.lib
). Типичный пример вызова данной команды:
# Скомпилировать статическую библиотеку "MyLibrary" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageConsumer.c":
add_library(MyLibrary STATIC ObjectHandler.c TimeManager.c MessageConsumer.c)
- Статические библиотеки задаются ключевым словом
STATIC
вторым аргументом и представляют из себя архивы объектных файлов, связываемых с исполняемыми файлами и другими библиотеками во время компиляции. - Динамические библиотеки задаются ключевым словом
SHARED
вторым аргументом и представляют из себя двоичные библиотеки, загружаемые операционной системой во время выполнения программы. - Модульные библиотеки задаются ключевым словом
MODULE
вторым аргументом и представляют из себя двоичные библиотеки, загружаемые посредством техник выполнения самим исполняемым файлом. - Объектные библиотеки задаются ключевым словом
OBJECT
вторым аргументом и представляют из себя набор объектных файлов, связываемых с исполняемыми файлами и другими библиотеками во время компиляции.
Добавление исходников к цели
Бывают случаи, требующие многократного добавления исходных файлов к цели. Для этого предусмотрена команда target_sources
, способная добавлять исходники к цели множество раз.
Первым аргументом команда target_sources
принимает название цели, ранее указанной с помощью команд add_library
или add_executable
, а последующие аргументы являются списком добавляемых исходных файлов.
Повторяющиеся вызовы команды target_sources
добавляют исходные файлы к цели в том порядке, в каком они были вызваны, поэтому нижние два блока кода являются функционально эквивалентными:
# Задать исполняемый файл "MyExecutable" из исходников
# "ObjectPrinter.c" и "SystemEvaluator.c":
add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c)
# Добавить к цели "MyExecutable" исходник "MessageConsumer.c":
target_sources(MyExecutable MessageConsumer.c)
# Добавить к цели "MyExecutable" исходник "ResultHandler.c":
target_sources(MyExecutable ResultHandler.c)
# Задать исполняемый файл "MyExecutable" из исходников
# "ObjectPrinter.c", "SystemEvaluator.c", "MessageConsumer.c" и "ResultHandler.c":
add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c MessageConsumer.c
ResultHandler.c)
Генерируемые файлы
Местоположение выходных файлов, сгенерированных командами add_executable
и add_library
, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:
- Переменные
RUNTIME_OUTPUT_DIRECTORY
иRUNTIME_OUTPUT_NAME
определяют местоположение целей выполнения. - Переменные
LIBRARY_OUTPUT_DIRECTORY
иLIBRARY_OUTPUT_NAME
определяют местоположение библиотек. - Переменные
ARCHIVE_OUTPUT_DIRECTORY
иARCHIVE_OUTPUT_NAME
определяют местоположение архивов.
Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для "не-DLL" платформ динамические библиотеки рассматриваются библиотечными целями, а для "DLL-платформ" — целями выполнения. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles
.
Важно подметить, что "DLL-платформами" считаются все платформы, основанные на Windows, в том числе и Cygwin.
Компоновка с библиотеками
Команда target_link_libraries
компонует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable
или add_library
, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Пример:
# Скомпоновать исполняемый файл "MyExecutable" с
# библиотеками "JsonParser", "SocketFactory" и "BrowserInvoker":
target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker)
Стоит отметить, что модульные библиотеки не подлежат компоновке с исполняемыми файлами или другими библиотеками, так как они предназначены исключительно для загрузки техниками выполнения.
Работа с целями
Как упомянули в комментариях, цели в CMake тоже подвержены ручному манипулированию, однако весьма ограниченному.
Имеется возможность управления свойствами целей, предназначенных для задания процесса сборки проекта. Команда get_target_property
присваивает предоставленной переменной значение свойства цели. Данный пример выводит значение свойства C_STANDARD
цели MyTarget
на экран:
# Присвоить переменной "VALUE" значение свойства "C_STANDARD":
get_target_property(VALUE MyTarget C_STANDARD)
# Вывести значение полученного свойства на экран:
message("'C_STANDARD' property is equal to [${VALUE}]")
Команда set_target_properties
устанавливает указанные свойства целей заданными значениями. Данная команда принимает список целей, для которых будут установлены значения свойств, а затем ключевое слово PROPERTIES
, после которого следует список вида "<название свойства> <новое значение>":
# Установить свойству 'C_STANDARD' значение "11",
# а свойству 'C_STANDARD_REQUIRED' значение "ON":
set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
Пример выше задал цели MyTarget
свойства, влияющие на процесс компиляции, а именно: при компиляции цели MyTarget
CMake затребует компилятора о использовании стандарта C11. Все известные именования свойств целей перечисляются на этой странице.
Также имеется возможность проверки ранее определённых целей с помощью конструкции if(TARGET <TargetName>)
:
# Выведет "The target was defined!" если цель "MyTarget" уже определена,
# а иначе выведет "The target was not defined!":
if(TARGET MyTarget)
message("The target was defined!")
else()
message("The target was not defined!")
endif()
Добавление подпроектов
Команда add_subdirectory
побуждает CMake к незамедлительной обработке указанного файла подпроекта. Пример ниже демонстрирует применение описанного механизма:
# Добавить каталог "subLibrary" в сборку основного проекта,
# а генерируемые файлы расположить в каталоге "subLibrary/build":
add_subdirectory(subLibrary subLibrary/build)
В данном примере первым аргументом команды add_subdirectory
выступает подпроект subLibrary
, а второй аргумент необязателен и информирует CMake о папке, предназначенной для генерируемых файлов включаемого подпроекта (например, CMakeCache.txt
и cmake_install.cmake
).
Стоит отметить, что все переменные из родительской области видимости унаследуются добавленным каталогом, а все переменные, определённые и переопределённые в данном каталоге, будут видимы лишь ему (если ключевое слово PARENT_SCOPE
не было определено аргументом команды set
). Данную особенность упомянули в комментариях к предыдущей статье.
Поиск пакетов
Команда find_package
находит и загружает настройки внешнего проекта. В большинстве случаев она применяется для последующей линковки внешних библиотек, таких как Boost и GSL. Данный пример вызывает описанную команду для поиска библиотеки GSL и последующей линковки:
# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED)
# Скомпоновать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)
# Уведомить компилятор о каталоге заголовков "GSL":
target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS})
В приведённом выше примере команда find_package
первым аргументом принимает наименование пакета, а затем требуемую версию. Опция REQUIRED
требует печати фатальной ошибки и завершении работы CMake, если требуемый пакет не найден. Противоположность — это опция QUIET
, требующая CMake продолжать свою работу, даже если пакет не был найден.
Далее исполняемый файл MyExecutable
линкуется с библиотекой GSL командой target_link_libraries
с помощью переменной GSL::gsl
, инкапсулирующей расположение уже скомпилированной GSL.
В конце вызывается команда target_include_directories
, информирующая компилятора о расположении заголовочных файлов библиотеки GSL. Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS
, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета).
Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET
. Это можно сделать путём проверки переменной <PackageName>_FOUND
, автоматически определяемой после завершения команды find_package
. Например, в случае успешного импортирования настроек GSL в Ваш проект, переменная GSL_FOUND
обратится в истину.
В общем случае, команда find_package
имеет две разновидности запуска: модульную и конфигурационную. Пример выше применял модульную форму. Это означает, что во время вызова команды CMake ищет скриптовый файл вида Find<PackageName>.cmake
в директории CMAKE_MODULE_PATH
, а затем запускает его и импортирует все необходимые настройки (в данном случае CMake запустила стандартный файл FindGSL.cmake
).
Способы включения заголовков
Информировать компилятора о располжении включаемых заголовков можно посредством двух команд: include_directories
и target_include_directories
. Вы решаете, какую из них использовать, однако стоит учесть некоторые различия между ними (идея предложена в комментариях).
Команда include_directories
влияет на область каталога. Это означает, что все директории заголовков, указанные данной командой, будут применяться для всех целей текущего CMakeLists.txt
, а также для обрабатываемых подпроектов (смотреть add_subdirectory
).
Команда target_include_directories
влияет лишь на указанную первым аргументом цель, а на другие цели никакого воздействия не оказывается. Пример ниже демонстрирует разницу между этими двумя командами:
add_executable(RequestGenerator RequestGenerator.c)
add_executable(ResponseGenerator ResponseGenerator.c)
# Применяется лишь для цели "RequestGenerator":
target_include_directories(RequestGenerator headers/specific)
# Применяется для целей "RequestGenerator" и "ResponseGenerator":
include_directories(headers)
В комментариях упомянуто, что в современных проектах применение команд include_directories
и link_libraries
является нежелательным. Альтернатива — это команды target_include_directories
и target_link_libraries
, действующие лишь на конкретные цели, а не на всю текущую область видимости.
Установка проекта
Команда install
генерирует установочные правила для Вашего проекта. Данная команда способна работать с целями, файлами, папками и многим другим. Сперва рассмотрим установку целей.
Для установки целей необходимо первым аргументом описанной функции передать ключевое слово TARGETS
, за которым должен следовать список устанавливаемых целей, а затем ключевое слово DESTINATION
с расположением каталога, в который установятся указанные цели. Данный пример демонстрирует типичную установку целей:
# Установить цели "TimePrinter" и "DataScanner" в директорию "bin":
install(TARGETS TimePrinter DataScanner DESTINATION bin)
Процесс описания установки файлов аналогичен, за тем исключением, что вместо ключевого слова TARGETS
следует указать FILES
. Пример, демонстрирующий установку файлов:
# Установить файлы "DataCache.txt" и "MessageLog.txt" в директорию "~/":
install(FILES DataCache.txt MessageLog.txt DESTINATION ~/)
Процесс описания установки папок аналогичен, за тем исключением, что вместо ключевого слова FILES
следует указать DIRECTORY
. Важно подметить, что при установке будет копироваться всё содержимое папки, а не только её название. Пример установки папок выглядит следующим образом:
# Установить каталоги "MessageCollection" и "CoreFiles" в директорию "~/":
install(DIRECTORY MessageCollection CoreFiles DESTINATION ~/)
После завершения обработки CMake всех Ваших файлов Вы можете выполнить установку всех описанных объектов командой sudo checkinstall
(если CMake генерирует Makefile
), или же выполнить данное действие интегрированной средой разработки, поддерживающей CMake.
Наглядный пример проекта
Данное руководство было бы неполным без демонстрации реального примера использования системы сборки CMake. Рассмотрим схему простого проекта, использующего CMake в качестве единственной системы сборки:
+ MyProject
- CMakeLists.txt
- Defines.h
- StartProgram.c
+ core
- CMakeLists.txt
- Core.h
- ProcessInvoker.c
- SystemManager.c
Главный файл сборки CMakeLists.txt
описывает компиляцию всей программы: сперва происходит вызов команды add_executable
, компилирующей исполняемый файл, затем вызывается команда add_subdirectory
, побуждающая обработку подпроекта, и наконец, исполняемый файл линкуется с собранной библиотекой:
# Задать минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
# Указать характеристики проекта:
project(MyProgram VERSION 1.0.0 LANGUAGES C)
# Добавить в сборку исполняемый файл "MyProgram":
add_executable(MyProgram StartProgram.c)
# Требовать обработку файла "core/CMakeFiles.txt":
add_subdirectory(core)
# Скомпоновать исполняемый файл "MyProgram" со
# скомпилированной статической библиотекой "MyProgramCore":
target_link_libraries(MyProgram MyProgramCore)
# Установить исполняемый файл "MyProgram" в директорию "bin":
install(TARGETS MyProgram DESTINATION bin)
Файл core/CMakeLists.txt
вызывается главным файлом сборки и компилирует статическую библиотеку MyProgramCore
, предназначенную для линковки с исполняемым файлом:
# Задать минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
# Добавить в сборку статическую библиотеку "MyProgramCore":
add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c)
После череды команд cmake . && make && sudo checkinstall
работа системы сборки CMake завершается успешно. Первая команда запускает обработку файла CMakeLists.txt
в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпонованный исполняемый файл MyProgram
в систему.
Заключение
Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю. До скорых встреч!
Комментарии (73)
mapron
05.12.2018 02:29cmake_minimum_required(VERSION 3.0)
Данный код, помимо проверки версии, что немаловажно, еще и выставляет все флаги совместимости (cmake policy) в соответствие с этой версией.
Можно выставить низкую версию, и нужное поведение, сломавшее обратную совместимость после ее выхода докрутить ручками. Так делают разработчики библиотек, которые хотят поддерживать древние версии cmake, но при этом и новые фичи местами использовать.
mapron
05.12.2018 02:31project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
У кого-то может возникнуть вопрос — а нафига может понадобиться вообще версию там указывать?
Это может использоваться в двух вещах:
-автоматическое версионирование so/dylib на unix-платформах (симлинки, все дела)
-интеграция с CPack и выставление версии продукта тамDiversus
05.12.2018 17:03-интеграция с CPack и выставление версии продукта там
Этот способ не удобен тем, что если версия нам нужна в основном проекте (например, для диалога «О программе»), то в этом случае приходится версию дублировать и в текст самого проекта (если разработка ведется, например, в Visual Studio) и в CMakeList.txt.
Т.е. два раза в CMakeList.txt и в какой-нибудь version.h, который подключен к проекту. Я нашел способ, чтобы это делать только в одном месте.
Как установить версию и для CMake (CPack) и для проектаСоздаем version.h и подключаем его в проекте со следующим содержимым:
#define SERVER_VERSION "1.0.0.9" // Номер версии, который используем в основной программе, например для вывода в окно "О программе".
А для CPack можно получить версию вот так:
file(READ "version.h" ver) string(REGEX MATCH "\x22([0-9]*.[0-9]*.[0-9]*.[0-9]*)\x22" _ ${ver}) set(version_h ${CMAKE_MATCH_1}) set(CPACK_PACKAGE_VERSION "${version_h}")
Т.е. читаем из version.h по регулярному выражению и присваиваем переменной CPACK_PACKAGE_VERSION эту версию собранную из файла проекта.Wilk
05.12.2018 20:41Здравствуйте!
Я для себя нашёл обратный способ — генерация функций для возврата информации о версии на основании описания проекта CMake. Фантазия у меня разыгралась, поэтому в результате генерируется несколько функций получения номера версии, в т.ч. с идентификатором коммита git, информацией о состоянии репозитория, из которого произведена сборка, числом коммитов с последнего тега. В последней версии добавилась также возможность вывода информации о лицензии, разработчиках и примечаний (NOTICE). Всё это генерируется в купе с ресурсами для MSVS, чтобы в проводнике можно было посмотреть версию (в упрощённом виде, разумеется) и информацию о разработчике.mapron
05.12.2018 20:42Про последнее — поддерживаю, написать один раз application.rc.in, туда всю метаинформацию упрятать и использовать шаблон для разных приложений (если в продукте оно не одно)
Wilk
05.12.2018 20:52Здравствуйте!
Я пошёл дальше — написал набор скриптов для CMake (извиняюсь за всё, что есть по ссылке), позволяющих более-менее стандартизировать сборку проектов. Замечу, что для меня стандартизировать значит не только применять общие принципы, но и иметь возможность копировать файл одного проекта в другой, поправить несколько название проекта и зависимости, и иметь рабочий проект с версионированием и всем, всем, всем. Копипаст — смертный грех, но ничего с собой поделать не могу.mapron
05.12.2018 21:03Посмотрел. Вопросы, конечно, возникли, но раз вы извинились, то озвучивать ничего не буду)
В целом есть ощущение что для CMake не хватает своего подобия boost или GSL — все лепят свои велосипеды (у KDE тоже есть свое подобие utk). Хорошо бы заморочиться сделать единый стиль и единые гайдлайны, которые покроют потребности большинства)
mapron
05.12.2018 20:41Ну не, я с вами не согласен. Если версяи нужна в диалоге о программе, то делаем так:
1. создаем version.h.in:
#include <string> namespace { const std::string ProjectVersion = "@CMAKE_PROJECT_VERSION@" }
2. вызываем
configure_file(${CMAKE_SOURCE_DIR}/path/to/version.h.in version.h)
3. включаем в коде диалога хидер version.h, радумемся
Парсинг хедер файлов — не очень идея (хотя в редких случаях так можно сделать для какого-то 3rdparty кода, чтобы не дублировать константы у себя).
mapron
05.12.2018 02:43Теперь Вы способны писать свои и понимать чужие CMake-файлы
Мне кажется, без объяснения механизма cmake_parse_arguments это вообще невозможно.
Эта команда позволяет легко и непринужденно воротить в собственном проекте некое подобие DSL поверх cmake, например:
AddTarget(NAME expat BASEDIR ${CMAKE_CURRENT_SOURCE_DIR}lib/ CSRC xml*.c DEFINES -D_LIB -DCOMPILED_FROM_DSP -DXML_STATIC -DWIN32 EXPORT_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}lib/ )
(скопипастил из реального кода).
Мы вроде помним, что в cmake иного типа, нежели vector(string), нет. Как же достигается построение функций, которые принимают нечто похожее на хеш-таблицы?
Очень просто, вводятся некоторые «маркерные» значения. Они могут быть либо опциями сами по себе, либо сигнализировать, что после них идет значение (одинарное либо список).
функция cmake_parse_arguments принимает всё это добро и раскладывает по полочкам — каждый список либо значение в отдельную переменную.
Это позволяет создавать из процедурного языка — некое подобие декларативного. Этим подходом в разной степени пользуются все крупные проекты.
Вот пример из кода opencv, не для целей, а для опций, правда:
OCV_OPTION(OPENCV_GENERATE_SETUPVARS "Generate setup_vars* scripts" ON IF (NOT ANDROID AND NOT APPLE_FRAMEWORK) )
p.s. а еще можно там реализовать вещи вроде
AddTarget(expat DEFINES [ UNIX ? COMPILED_UNDER_UNIX : COMPILED_UNDER_DOORS ] )
Квадратные скобки это вообще удивительная магия, про них можно отдельную статью писать)Gymmasssorla Автор
06.12.2018 00:37Механизм разбора аргументов добавлен в раздел «Разбор аргументов» предыдущей статьи (т.к. это момент синтаксиса). Спасибо за участие!
mapron
05.12.2018 02:52Цели в cmake — потрясающая вещь. Это с одной стороны, глобальная переменная, которая при этом еще и объект) К сожалению, из-за примитивного синтаксиса, так просто их методы не подергать, приходится довольствоваться get_target_property/set_target_properties. Одно из замечательных свойств то, что можно для цели выставлять свои значения, не определенные стандартом (но и по понятным причинам без особой надобности это лучше не делать — огребешь проблем с совместимостью).
Почему я так положительно об этом отзываюсь? Ну, например, это позволяет написать систему фиксапа (расчета рантайм-зависимостей) полностью определяемую на этапе конфигурирования — она будет работать куда быстрее чем то что есть в bundleUtiities.
уместным тут будет упомянуть функцию if (TARGET smth), которая позволяет проверить что цель уже определена (если например, вы хотите реализовать жесткий порядок определения зависимостей в своей DSL-функции)Gymmasssorla Автор
05.12.2018 18:52Добавлен раздел «Работа с целями». Про возможность проверки определения цели сам не знал)
ksergey01
05.12.2018 10:32По моему эта строчка лишняя, т.к. в зависимостях есть GSL::gsl (target_link_libraries(...))
include_directories(${GSL_INCLUDE_DIRS})
Gymmasssorla Автор
05.12.2018 15:25В любом случае, строчку с include_directories я оставлю, так как не знаю где ещё в руководстве можно рассказать про эту часто используемую команду)
ksergey01
05.12.2018 15:30Лучше расскажите про target_include_directories(...) и чем она отличается от include_directories
Wilk
05.12.2018 15:54+1Здравствуйте!
Стоит также упомянуть, что функции include_directories() и link_libraries() не рекомендованы для использования в современных проектах, т.к. работают на уровне дирректории, а не с целями. Рекомендованный вариант это использование target_include_directories() и target_link_libraries(), причём для указания зависимостей желательно использовать именно последнюю, т.к. современный CMake крутится вокруг целей, всё же, а не переменных. Конечно, очень много библиотек-староверов, не предоставляющих (полноценых) пакетов CMake, но это уже отдельная история, в которой надо либо внести соответствующее изменение в библиотеку, если разработчик принимает патчи, либо написать свой модуль, который добавляет необходимые импортированные цели.Gymmasssorla Автор
05.12.2018 16:21-1Добавлено в раздел «Способы включения заголовков». Спасибо за полезное замечание!)
DaylightIsBurning
07.12.2018 01:39А есть какой-то ключ что бы cmake ругался на все устаревшие функции типа include_directories(), подобно -Wall/CoreGuidlenes?
Wilk
07.12.2018 01:57Здравствуйте!
Насколько мне известно — нет. Самое большее, что можно получить, это предупреждения на тему различных устаревших политик, однако политики, насколько я понимаю, затрагивают лишь отдельные аспекты поведения CMake, но не делают какие-либо функции, особенно стандартные, устаревшими. Подобный вопрос не так давно поднимался в списке рассылки, и Craig Scott отметил, что тема предупреждений об устаревших/плохих практиках всё больше интересует сообщество, но на данный момент никаких подвижек в сторону реализации предупреждений нет.mapron
07.12.2018 14:01Здравствуйте! (простите, не удержался)
я бы не назвал include_directories устаревшей, она может быть использована в некоторых случаях: когда ты делаешь порт под какой-то тулчейн, в котором компилятор автоматически не знает про необходимые пути к стандартной библиотеке (ембедед gcc тулчейн, с несколькими возможным целями для разных девайсов — с хидрами вроде watchdog и тп).
В таком случае include_directories в файле тулчейна вполне может использоваться.
yaroslavche
05.12.2018 16:44Спасибо за статью, очень познавательно. У меня такой вопрос: как узнать список установленных библиотек, которые я могу найти с помощью
find_library
? Или вот более конкретный пример: есть библиотека PHP-CPP. Я её себе установил и в своём CMakeLists.txt хочу использовать как зависимость:
find_library(PHPCPP phpcpp) if(NOT PHPCPP_FOUND) message(SEND_ERROR "Failed to find PHP-CPP") return() endif()
Но так не работает. Что я делаю не так? Объясните, пожалуйста, как правильно работать з зависимостями и о функциях
find_package
,find_library
иfind_file
.
Кстати, довольно удобная функция
find_program
в сочетании сexecute_process
(может кому пригодится):
cmake_minimum_required(VERSION 3.10) project(MyProject VERSION 1.2.3.4 LANGUAGES NONE) FIND_PROGRAM(DOCKER NAMES docker) if(NOT DOCKER) message(SEND_ERROR "Failed to find docker") return() endif() execute_process( COMMAND ${DOCKER} --version OUTPUT_VARIABLE DOCKER_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ) message("${DOCKER_VERSION}") # выведет примерно следующее: "Docker version 17.09.1-ce, build f4ffd2511ce9" # или "Failed to find docker"
mapron
05.12.2018 20:47Отвечаю: find_package(phpcpp) будет искать скрипт FindPhpcpp.cmake по путям, известным cmake (MODULE_SEARCH_PATH, а так же его стандартная библиотека).
find_file — самая низкоуровневая штука. Говоришь имя файла, и где искать — функция перебирает известные директории, и если находит, то выставляет выходную переменную в значение полного пути к файлу.
find_library — некоторая расширенная версия предыдущей функции. Может найти в перечисленных каталогах статичную библиотеку, динамическую, а так же учитывает платформозависимый префикс вроде «lib» и версионные симлинки (unix).
Эти две функции обычно разработчиками cmake-биндингов библиотеки активно используются в Find* скриптах.
find_package — высокоуровневая штука. вы ей говорите — «хочу Qt5Core», она и инклюды найдет, и библиотеку, и флаги, если нужно дополнительные определит. Обычно получается что у вас уже есть Qt5::Core какой-нибудь или boost::thread, который вы просто указываете в target_link_libraries и радуетесь в жизни (инклюды прокинутся тоже).
amarao
05.12.2018 18:00wc -l /github/FreeCAD/CMakeLists.txt
1186 /github/FreeCAD/CMakeLists.txt
Больно.0xd34df00d
05.12.2018 19:20Да что вы знаете о боли.
d34df00d@deadaven ~/Programming/someProjectOfMine/src (master) % find ./ -name CMakeLists.txt -exec cat '{}' \; | wc -l 10012
amarao
05.12.2018 19:21+1Ещё больнее. Я знаю, многие программисты (особенно, на Си/С++) привыкли к четырёхзначным номерам строк, но мне кажется, что если модуль такого размера, то его архитектуру кто-то плохо продумал. Кстати, а CMake поддерживает модулярность? 10к строк уже очень просятся на что-то вроде CMakeList.d/
Gymmasssorla Автор
05.12.2018 19:32CMake поддерживает модульность. Командой “include” можно исполнять скрипты на CMake, это было описано в разделе “Запуск скриптовых файлов”.
0xd34df00d
05.12.2018 19:32Там не зря
find
. 185 файловCMakeLists.txt
суммарно.
А, блин, это я ещё кастомные
FindFoo.cmake
не считал. Ну и ладно.
mapron
05.12.2018 20:49У нас примерно 10к cmake скриптов на 1M строк С++. мне кажется, это разумное соотношение. С учетом того что там делается много всего — работа с пакетами, бандлы, обжимка разных инсталляторов и их перепаковка и тп.
DaylightIsBurning
05.12.2018 20:01+1И это ужасно. Как бы хотелось, что бы зависимости прописывались единственной строкой типа «import boost/graph», что бы не нужен был весь этот cmake.
Gymmasssorla Автор
05.12.2018 20:49Полностью согласен с Вами. Я думаю, что подобные решения уже существуют на рынке, просто никто ими не пользуется из-за их непопулярности.
Было-бы неплохо стандартизировать поиск зависимостей, чтобы не писать свой велосипедо-Find каждый раз для каждой библиотеки, если это возможно…
AMDev
05.12.2018 18:13Было бы здорово разобрать прмер проекта для кросплатформенной сборки,
например вот такой: https://github.com/cginternals/cmake-init
Полезные ссылки по cmake: awesome-cmakeGymmasssorla Автор
05.12.2018 18:53Добрый вечер. Да, идея действительно хорошая и достойна отдельной статьи. Возможно, четвёртая статья будет на эту тему)
shukan
05.12.2018 19:37Подскажите пожалуйста, как нужно прописать, чтобы в vcxproj попадал .lib файл, а не .dll файл?
Имеем проект отсюда github.com/gost-engine/engine пробуем его собрать.
Имеем запись в CMakeLists.txt следующего вида:
add_executable(test_grasshopper test_grasshopper.c)
target_link_libraries(test_grasshopper gost_engine gost_core ${OPENSSL_CRYPTO_LIBRARY})
add_test(NAME grasshopper
COMMAND test_grasshopper)
А ниже описание для gost.dll (и gost.lib наверное, тоже, я не очень понимаю, что это задает, если честно):
add_library(gost_engine MODULE ${GOST_ENGINE_SOURCE_FILES})
set_target_properties(gost_engine PROPERTIES PREFIX "" OUTPUT_NAME "gost")
target_link_libraries(gost_engine gost_core ${OPENSSL_CRYPTO_LIBRARY})
В итоге вызов:cmake -DCMAKE_BUILD_TYPE=Release ..
приводит к тому что в файле test_grasshopper.vcxproj получаем для AdditionalDependencies:
bin\Release\gost.dll;Release\gost_core.lib;...
А надо .lib вместо .dll иначе в Visual Studio не собирается, надо так (прописываю ручками):
Release\gost.lib;Release\gost_core.lib;...
Уже всю голову себе сломал, что же такого поменять в настройках для test_grasshoperWilk
05.12.2018 20:47Здравствуйте!
Попробуйте изменить тип библиотеки с MODULE на SHARED. Я не работал с модульными библиотеками, но они, насколько я могу судить по документации, не предназначены для использования в качестве зависимостей времени сборки. Их основное назначение это использование в связке с функциями динамической загрузки библиотек времени выполнения (dlopen и т.п.). Поэтому вполне может быть, что CMake считает, что надо компановать проект с dll, а не с lib.shukan
05.12.2018 21:54SHARED помогло, спасибо!
А я test_grasshoper долго мучал, а надо было add_library.
Usul
06.12.2018 11:39Спасибо за статью!
Поясните пожалуйста вот такой момент:
set(LIBRARY_TYPE SHARED) if (STATIC) set(LIBRARY_TYPE) endif() #... add_library (libtesseract ${LIBRARY_TYPE} ${tesseract_src} ${tesseract_hdr} ${tesseract_rsc} )
Отсюда
Переменная STATIC в коде не задается. Правильно ли я понимаю, что для компиляции в виде статической библиотеки нужно определять STATIC через командную строку cmake?
Причина вопроса: пытаюсь понять, как правильно собрать библиотеку tesseract и ее зависимости в виде статических библиотек, чтобы в итоге можно было создать приложение для OCR в виде единственного исполняемого файла.Gymmasssorla Автор
06.12.2018 11:46Да, если такой код запустить с флагом "-DSTATIC=TRUE", то библиотека скомпилируется как статическая (т.к. переменная «LIBRARY_TYPE» затем обратится в пустую строку, a «add_library» по умолчанию компилирует статическую библиотеку).
Usul
06.12.2018 12:41Спасибо, все получилось!
mapron
06.12.2018 13:20Совет: лучше использовать общеупотребительный
cmake.org/cmake/help/v3.0/variable/BUILD_SHARED_LIBS.html
чтобы у других разработчиков было привычное понимание) Ну это если вы действительно хотите переключалку режима сборки.Usul
07.12.2018 04:50Спасибо! Если придется делать собственный cmake-проект, обязательно так и поступлю.
Вообще, главная сложность при изучении инструментария типа cmake — как раз такие вот «лучшие практики». Из справочной документации и обзорных статей их почерпнуть непросто. По некоторым тулзам есть специальные руководства и даже книги. Для git, например, есть Pro Git. Есть ли что-нибудь подобное по cmake?
Gymmasssorla Автор
07.12.2018 10:32- Книга "Mastering CMake", возможно там явно рассказывают про лучшие практики.
- Статья про улучшение скриптов CMake, даны несколько полезных советов.
- В документации тоже встречаются некоторые "наставления": тут рекомендуется применять "target_include_directories" вместо "include_directories".
mapron
07.12.2018 13:57Прочитал Mastering Cmake. Из того что узнал для себя — все-таки можно делать плагины, Loadable Commands называются — уж как я не пробовал искать в гугле подобное поведение! Надо будет поковыряться, что с ними можно вытворить (к слову, даже когда я в багтрекере cmake спрашивал про подобную функциональность, мне ничего не сказали).
По поводу best practices — да вот хз. Есть некоторые хорошие советы, например разделы про портирование на новую платформу и кросскомпиляцию — они зачетные.
А в остальном… тема взаимодействия кастомных целей и команд вообще слабо раскрыта.
Ryppka
06.12.2018 13:07В начале любого CMakeLists.txt следует задать характеристики проекта командой project для лучшего оформления интегрированными средами и прочими инструментами разработки.
Мне кажется, стоит упомянуть, что между требуемой версией и объявлением проекта может быть полезным добавить:
- Включение файла локальных настроек для конкретной машины, т.к. расположение компиляторов, зависимостей и т.д. на разных машинах может отличаться
- Дополнительные пути для поиска модулей CMake, т.к. они могут потребоваться уже при объявлении проекта
- включить тулчейн-файл для кросс-компиляции. На мой вкус, это может быть удобнее, чем включать его через командную строку
- И наконец иногда требуется просто задать какие-то дополнительные переменные
mapron
06.12.2018 13:22+1Да, например, под маком переменную CMAKE_OSX_SYSROOT нужно выставлять обязательно ДО команды project().
но это все такие нюансы, с которыми может не каждый столкнуться, а пихать все-все в статью для новичков — только отпугнет их… нужно как-то придерживаться золотой середины между простотой и полнотой.
staticmain
А как экспортировать header-файл для библиотеки? Чтобы руками не составлять header для внутреннего использования и header для использования в другом проекте. В интернете есть ссылка на generate_export_header, вот только везде все тупо копируют один пример из родной доки на экспорт класса, а на функции\enum\define\const примеров нет нигде и заставить это работать не выходит.
Wilk
Здравствуйте!
Экспорт функции:
Что Вы имеете в виду, говоря про экспорт «enum\define\const»?
staticmain
Буквально. Вот пример части файла, который генерирует моя система сборки на основе маркеров в исходном коде:
Wilk
Возможно я что-то упускаю, но для того, чтобы использовать макроопределения библиотеки в пользовательском коде экспортировать их не надо, также как псевдонимы типов (typedef), определения POD типов (C struct) и перечисления (enum).
Я не пользовался возможность экспорта констант (const int kMyConstatn, например), т.к. мне это кажется не слишком правильным, но можно попробовать какой-то из вариантов, описанных здесь с поправкой на использование MY_LIBRARY_EXPORT макроса вместо __declspec(dllexport).
staticmain
Ничего не понятно. Как пользовательский компилятор поймет, что map_t это структура с 50 полями определенных типов?
В каком смысле? А константы для правильного использования библиотеки? А коэффициенты для функций, которые она предоставляет? Да возьмите любую библиотеку для работы с математикой, там различных констант типа
Будет с полсотни. А физические движки? А расчетные библиотеки? Да банально библиотеки работы с XML уже предоставляют константы, обозначающие тип узла.
Wilk
Какое отношение макроопределение имеет к некоему map_t? А именно макроопределения и создаются при помощи define. Если вы говорите про псевдонимы типов вида
то вопрос в том, нужно ли компилятору пользователя знать о структуре объектов типа map_t, т.е. является ли полное определение типа часть интерфейса библиотеки. Если является, то определение типа должно находится в интерфейсном заголовочном файле и быть доступно компилятору при компиляции пользовательского кода. Если не является, т.е. объекты типа используются исключительно через указатели, передаваемые на вход функциям библиотеки, то необходимости в предоставлении пользователю определения типа нет, и можно в интерфейсных заголовочных файлах использовать
Внутри библиотеки при этом можно использовать приватный заголовочный файл, которые не входит в интерфейс библиотеки и не поставляется пользователю, Этот файл включает в себя интерфейсный файл, в котором объявлен псевдоним, после чего содержит определение типа struct map. Такой подход, если он используется, позволяет создавать обратно совместимые библиотеки, в которых вся информация об устройстве типов содержится исключительно внутри библиотеки, и которые не требуют изменения или перекомпиляции пользовательского кода при изменении используемых структур данных. Разумеется, всё это при условии, что сигнатуры функций не изменяются.
enum?
Да, для математики использование наборов констант является вполне нормальным. Хотя, если я не ошибаюсь, конечно, в большинстве случаев такие константы не экспортируются — они просто определены в заголовочных файлах и могут быть напрямую использованы. Честно скажу, никогда не задумывался над тем, в какой код это всё генерируется.
enum? Кроме того, использование констант-идентификаторов типов узлов является не единственным вариантом. Если мы говорим не про C, то разные типы узлов вполне могут быть представлены различными полиморфными типами. Строго говоря, и в C можно что-то подобное сделать, но это потребует большего количества кода и будет работать хуже.
После прочтения Ваших вопросов мне не вполне понятно, об одном и том же мы говорим или нет. Я не совсем уловил связь между интерфейсными заголовочными файлами библиотеки и экспортом символов из библиотеки. Разумеется, экспорт символом происходит из интерфейсных заголовочных файлов, но как это связано с объявлениями и определениями типов и макроопределениями, для меня является загадкой.
staticmain
Судя по всему мы друг друга недопонимаем.
Как разработчик библиотеки я создаю функции, макроопределения, константы, перечисления, структуры, объединения и прочие типы данных, которые использую внутри библиотеки для реализации внутренних и внешних функций.
Какие-то из этих объектов предназначены только для того, чтобы быть использованными внутри библиотеки (например для реализации межпотокового хранилища объектов). Какие-то же наоборот, должен использовать пользователь для того, чтобы передать какие-то параметры в функции и\или иметь контроль над внутренним состоянием библиотеки.
Именно эти внешние объекты (функции, макроопределения, константы, перечисления, структуры, объединения и прочие типы данных) должны быть во внешнем заголовочном файле, который будет подключать пользователь. Испокон веков кто-то создавал руками два файла, дублируя код и имея вероятность забыть исправить какой-то тип; кто-то (например как поступил я), написал свою систему сборки, в которой нужный для внешнего header файла объект помечается слово EXPORT и он автоматически оказывается в генерируемом файле. Судя по всему cmake обещает подобную функциональность. Как ее использовать?
Wilk
Я примерно представляю, чего Вы хотите добиться, т.к. минимальный опыт создания библиотек для пользования третьими лицами у меня есть.
Честно говоря, не совсем понимаю, к чему такие сложности. Всегда, когда мне нужно создать какой-то заголовочный файл, который должен быть использован конечными пользователями, и который также используется в коде самой библиотеки, я создавал его в единственном экземпляре. Если компиляция библиотеки требует определений, отсутствующих в публичном файле, то эти определения размещаются в приватном заголовочном файле, которые не доступен пользователю библиотеки и используется только во время её сборки. Причин для дублирования кода я, честно говоря, не вижу.
CMake, в свою очередь, обещает несколько иное. Помимо предоставления пользователю заголовочных файлов необходимо также предоставлять и файлы, которые будут использованы компановщиком для разрешения ссылок на символы (функции, статические переменные и т.п.) во время компановки исполняемых файлов или разделяемых библиотек пользователя. Для того, чтобы символы попали в файл компановщика, они должны быть экспортированы.
В случае GCC всё просто, т.к. по умолчанию он экспортирует вообще все символы из библиотек.
В случае MSVC всё не так просто, т.к. его поведение по умолчанию прямо противоположное. Для того, чтобы экспортировать символ из библиотеки, компилируемой MSVC, необходимо при компиляции объявить его с аттрибутом (?) __declspec(dllexport), а при использовании библиотеки с аттрибутом __declspec(dllimport).
Поддерживать две отдельные версии заголовочных файлов, как Вы правильно заметили, мягко говоря не удобно, поэтому общей практикой является использование одного файла, в котором экспортируемые символы помечены специальным макросом, который разворачивается в экспортирующий или импортирующий аттрибут в зависимости от того, собирается сама библиотека (определяется по присутствию специального макроопределения), либо собирается код, использующий библиотеку.
CMake позволяет одной командой сгенерировать заголовочный файл, содержащий необходимые макроопределения и логику обработки типа сборки (сама библиотека или пользовательский код).
Видимо да, это было непонимание, т.к. у нас с Вами разные определения «экспорта».
staticmain
Окей, где взять пример экспорта чего-то кроме объекта? С ходу завести это не получилось, файл просто не генерируется. В сети просто перепечатка с оф.мануала где есть только пример экспорта объекта.
Да не причем тут компоновщик. Просто из одного хэдера скопировать определение (которое не попадет в PE/ELF) в другой, генерируемый.
Это не очень удобно, когда есть один публичный хэдер на 3000+ строк и аккуратно разбитые на субкаталоги и нейминги приватные. Это очень НЕ удобно искать, где объявлена константа, особенно когда A, B, D объявлены в привате, а C во внешнем.
Wilk
При том, что генерируемый CMake файл предназначен именно для формирования библиотечных файлов, потребляемых компоновщиком.
Вот пример генерируемого заголовочного файла (сборка MSVC):
Вот пример с экспортом нескольких функций:
Собственно, я стараюсь не делать файлы на 3000+ строк. Что мешает также аккуратно разбить публичный файл на отдельные, логически завершённые файлы, разложить их по отдельным папкам, а потом создать один общий заголовочный файл (если он насколько нужен) включающий все отдельные заголовочные файлы. На мой взгляд, и порядка больше, и дублировать ничего не надо. Хотя мой опыт, в силу ничтожности, может быть не показателен.
mapron
Я внимательно следил за всей вашей веткой, решил все же высказаться.
Публичный интерфейс (хедер) в случае использования его снаружи библиотеки и изнутри должен полностью совпадать.
То что не используется снаружи — помещается в приватные хедеры (неэкспортируемые функции, макросы, и тд).
Внутри библиотеки мы используем и публичные и приватные хедеры.
Далее, если одна и та же функция должна быть скомпилирована по-разному в зависимости от ее использования (как в случае динамического экспорта в VC), то для таких нужд необходимые символы отмечаются макросом. обычно это что-то вроде LIBNAME_EXPORT или LIBNAME_API.
Определение этого макроса можно помещать как в этом же публичном хидере, ручками
, либо вынести в генерируемый export_some_lib.h, который создавать на основе имени библиотеки. cmake автоматически создаст нужный дефайн для того чтобы предварять имена символов, а так же дефайн, который нужно будет передавать в качестве определения при сборки цели библиотеки (cmake это тоже может сделать автоматически).
Не вижу ни одного реального кейса чтобы «копировать определения из одного хидера в другой».
если нужно после установки придать другие имена хидерам, другое дело — это делается как в libc++
github.com/llvm-mirror/libcxx/blob/master/include/CMakeLists.txt
так и в Qt, например.
Само содержимое хедеров не меняется.
про публичный хидер на 3000 строк — ну и заинклюдить в нем все необходимые подфайлы, делов-то)
staticmain
Когда не все коды для какой-либо функции должен использовать пользователь. Такое было уже несколько раз. Примерно так
Таким образом одно семантически сгруппированное объявление оказывается «разорвано» в процессе разработки. При раздельных header будет такая ситуация:
header_private.h
header_public.h
Открыв в редакторе файл header_private.h можно подумать, что это все доступные константы и принять неверное решение.
Когда public генерируется ситуация иная:
Все возможные константы семантического блока можно увидеть сразу.
mapron
Что-то не понял, приведенный фрагмент кода не нужно генерировать, это ж объявление которое вы можете использовать и снаружи, и внутри. В чем проблема?
Для enum можно использовать блок #ifdef INTERNAL_COMPILATION для определения значений для внутреннего пользования.
Но в любом случае, у меня ощущение какого-то глобального просчета в проектировании интерфейса, ибо через одну «дырку», которую видит пользователь не должны ходить и публичные значения, и какие-то секретные.
Для меня это звучит как примерно «если в функцию fopen в качестве mode передать „пыщь-пыщь“, то будет удалена вся файловая система».
В хорошем публичном интерфейсе не должно быть никаких скрытых параметров.
Но повторюсь, если волей «исторически сложилось» такие параметры нужны — они заключаются в ifdef блоки.
dllimport — да, через макрос. Если нужна поддержка win.
FloorZ
А зачем такой геморрой со сборкой?.. Я почитал ветку комментариев и не вдупляю одного. Зачем генерировать что-то?
Есть у нас хедеры с экспортируемым функционалом? — так сделать же можно под них просто отдельный .h и все. А приватный функционал — в другой .h.
Типо того:
И экспортировать только те хэдеры, в которых публичный функционал.
staticmain
FloorZ
А зачем раздувать публичный хедер на 3000+ строк? В чем цель? Что бы IDE у пользователей по два часа его парсил на поиск констант, функций и прочее?
Не проще ли разбить его на много мелких хедеров по функциональности. Что бы подключать только тот заголовок, функции которого ты будешь юзать.
Что именно не удобно искать? Вроде бы сейчас любая IDE и любой вменяемый текстовый редактор типо Атом или VSCode умеет подсвечивать все что угодно. Зажал контрл — он тебе показал константу, в каком хедере лежит и сразу открыл в доп-вкладке.
Если есть константы, которые очень прям важные такие и часто на них, по какой то не ведомой мне причине надо смотреть — выдели их в отдельный хедер или классифицируй по разным файлам, что бы по смыслу было понятно, что константы буффера например лежат в buffer.h, а константы например потокового менеджера в thread_manager.h и т.д.
Ты же не подключаешь весь stl разом? А подключаешь только то, что используешь и по мере надобности.
staticmain
Если ваша IDE загибается от 3000 строк, то как она парсит stl?
Не все программируют в Atom или VSCode. Существуют и другие редакторы, которые не едят по 4 GB RAM и не падают от попытки замены 22 000 совпадений.
Но зачем? Зачем мне руками делать работу, которую может выполнить система сборки?
Написать одно слово стало менее удобно чем руками распиливать файлы пытаясь соблюсти circular reference и разгребая зависимости?
Вот только по факту даже просто подключив stdio вы уже получаете половину stl просто потому что stdio подключает их сам.
FloorZ
Яркий пример, шаблонная библиотека nlohmann::json, будет по лучше rapidJSON, так еще по всем stl правилам.
Там есть вариант поставки в виде одного .h файла. Например та же Visual Studio и QTCreator последних версий, если ты подключаешь именно цельный файл, начинаешь писать что-то и например хочешь глянуть оглавление какого то вспомогательного класса. Пытаешься перейти на оглавление функции и БАЦ, IDE думает очень долго, т.к. парситься чудовищного размера h файл. Так же иногда отказывается автоматическое дописывания функций, т.к. долго парсит один файл.
Хотя если взять нормальные заголовки и подключать их — и ситаксис подсвечивается с ходу и находит функции автоматически.
Ну не в Notepad++ же пишешь? Ну в vim, но даже вим с плагинами умеет находить все что угодно.
Это абсолютно не продуктивно писать в среде, которая автоматически не может найти нужные константы и подсветить синтаксис.
Т.е. писать руками 100500 флагов в файле на 3000+ строк заголовка, а после в систему сборки встраивать костыли, что бы он парсил файл и выдергивал из него нужные куски в отдельный файл — это нормально? Ну если честно, я считаю это не рациональным.
А в дальнейшем пихать все в один .h файл, с ростом проекта — откровенная дикость, времен нулевых.
Ну это уже сишные либы. На и не вижу я там, что бы cstdio и stdio.h подключали пол stl. Точнее я вообще не вижу там ничего, кроме пары инклудов, связанные уже с текущей платформой, что бы получить дискриптор ввода-вывода у системы.
staticmain
На удаленном сервере может быть что угодно, начиная от nano и заканчивая vi.
А писать в среде, от которой даже современные ноутбуки сгорают от перегрева — нормально? От которой приходит ООМ и начинает крошить все, до чего дотянется — нормально?
Писать руками одно слово напротив объекта, который нужно экспортировать в любом файле (не в одном на 3000. Внешний хэдер генерируется сам).
И? Чем они отличаются от любых других? Их точно так же парсит среда и пытается подставить автодополнением что угодно.
FloorZ
Нормальная IDE. Более того, если VS не тянет — бери QtCreator. Обе они поддерживают удаленную отладку! Можно взять VSCode, жрет не много, умеет тоже в удаленную отладку и компиляцию.
На сервере подними gdb-server и все. Все среды умеют в удаленную компиляцию и удаленную отладку. Чего заниматься таким геморроем и писать в nano?)
И я не вижу в ней пол STL. Тот же stdio.h меньше 3к строк. А с динамической линковкой и _NO_CRT_STDIO_INLINE, код в несколько раз уменьшается.
Я никогда не изучал, каким образом анализаторы синтаксиса парсят файлы. Но подозреваю, что данные хранятся в виде дерева, сопоставленного с файлами. Потому что обьяснить, почему от больших файлов, особенно когда их размер под 10к глючит анализатор — обьяснить не могу.
staticmain
Вы теоретик. Видимо на настоящих боевых headless серверах вы никогда не работали. Которые находятся за 2-3 NAT сетями, для которых чтобы туда зайти надо поочередно авторизовываться на 2-3 промежуточных узлах.
Простой пример куска автоматической тулзы которая облегчает мне половину работы и авторизуется на узле второго хопа (бывают и глубже):
Доустанавливать что-то на таких серверах зачастую невозможно из-за политик безопасности и стабильности. Что-то торчащее наружу — уязвимость. Что-то лишнее на сервере — угроза стабильности работы.
FloorZ
Один, десять натов. И что здесь такого? Через ssh выгрузил, скомпилировал, готово… Зачем удаленно что-то дописывать? Как это мешает использовать нормальную среду и деплить через свой сценарий или сценарий любой из сред?
И это не отменяет того факта, что хреначить все в один заголовочный файл — плохой тон. А если на предприятии вы пишите код через nano — так это проблема предприятия, которое не заботит производительность, качество и скорость разработки.
mapron
Согласен с комментатором выше — наличие тулчейна на боевом сервере — запрещено многими политиками.
Я молчу уже про всякие ембедед промышленные штуки где может быть тупо 64 мб памяти на все про все, и линкер там тупо не запустится =)
staticmain
Вы никогда не работал с такими серверами. Зайти туда можно по ключу через jump-сервер. А прокинуть порт можно только имея пароль пользователя на удаленном сервере. Который никому просто так не дается (через jump-сервер он не нужен). Поэтому просто так извне по ssh туда не зайти.
Это отладка, а не разработка. И представьте себе, это компромисс между «у нас дыры везде» как везде в россии, и «запретить все».