Введение


В данной статье рассмотрено использование системы сборки 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, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:



Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для "не-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)


  1. staticmain
    04.12.2018 22:39

    А как экспортировать header-файл для библиотеки? Чтобы руками не составлять header для внутреннего использования и header для использования в другом проекте. В интернете есть ссылка на generate_export_header, вот только везде все тупо копируют один пример из родной доки на экспорт класса, а на функции\enum\define\const примеров нет нигде и заставить это работать не выходит.


    1. Wilk
      04.12.2018 23:27
      -1

      Здравствуйте!

      Экспорт функции:

      MY_LIBRARY_EXPORT bool MyFunction ();
      


      Что Вы имеете в виду, говоря про экспорт «enum\define\const»?


      1. staticmain
        05.12.2018 00:13

        Что Вы имеете в виду, говоря про экспорт «enum\define\const»?


        Буквально. Вот пример части файла, который генерирует моя система сборки на основе маркеров в исходном коде:
        /* Exported from ./hashes/bxisha1.h */
        #define SHA1_SIZE (20)
        
        #define SHA1_STEP_COUNT (5)
        
        typedef struct
        {
            u32     leng;
            u32     totl[2];
        
            u32     h[SHA1_STEP_COUNT];
            u32     w[16];
            hash_20 hash;
        } sha1_t;
        
        void sha1_init     (sha1_t * sha1);
        void sha1_append   (sha1_t * sha1, const u8 * data, u32 len);
        void sha1_appendstr(sha1_t * sha1, const char * str);
        void sha1_appendi8 (sha1_t * sha1, i8 num);
        void sha1_appendu8 (sha1_t * sha1, u8 num);
        void sha1_appendi16(sha1_t * sha1, i16 num);
        void sha1_appendu16(sha1_t * sha1, u16 num);
        void sha1_appendi32(sha1_t * sha1, i32 num);
        void sha1_appendu32(sha1_t * sha1, u32 num);
        void sha1_final    (sha1_t * sha1);
        void sha12str      (sha1_t * sha1, char * out);
        
        /* Exported from ./hashes/bxicrypt.h */
        BXI_USES_MEM void bxi_crypt  (const char *    string, const char * salt,
                                             const char * key, char * out); /* @test */
        BXI_USES_MEM void bxi_decrypt(const char * hexstring, const char * salt,
                                             const char * key, char * out); /* @test */
        
        /* Exported from ./types/bxibools.h */
        typedef u8   b8;
        typedef u16  b16;
        typedef u32  b32;
        #define BITS_IN_B8   ( 8)
        #define BITS_IN_B16  (16)
        #define BITS_IN_B32  (32)


        1. Wilk
          05.12.2018 00:28

          Возможно я что-то упускаю, но для того, чтобы использовать макроопределения библиотеки в пользовательском коде экспортировать их не надо, также как псевдонимы типов (typedef), определения POD типов (C struct) и перечисления (enum).

          Я не пользовался возможность экспорта констант (const int kMyConstatn, например), т.к. мне это кажется не слишком правильным, но можно попробовать какой-то из вариантов, описанных здесь с поправкой на использование MY_LIBRARY_EXPORT макроса вместо __declspec(dllexport).


          1. staticmain
            05.12.2018 18:54

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

            Ничего не понятно. Как пользовательский компилятор поймет, что map_t это структура с 50 полями определенных типов?

            т.к. мне это кажется не слишком правильным

            В каком смысле? А константы для правильного использования библиотеки? А коэффициенты для функций, которые она предоставляет? Да возьмите любую библиотеку для работы с математикой, там различных констант типа
            long double PI_DIV_ROOT_2 = 2,2203152939768277384782365732759875932

            Будет с полсотни. А физические движки? А расчетные библиотеки? Да банально библиотеки работы с XML уже предоставляют константы, обозначающие тип узла.


            1. Wilk
              05.12.2018 20:35

              Ничего не понятно. Как пользовательский компилятор поймет, что map_t это структура с 50 полями определенных типов?


              Какое отношение макроопределение имеет к некоему map_t? А именно макроопределения и создаются при помощи define. Если вы говорите про псевдонимы типов вида
              typedef struct map {
                  // ...
              } map_t;
              

              то вопрос в том, нужно ли компилятору пользователя знать о структуре объектов типа map_t, т.е. является ли полное определение типа часть интерфейса библиотеки. Если является, то определение типа должно находится в интерфейсном заголовочном файле и быть доступно компилятору при компиляции пользовательского кода. Если не является, т.е. объекты типа используются исключительно через указатели, передаваемые на вход функциям библиотеки, то необходимости в предоставлении пользователю определения типа нет, и можно в интерфейсных заголовочных файлах использовать
              typedef struct map map_t;
              

              Внутри библиотеки при этом можно использовать приватный заголовочный файл, которые не входит в интерфейс библиотеки и не поставляется пользователю, Этот файл включает в себя интерфейсный файл, в котором объявлен псевдоним, после чего содержит определение типа struct map. Такой подход, если он используется, позволяет создавать обратно совместимые библиотеки, в которых вся информация об устройстве типов содержится исключительно внутри библиотеки, и которые не требуют изменения или перекомпиляции пользовательского кода при изменении используемых структур данных. Разумеется, всё это при условии, что сигнатуры функций не изменяются.

              В каком смысле? А константы для правильного использования библиотеки?

              enum?

              А коэффициенты для функций, которые она предоставляет? Да возьмите любую библиотеку для работы с математикой, там различных констант типа
              long double PI_DIV_ROOT_2 = 2,2203152939768277384782365732759875932

              Будет с полсотни.


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

              Да банально библиотеки работы с XML уже предоставляют константы, обозначающие тип узла.

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

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


              1. staticmain
                05.12.2018 22:21

                об одном и том же мы говорим или нет

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

                Какие-то из этих объектов предназначены только для того, чтобы быть использованными внутри библиотеки (например для реализации межпотокового хранилища объектов). Какие-то же наоборот, должен использовать пользователь для того, чтобы передать какие-то параметры в функции и\или иметь контроль над внутренним состоянием библиотеки.

                Именно эти внешние объекты (функции, макроопределения, константы, перечисления, структуры, объединения и прочие типы данных) должны быть во внешнем заголовочном файле, который будет подключать пользователь. Испокон веков кто-то создавал руками два файла, дублируя код и имея вероятность забыть исправить какой-то тип; кто-то (например как поступил я), написал свою систему сборки, в которой нужный для внешнего header файла объект помечается слово EXPORT и он автоматически оказывается в генерируемом файле. Судя по всему cmake обещает подобную функциональность. Как ее использовать?


                1. Wilk
                  05.12.2018 22:57

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

                  Испокон веков кто-то создавал руками два файла, дублируя код и имея вероятность забыть исправить какой-то тип; кто-то (например как поступил я), написал свою систему сборки, в которой нужный для внешнего header файла объект помечается слово EXPORT и он автоматически оказывается в генерируемом файле.


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

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

                  В случае GCC всё просто, т.к. по умолчанию он экспортирует вообще все символы из библиотек.

                  В случае MSVC всё не так просто, т.к. его поведение по умолчанию прямо противоположное. Для того, чтобы экспортировать символ из библиотеки, компилируемой MSVC, необходимо при компиляции объявить его с аттрибутом (?) __declspec(dllexport), а при использовании библиотеки с аттрибутом __declspec(dllimport).

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

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

                  Видимо да, это было непонимание, т.к. у нас с Вами разные определения «экспорта».


                  1. staticmain
                    05.12.2018 23:01

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


                    Окей, где взять пример экспорта чего-то кроме объекта? С ходу завести это не получилось, файл просто не генерируется. В сети просто перепечатка с оф.мануала где есть только пример экспорта объекта.

                    Для того, чтобы символы попали в файл компановщика, они должны быть экспортированы.


                    Да не причем тут компоновщик. Просто из одного хэдера скопировать определение (которое не попадет в PE/ELF) в другой, генерируемый.

                    Причин для дублирования кода я, честно говоря, не вижу.

                    Это не очень удобно, когда есть один публичный хэдер на 3000+ строк и аккуратно разбитые на субкаталоги и нейминги приватные. Это очень НЕ удобно искать, где объявлена константа, особенно когда A, B, D объявлены в привате, а C во внешнем.


                    1. Wilk
                      05.12.2018 23:26

                      Да не причем тут компоновщик.

                      При том, что генерируемый CMake файл предназначен именно для формирования библиотечных файлов, потребляемых компоновщиком.

                      Вот пример генерируемого заголовочного файла (сборка MSVC):
                      #ifndef UTK_QT_EXPORT_H
                      #define UTK_QT_EXPORT_H
                      
                      #ifdef UTK_QT_STATIC_DEFINE
                      #  define UTK_QT_EXPORT
                      #  define UTK_QT_NO_EXPORT
                      #else
                      #  ifndef UTK_QT_EXPORT
                      #    ifdef utk_qt_BUILD
                              /* We are building this library */
                      #      define UTK_QT_EXPORT __declspec(dllexport)
                      #    else
                              /* We are using this library */
                      #      define UTK_QT_EXPORT __declspec(dllimport)
                      #    endif
                      #  endif
                      
                      #  ifndef UTK_QT_NO_EXPORT
                      #    define UTK_QT_NO_EXPORT 
                      #  endif
                      #endif
                      
                      #ifndef UTK_QT_DEPRECATED
                      #  define UTK_QT_DEPRECATED __declspec(deprecated)
                      #endif
                      
                      #ifndef UTK_QT_DEPRECATED_EXPORT
                      #  define UTK_QT_DEPRECATED_EXPORT UTK_QT_EXPORT UTK_QT_DEPRECATED
                      #endif
                      
                      #ifndef UTK_QT_DEPRECATED_NO_EXPORT
                      #  define UTK_QT_DEPRECATED_NO_EXPORT UTK_QT_NO_EXPORT UTK_QT_DEPRECATED
                      #endif
                      
                      #if 0 /* DEFINE_NO_DEPRECATED */
                      #  ifndef UTK_QT_NO_DEPRECATED
                      #    define UTK_QT_NO_DEPRECATED
                      #  endif
                      #endif
                      
                      #endif /* UTK_QT_EXPORT_H */
                      


                      Вот пример с экспортом нескольких функций:
                      #ifndef INCLUDE_UTK_QT_SQL_QUERY_HPP
                      #define INCLUDE_UTK_QT_SQL_QUERY_HPP
                      
                      
                      #include <tuple>
                      
                      #include <QSqlQuery>
                      #include <QString>
                      #include <QVariant>
                      
                      #include "utk/qt/utk_qt_export.h"
                      
                      
                      namespace utk {
                      	namespace qt {
                      		inline namespace v1 {
                      			UTK_QT_EXPORT std::tuple< QString, QStringList >
                      			    sqlQueryInsertListPlaceholder (
                      			        QString i_query_template,
                      			        unsigned int i_list_element_count,
                      			        QString i_list_placeholder,
                      			        QString i_list_element_prefix);
                      
                      			UTK_QT_EXPORT std::tuple< QString, QVariantMap >
                      			    sqlQueryInsertListPlaceholder (
                      			        QString i_query_template,
                      			        const QMap< QString, QPair< QVariantList, QString > >&
                      			            i_replacement_parameters);
                      
                      			UTK_QT_EXPORT void sqlQueryBindValues (
                      			    const QVariantMap& i_values, QSqlQuery& io_query);
                      
                      			UTK_QT_EXPORT QVariantList
                      			    unpackSqlQueryArrayValue (QString i_packed_list_value);
                      		}
                      	}
                      }
                      
                      
                      #endif /* INCLUDE_UTK_QT_SQL_QUERY_HPP */
                      


                      Это не очень удобно, когда есть один публичный хэдер на 3000+ строк и аккуратно разбитые на субкаталоги и нейминги приватные.

                      Собственно, я стараюсь не делать файлы на 3000+ строк. Что мешает также аккуратно разбить публичный файл на отдельные, логически завершённые файлы, разложить их по отдельным папкам, а потом создать один общий заголовочный файл (если он насколько нужен) включающий все отдельные заголовочные файлы. На мой взгляд, и порядка больше, и дублировать ничего не надо. Хотя мой опыт, в силу ничтожности, может быть не показателен.


                      1. mapron
                        06.12.2018 13:37

                        Я внимательно следил за всей вашей веткой, решил все же высказаться.

                        Какие-то из этих объектов предназначены только для того, чтобы быть использованными внутри библиотеки (например для реализации межпотокового хранилища объектов). Какие-то же наоборот, должен использовать пользователь для того, чтобы передать какие-то параметры в функции и\или иметь контроль над внутренним состоянием библиотеки.

                        Именно эти внешние объекты (функции, макроопределения, константы, перечисления, структуры, объединения и прочие типы данных) должны быть во внешнем заголовочном файле, который будет подключать пользователь. Испокон веков кто-то создавал руками два файла, дублируя код и имея вероятность забыть исправить какой-то тип; кто-то (например как поступил я), написал свою систему сборки, в которой нужный для внешнего header файла объект помечается слово EXPORT и он автоматически оказывается в генерируемом файле. Судя по всему cmake обещает подобную функциональность. Как ее использовать?


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

                        Далее, если одна и та же функция должна быть скомпилирована по-разному в зависимости от ее использования (как в случае динамического экспорта в VC), то для таких нужд необходимые символы отмечаются макросом. обычно это что-то вроде LIBNAME_EXPORT или LIBNAME_API.
                        Определение этого макроса можно помещать как в этом же публичном хидере, ручками
                        // я про вот такие блоки
                        # ifdef utk_qt_BUILD
                        /* We are building this library */
                        # define UTK_QT_EXPORT __declspec(dllexport)
                        # else
                        /* We are using this library */
                        # define UTK_QT_EXPORT __declspec(dllimport)
                        # endif
                        # endif


                        , либо вынести в генерируемый export_some_lib.h, который создавать на основе имени библиотеки. cmake автоматически создаст нужный дефайн для того чтобы предварять имена символов, а так же дефайн, который нужно будет передавать в качестве определения при сборки цели библиотеки (cmake это тоже может сделать автоматически).

                        Не вижу ни одного реального кейса чтобы «копировать определения из одного хидера в другой».

                        если нужно после установки придать другие имена хидерам, другое дело — это делается как в libc++
                        github.com/llvm-mirror/libcxx/blob/master/include/CMakeLists.txt
                        так и в Qt, например.
                        Само содержимое хедеров не меняется.

                        про публичный хидер на 3000 строк — ну и заинклюдить в нем все необходимые подфайлы, делов-то)


                        1. staticmain
                          06.12.2018 13:57

                          Не вижу ни одного реального кейса чтобы «копировать определения из одного хидера в другой».


                          Когда не все коды для какой-либо функции должен использовать пользователь. Такое было уже несколько раз. Примерно так
                          const u32 func_code_select = (1);
                          const u32 func_code_insert = (2); // Не должна быть в пользовательском header
                          const u32 func_code_update = (3); // Не должна быть в пользовательском header
                          const u32 func_code_delete = (4);
                          


                          Таким образом одно семантически сгруппированное объявление оказывается «разорвано» в процессе разработки. При раздельных header будет такая ситуация:
                          header_private.h
                          const u32 func_code_insert = (2);
                          const u32 func_code_update = (3);
                          

                          header_public.h
                          const u32 func_code_select = (1);
                          const u32 func_code_delete = (4);
                          


                          Открыв в редакторе файл header_private.h можно подумать, что это все доступные константы и принять неверное решение.

                          Когда public генерируется ситуация иная:
                          EXPORT const u32 func_code_select = (1);
                                 const u32 func_code_insert = (2); 
                                 const u32 func_code_update = (3);
                          EXPORT const u32 func_code_delete = (4);
                          


                          Все возможные константы семантического блока можно увидеть сразу.


                          1. mapron
                            06.12.2018 15:21

                            Когда public генерируется ситуация иная:

                            Что-то не понял, приведенный фрагмент кода не нужно генерировать, это ж объявление которое вы можете использовать и снаружи, и внутри. В чем проблема?

                            Для enum можно использовать блок #ifdef INTERNAL_COMPILATION для определения значений для внутреннего пользования.

                            Но в любом случае, у меня ощущение какого-то глобального просчета в проектировании интерфейса, ибо через одну «дырку», которую видит пользователь не должны ходить и публичные значения, и какие-то секретные.
                            Для меня это звучит как примерно «если в функцию fopen в качестве mode передать „пыщь-пыщь“, то будет удалена вся файловая система».
                            В хорошем публичном интерфейсе не должно быть никаких скрытых параметров.

                            Но повторюсь, если волей «исторически сложилось» такие параметры нужны — они заключаются в ifdef блоки.
                            dllimport — да, через макрос. Если нужна поддержка win.


        1. FloorZ
          06.12.2018 03:54

          А зачем такой геморрой со сборкой?.. Я почитал ветку комментариев и не вдупляю одного. Зачем генерировать что-то?

          Есть у нас хедеры с экспортируемым функционалом? — так сделать же можно под них просто отдельный .h и все. А приватный функционал — в другой .h.

          Типо того:

          //public.h
          #ifndef __PUBLIC_H__
          #define __PUBLIC_H_
          extern "C" {
             struct A {...};
             void foo();
          };
          ...
          

          //private.h
          #ifndef __PRIVATE_H__
          #define __PRIVATE_H_
          #include "public.h"
          /*но я бы не делал длинные цепочки include*/
          void foo2();
          ...
          


          //source.cpp
          #include "private.h"
          /*или лучше так, что бы не создавать сложные цепочки 
          #include "private.h"
          #include "public.h"
          */
          ...
          


          И экспортировать только те хэдеры, в которых публичный функционал.


          1. staticmain
            06.12.2018 07:27

            Это не очень удобно, когда есть один публичный хэдер на 3000+ строк и аккуратно разбитые на субкаталоги и нейминги приватные. Это очень НЕ удобно искать, где объявлена константа, особенно когда A, B, D объявлены в привате, а C во внешнем.


            1. FloorZ
              06.12.2018 08:57

              А зачем раздувать публичный хедер на 3000+ строк? В чем цель? Что бы IDE у пользователей по два часа его парсил на поиск констант, функций и прочее?
              Не проще ли разбить его на много мелких хедеров по функциональности. Что бы подключать только тот заголовок, функции которого ты будешь юзать.

              Что именно не удобно искать? Вроде бы сейчас любая IDE и любой вменяемый текстовый редактор типо Атом или VSCode умеет подсвечивать все что угодно. Зажал контрл — он тебе показал константу, в каком хедере лежит и сразу открыл в доп-вкладке.

              Если есть константы, которые очень прям важные такие и часто на них, по какой то не ведомой мне причине надо смотреть — выдели их в отдельный хедер или классифицируй по разным файлам, что бы по смыслу было понятно, что константы буффера например лежат в buffer.h, а константы например потокового менеджера в thread_manager.h и т.д.

              Ты же не подключаешь весь stl разом? А подключаешь только то, что используешь и по мере надобности.


              1. staticmain
                06.12.2018 09:04

                Что бы IDE у пользователей по два часа его парсил на поиск констант, функций и прочее?


                Если ваша IDE загибается от 3000 строк, то как она парсит stl?

                Вроде бы сейчас любая IDE и любой вменяемый текстовый редактор типо Атом или VSCode умеет подсвечивать все что угодно.

                Не все программируют в Atom или VSCode. Существуют и другие редакторы, которые не едят по 4 GB RAM и не падают от попытки замены 22 000 совпадений.

                выдели их в отдельный хедер или классифицируй по разным файлам, что бы по смыслу было понятно, что константы буффера например лежат в buffer.h, а константы например потокового менеджера в thread_manager.h и т.д.


                Но зачем? Зачем мне руками делать работу, которую может выполнить система сборки?

                EXPORT_FROM
                #define CML_FIND_DELIMETER   ('.')
                #define CML_FIND_LONGESTPATH (1024)
                EXPORT_TO
                
                void path_curr(const char * path, char * dest, char delimeter);
                void path_next(const char * path, char * dest, char delimeter);
                
                EXPORT CML_Node * cml_find    (CML_Node * node, const char * path, u32 type);
                EXPORT CML_Node * cml_find_del(CML_Node * node, const char * path, u32 type, char delimeter);
                EXPORT CML_Node * cml_find_del_i32(CML_Node * node, const char * path, i32 data, char delimeter);


                Написать одно слово стало менее удобно чем руками распиливать файлы пытаясь соблюсти circular reference и разгребая зависимости?

                Ты же не подключаешь весь stl разом? А подключаешь только то, что используешь и по мере надобности.

                Вот только по факту даже просто подключив stdio вы уже получаете половину stl просто потому что stdio подключает их сам.


                1. FloorZ
                  06.12.2018 09:27

                  Если ваша IDE загибается от 3000 строк, то как она парсит stl?

                  Яркий пример, шаблонная библиотека nlohmann::json, будет по лучше rapidJSON, так еще по всем stl правилам.
                  Там есть вариант поставки в виде одного .h файла. Например та же Visual Studio и QTCreator последних версий, если ты подключаешь именно цельный файл, начинаешь писать что-то и например хочешь глянуть оглавление какого то вспомогательного класса. Пытаешься перейти на оглавление функции и БАЦ, IDE думает очень долго, т.к. парситься чудовищного размера h файл. Так же иногда отказывается автоматическое дописывания функций, т.к. долго парсит один файл.
                  Хотя если взять нормальные заголовки и подключать их — и ситаксис подсвечивается с ходу и находит функции автоматически.

                  Не все программируют в Atom или VSCode. Существуют и другие редакторы, которые не едят по 4 GB RAM и не падают от попытки замены 22 000 совпадений.

                  Ну не в Notepad++ же пишешь? Ну в vim, но даже вим с плагинами умеет находить все что угодно.
                  Это абсолютно не продуктивно писать в среде, которая автоматически не может найти нужные константы и подсветить синтаксис.

                  Но зачем? Зачем мне руками делать работу, которую может выполнить система сборки?

                  Т.е. писать руками 100500 флагов в файле на 3000+ строк заголовка, а после в систему сборки встраивать костыли, что бы он парсил файл и выдергивал из него нужные куски в отдельный файл — это нормально? Ну если честно, я считаю это не рациональным.
                  А в дальнейшем пихать все в один .h файл, с ростом проекта — откровенная дикость, времен нулевых.

                  Вот только по факту даже просто подключив stdio вы уже получаете половину stl просто потому что stdio подключает их сам.

                  Ну это уже сишные либы. На и не вижу я там, что бы cstdio и stdio.h подключали пол stl. Точнее я вообще не вижу там ничего, кроме пары инклудов, связанные уже с текущей платформой, что бы получить дискриптор ввода-вывода у системы.


                  1. staticmain
                    06.12.2018 09:31

                    Ну не в Notepad++ же пишешь? Ну в vim, но даже вим с плагинами умеет находить все что угодно.
                    Это абсолютно не продуктивно писать в среде, которая автоматически не может найти нужные константы и подсветить синтаксис.


                    На удаленном сервере может быть что угодно, начиная от nano и заканчивая vi.

                    А писать в среде, от которой даже современные ноутбуки сгорают от перегрева — нормально? От которой приходит ООМ и начинает крошить все, до чего дотянется — нормально?

                    Т.е. писать руками 100500 флагов в файле на 3000+ строк заголовка,

                    Писать руками одно слово напротив объекта, который нужно экспортировать в любом файле (не в одном на 3000. Внешний хэдер генерируется сам).

                    Ну это уже сишные либы.

                    И? Чем они отличаются от любых других? Их точно так же парсит среда и пытается подставить автодополнением что угодно.


                    1. FloorZ
                      06.12.2018 09:45

                      На удаленном сервере может быть что угодно, начиная от nano и заканчивая vi.

                      А писать в среде, от которой даже современные ноутбуки сгорают от перегрева — нормально? От которой приходит ООМ и начинает крошить все, до чего дотянется — нормально?

                      Нормальная IDE. Более того, если VS не тянет — бери QtCreator. Обе они поддерживают удаленную отладку! Можно взять VSCode, жрет не много, умеет тоже в удаленную отладку и компиляцию.
                      На сервере подними gdb-server и все. Все среды умеют в удаленную компиляцию и удаленную отладку. Чего заниматься таким геморроем и писать в nano?)
                      И? Чем они отличаются от любых других? Их точно так же парсит среда и пытается подставить автодополнением что угодно.

                      И я не вижу в ней пол STL. Тот же stdio.h меньше 3к строк. А с динамической линковкой и _NO_CRT_STDIO_INLINE, код в несколько раз уменьшается.

                      Я никогда не изучал, каким образом анализаторы синтаксиса парсят файлы. Но подозреваю, что данные хранятся в виде дерева, сопоставленного с файлами. Потому что обьяснить, почему от больших файлов, особенно когда их размер под 10к глючит анализатор — обьяснить не могу.


                      1. staticmain
                        06.12.2018 09:55

                        Обе они поддерживают удаленную отладку! Можно взять VSCode, жрет не много, умеет тоже в удаленную отладку и компиляцию.
                        На сервере подними gdb-server и все. Все среды умеют в удаленную компиляцию и удаленную отладку. Чего заниматься таким геморроем и писать в nano?)

                        Вы теоретик. Видимо на настоящих боевых headless серверах вы никогда не работали. Которые находятся за 2-3 NAT сетями, для которых чтобы туда зайти надо поочередно авторизовываться на 2-3 промежуточных узлах.

                        Простой пример куска автоматической тулзы которая облегчает мне половину работы и авторизуется на узле второго хопа (бывают и глубже):
                            terminal_open3 "expect -c 'spawn /usr/bin/ssh -o StrictHostKeyChecking=no <XXXXXX>; "`
                                                     `"expect \\\"<YYY>\\\"; "`
                                                     `"send \\\"ssh $ip\n\\\"; "`
                                                     `"send \\\"cd /tmp/ygg-$DATETIME && ./scripts/build.sh debug && cd bin && export LD_LIBRARY_PATH=.\n\\\";"`
                                                     `"interact;'"                    "expect -c 'spawn /usr/bin/ssh -o StrictHostKeyChecking=no <XXXXXX>; "`
                                                     `"expect \\\"<YYY>\\\"; "`
                                                     `"send \\\"ssh $ip\n\\\"; "`
                                                     `"send \\\"cd /tk/dd24/log/ctms && tail -f hall_*.log\n\\\";"`
                                                     `"interact;'"                    "expect -c 'spawn /usr/bin/ssh -o StrictHostKeyChecking=no <XXXXXX>; "`
                                                     `"expect \\\"<YYY>\\\"; "`
                                                     `"send \\\"ssh $ip\n\\\"; "`
                                                     `"send \\\"cda && bin/mysql\n\\\";"`
                                                     `"interact;'"


                        Доустанавливать что-то на таких серверах зачастую невозможно из-за политик безопасности и стабильности. Что-то торчащее наружу — уязвимость. Что-то лишнее на сервере — угроза стабильности работы.


                        1. FloorZ
                          06.12.2018 10:11

                          Вы теоретик. Видимо на настоящих боевых headless серверах вы никогда не работали. Которые находятся за 2-3 NAT сетями, для которых чтобы туда зайти надо поочередно авторизовываться на 2-3 промежуточных узлах.


                          Один, десять натов. И что здесь такого? Через ssh выгрузил, скомпилировал, готово… Зачем удаленно что-то дописывать? Как это мешает использовать нормальную среду и деплить через свой сценарий или сценарий любой из сред?

                          И это не отменяет того факта, что хреначить все в один заголовочный файл — плохой тон. А если на предприятии вы пишите код через nano — так это проблема предприятия, которое не заботит производительность, качество и скорость разработки.


                          1. mapron
                            06.12.2018 13:40

                            Согласен с комментатором выше — наличие тулчейна на боевом сервере — запрещено многими политиками.
                            Я молчу уже про всякие ембедед промышленные штуки где может быть тупо 64 мб памяти на все про все, и линкер там тупо не запустится =)


                          1. staticmain
                            06.12.2018 13:49

                            Один, десять натов. И что здесь такого? Через ssh выгрузил, скомпилировал, готово…

                            Вы никогда не работал с такими серверами. Зайти туда можно по ключу через jump-сервер. А прокинуть порт можно только имея пароль пользователя на удаленном сервере. Который никому просто так не дается (через jump-сервер он не нужен). Поэтому просто так извне по ssh туда не зайти.

                            А если на предприятии вы пишите код через nano — так это проблема предприятия, которое не заботит производительность, качество и скорость разработки.

                            Это отладка, а не разработка. И представьте себе, это компромисс между «у нас дыры везде» как везде в россии, и «запретить все».


  1. Wilk
    05.12.2018 00:22

    del


  1. mapron
    05.12.2018 02:29

    cmake_minimum_required(VERSION 3.0)

    Данный код, помимо проверки версии, что немаловажно, еще и выставляет все флаги совместимости (cmake policy) в соответствие с этой версией.
    Можно выставить низкую версию, и нужное поведение, сломавшее обратную совместимость после ее выхода докрутить ручками. Так делают разработчики библиотек, которые хотят поддерживать древние версии cmake, но при этом и новые фичи местами использовать.


    1. Gymmasssorla Автор
      05.12.2018 15:34

      Добавлено в раздел «Проверка версии CMake».


  1. mapron
    05.12.2018 02:31

    project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
    У кого-то может возникнуть вопрос — а нафига может понадобиться вообще версию там указывать?
    Это может использоваться в двух вещах:
    -автоматическое версионирование so/dylib на unix-платформах (симлинки, все дела)
    -интеграция с CPack и выставление версии продукта там


    1. 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 эту версию собранную из файла проекта.


      1. Wilk
        05.12.2018 20:41

        Здравствуйте!

        Я для себя нашёл обратный способ — генерация функций для возврата информации о версии на основании описания проекта CMake. Фантазия у меня разыгралась, поэтому в результате генерируется несколько функций получения номера версии, в т.ч. с идентификатором коммита git, информацией о состоянии репозитория, из которого произведена сборка, числом коммитов с последнего тега. В последней версии добавилась также возможность вывода информации о лицензии, разработчиках и примечаний (NOTICE). Всё это генерируется в купе с ресурсами для MSVS, чтобы в проводнике можно было посмотреть версию (в упрощённом виде, разумеется) и информацию о разработчике.


        1. mapron
          05.12.2018 20:42

          Про последнее — поддерживаю, написать один раз application.rc.in, туда всю метаинформацию упрятать и использовать шаблон для разных приложений (если в продукте оно не одно)


          1. Wilk
            05.12.2018 20:52

            Здравствуйте!

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


            1. mapron
              05.12.2018 21:03

              Посмотрел. Вопросы, конечно, возникли, но раз вы извинились, то озвучивать ничего не буду)
              В целом есть ощущение что для CMake не хватает своего подобия boost или GSL — все лепят свои велосипеды (у KDE тоже есть свое подобие utk). Хорошо бы заморочиться сделать единый стиль и единые гайдлайны, которые покроют потребности большинства)


              1. Wilk
                05.12.2018 21:08

                С удовольствием приму конструктивную критику)


      1. 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 кода, чтобы не дублировать константы у себя).


        1. Diversus
          06.12.2018 10:00

          Интересный подход… Это получается, надо при изменении версии запустить сборку CMake, она в свою очередь вызовет изменение файла version.h.
          Кхм, даже не знал, что так можно… Спасибо mapron


  1. 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 ]
    )
    

    Квадратные скобки это вообще удивительная магия, про них можно отдельную статью писать)


    1. Gymmasssorla Автор
      06.12.2018 00:37

      Механизм разбора аргументов добавлен в раздел «Разбор аргументов» предыдущей статьи (т.к. это момент синтаксиса). Спасибо за участие!


  1. mapron
    05.12.2018 02:52

    Цели в cmake — потрясающая вещь. Это с одной стороны, глобальная переменная, которая при этом еще и объект) К сожалению, из-за примитивного синтаксиса, так просто их методы не подергать, приходится довольствоваться get_target_property/set_target_properties. Одно из замечательных свойств то, что можно для цели выставлять свои значения, не определенные стандартом (но и по понятным причинам без особой надобности это лучше не делать — огребешь проблем с совместимостью).

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

    уместным тут будет упомянуть функцию if (TARGET smth), которая позволяет проверить что цель уже определена (если например, вы хотите реализовать жесткий порядок определения зависимостей в своей DSL-функции)


    1. Gymmasssorla Автор
      05.12.2018 18:52

      Добавлен раздел «Работа с целями». Про возможность проверки определения цели сам не знал)


  1. ksergey01
    05.12.2018 10:32

    По моему эта строчка лишняя, т.к. в зависимостях есть GSL::gsl (target_link_libraries(...))

    include_directories(${GSL_INCLUDE_DIRS})


    1. Gymmasssorla Автор
      05.12.2018 15:25

      В любом случае, строчку с include_directories я оставлю, так как не знаю где ещё в руководстве можно рассказать про эту часто используемую команду)


      1. ksergey01
        05.12.2018 15:30

        Лучше расскажите про target_include_directories(...) и чем она отличается от include_directories


        1. Gymmasssorla Автор
          05.12.2018 16:12
          -1

          Рассказал в разделе «Способы включения заголовков».


      1. Wilk
        05.12.2018 15:54
        +1

        Здравствуйте!

        Стоит также упомянуть, что функции include_directories() и link_libraries() не рекомендованы для использования в современных проектах, т.к. работают на уровне дирректории, а не с целями. Рекомендованный вариант это использование target_include_directories() и target_link_libraries(), причём для указания зависимостей желательно использовать именно последнюю, т.к. современный CMake крутится вокруг целей, всё же, а не переменных. Конечно, очень много библиотек-староверов, не предоставляющих (полноценых) пакетов CMake, но это уже отдельная история, в которой надо либо внести соответствующее изменение в библиотеку, если разработчик принимает патчи, либо написать свой модуль, который добавляет необходимые импортированные цели.


        1. Gymmasssorla Автор
          05.12.2018 16:21
          -1

          Добавлено в раздел «Способы включения заголовков». Спасибо за полезное замечание!)


        1. DaylightIsBurning
          07.12.2018 01:39

          А есть какой-то ключ что бы cmake ругался на все устаревшие функции типа include_directories(), подобно -Wall/CoreGuidlenes?


          1. Wilk
            07.12.2018 01:57

            Здравствуйте!

            Насколько мне известно — нет. Самое большее, что можно получить, это предупреждения на тему различных устаревших политик, однако политики, насколько я понимаю, затрагивают лишь отдельные аспекты поведения CMake, но не делают какие-либо функции, особенно стандартные, устаревшими. Подобный вопрос не так давно поднимался в списке рассылки, и Craig Scott отметил, что тема предупреждений об устаревших/плохих практиках всё больше интересует сообщество, но на данный момент никаких подвижек в сторону реализации предупреждений нет.


            1. mapron
              07.12.2018 14:01

              Здравствуйте! (простите, не удержался)

              я бы не назвал include_directories устаревшей, она может быть использована в некоторых случаях: когда ты делаешь порт под какой-то тулчейн, в котором компилятор автоматически не знает про необходимые пути к стандартной библиотеке (ембедед gcc тулчейн, с несколькими возможным целями для разных девайсов — с хидрами вроде watchdog и тп).
              В таком случае include_directories в файле тулчейна вполне может использоваться.


  1. 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"


    1. 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 и радуетесь в жизни (инклюды прокинутся тоже).


  1. amarao
    05.12.2018 18:00

    wc -l /github/FreeCAD/CMakeLists.txt
    1186 /github/FreeCAD/CMakeLists.txt

    Больно.


    1. 0xd34df00d
      05.12.2018 19:20

      Да что вы знаете о боли.


      d34df00d@deadaven ~/Programming/someProjectOfMine/src (master) % find ./ -name CMakeLists.txt -exec cat '{}' \; | wc -l     
      10012


      1. amarao
        05.12.2018 19:21
        +1

        Ещё больнее. Я знаю, многие программисты (особенно, на Си/С++) привыкли к четырёхзначным номерам строк, но мне кажется, что если модуль такого размера, то его архитектуру кто-то плохо продумал. Кстати, а CMake поддерживает модулярность? 10к строк уже очень просятся на что-то вроде CMakeList.d/


        1. Gymmasssorla Автор
          05.12.2018 19:32

          CMake поддерживает модульность. Командой “include” можно исполнять скрипты на CMake, это было описано в разделе “Запуск скриптовых файлов”.


        1. 0xd34df00d
          05.12.2018 19:32

          Там не зря find. 185 файлов CMakeLists.txt суммарно.


          А, блин, это я ещё кастомные FindFoo.cmake не считал. Ну и ладно.


        1. mapron
          05.12.2018 20:49

          У нас примерно 10к cmake скриптов на 1M строк С++. мне кажется, это разумное соотношение. С учетом того что там делается много всего — работа с пакетами, бандлы, обжимка разных инсталляторов и их перепаковка и тп.


      1. DaylightIsBurning
        05.12.2018 20:01
        +1

        И это ужасно. Как бы хотелось, что бы зависимости прописывались единственной строкой типа «import boost/graph», что бы не нужен был весь этот cmake.


        1. Gymmasssorla Автор
          05.12.2018 20:49

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

          Было-бы неплохо стандартизировать поиск зависимостей, чтобы не писать свой велосипедо-Find каждый раз для каждой библиотеки, если это возможно…


  1. AMDev
    05.12.2018 18:13

    Было бы здорово разобрать прмер проекта для кросплатформенной сборки,
    например вот такой: https://github.com/cginternals/cmake-init
    Полезные ссылки по cmake: awesome-cmake


    1. Gymmasssorla Автор
      05.12.2018 18:53

      Добрый вечер. Да, идея действительно хорошая и достойна отдельной статьи. Возможно, четвёртая статья будет на эту тему)


  1. 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_grasshoper


    1. Wilk
      05.12.2018 20:47

      Здравствуйте!

      Попробуйте изменить тип библиотеки с MODULE на SHARED. Я не работал с модульными библиотеками, но они, насколько я могу судить по документации, не предназначены для использования в качестве зависимостей времени сборки. Их основное назначение это использование в связке с функциями динамической загрузки библиотек времени выполнения (dlopen и т.п.). Поэтому вполне может быть, что CMake считает, что надо компановать проект с dll, а не с lib.


      1. shukan
        05.12.2018 21:54

        SHARED помогло, спасибо!

        А я test_grasshoper долго мучал, а надо было add_library.


  1. 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 в виде единственного исполняемого файла.


    1. Gymmasssorla Автор
      06.12.2018 11:46

      Да, если такой код запустить с флагом "-DSTATIC=TRUE", то библиотека скомпилируется как статическая (т.к. переменная «LIBRARY_TYPE» затем обратится в пустую строку, a «add_library» по умолчанию компилирует статическую библиотеку).


      1. Usul
        06.12.2018 12:41

        Спасибо, все получилось!


        1. mapron
          06.12.2018 13:20

          Совет: лучше использовать общеупотребительный
          cmake.org/cmake/help/v3.0/variable/BUILD_SHARED_LIBS.html
          чтобы у других разработчиков было привычное понимание) Ну это если вы действительно хотите переключалку режима сборки.


          1. Usul
            07.12.2018 04:50

            Спасибо! Если придется делать собственный cmake-проект, обязательно так и поступлю.

            Вообще, главная сложность при изучении инструментария типа cmake — как раз такие вот «лучшие практики». Из справочной документации и обзорных статей их почерпнуть непросто. По некоторым тулзам есть специальные руководства и даже книги. Для git, например, есть Pro Git. Есть ли что-нибудь подобное по cmake?


            1. Gymmasssorla Автор
              07.12.2018 10:32

              • Книга "Mastering CMake", возможно там явно рассказывают про лучшие практики.
              • Статья про улучшение скриптов CMake, даны несколько полезных советов.
              • В документации тоже встречаются некоторые "наставления": тут рекомендуется применять "target_include_directories" вместо "include_directories".


              1. mapron
                07.12.2018 13:57

                Прочитал Mastering Cmake. Из того что узнал для себя — все-таки можно делать плагины, Loadable Commands называются — уж как я не пробовал искать в гугле подобное поведение! Надо будет поковыряться, что с ними можно вытворить (к слову, даже когда я в багтрекере cmake спрашивал про подобную функциональность, мне ничего не сказали).

                По поводу best practices — да вот хз. Есть некоторые хорошие советы, например разделы про портирование на новую платформу и кросскомпиляцию — они зачетные.
                А в остальном… тема взаимодействия кастомных целей и команд вообще слабо раскрыта.


  1. Ryppka
    06.12.2018 13:07

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

    Мне кажется, стоит упомянуть, что между требуемой версией и объявлением проекта может быть полезным добавить:
    • Включение файла локальных настроек для конкретной машины, т.к. расположение компиляторов, зависимостей и т.д. на разных машинах может отличаться
    • Дополнительные пути для поиска модулей CMake, т.к. они могут потребоваться уже при объявлении проекта
    • включить тулчейн-файл для кросс-компиляции. На мой вкус, это может быть удобнее, чем включать его через командную строку
    • И наконец иногда требуется просто задать какие-то дополнительные переменные


    1. mapron
      06.12.2018 13:22
      +1

      Да, например, под маком переменную CMAKE_OSX_SYSROOT нужно выставлять обязательно ДО команды project().
      но это все такие нюансы, с которыми может не каждый столкнуться, а пихать все-все в статью для новичков — только отпугнет их… нужно как-то придерживаться золотой середины между простотой и полнотой.