Рун не должен резать
Тот, кто в них не смыслит.
В непонятных знаках
Всякий может сбиться.
(Сага об Эгиле)

Многие начинающие программисты, уже освоив синтаксис C++, обнаруживают, что нет простого способа как подключить библиотеку, так и собрать программу для другой ОС, или чего хуже, под другим компьютером с той же ОС.
Это проблема отчасти порождается спорной практикой в обучении, когда код учат писать и запускать средствами IDE, таких как Visual Studio, Code::Blocks, и других. Поначалу такой подход работает, но лишь до первой реальной задачи сделать что-то, что запустится не только на вашем компьютере. Тут-то новички и сталкиваются с отсутствием стандартной системы сборки и менеджера зависимостей. После осознания этой сложности, большинство студентов, как правило, переходят на другие языки, попутно тиражируя в индустрии миф о том, что C++ де не кроссплатформенный язык, а вот мой Python/Java/C# - да. Между тем нас окружает множество замечательных кроссплатформенных программ написанных на C++. Если вы обучаетесь C++ и хотите понять как создавать такое ПО, то эта статья для вас.

1. Минутка теории. Получение исполняемого файла всегда и везде, зачем это необходимо?

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

У Visual Studio, к примеру, сборщиком по умолчанию является MSBuild, а у CLion - CMake. Те дополнительные файлы, которые вы видите, создав новый проект через интерфейс IDE, и являются конфигурацией для системы сборки. Наша задача просто научиться взаимодействовать с ней напрямую. Для этого мы возьмем CMake, потому что в отличие от MSBuild, эта система сборки запускается на большинстве ОС.

Почему надо разобраться в конфигурации системы сборки? Разве мне недостаточно среды, спросите вы? Потому что ваша среда может запуститься не везде, а система сборки гораздо более переносима, с ней вы сможете автоматизировать сборку, использовать контейнеры Docker, сможете оставить в репозитории билд скрипт который соберет ваш код на любом компьютере - ну разве не круто?

2. Пишем конфигурацию сборки самостоятельно

Итак, пришло время выйти из уютного мира в IDE, и вступить на путь уже кроссплатформенной стрельбы себе в ногу. Прежде всего, убедитесь что ваш код использует только стандартные функции С++. Если он вызывает ОС-зависимые API, вроде #include <Windows.h>, то рассмотрите чем их можно заменить. После этого воспользуемся следующим шаблоном CMake и создадим новый файл CMakeLists.txt:

cmake_minimum_required(VERSION 3.18)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Подключаем менеджер зависимостей Conan
list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR})
if (NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake")
    message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan")
    file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/v0.16.1/conan.cmake"
            "${CMAKE_BINARY_DIR}/conan.cmake"
            EXPECTED_HASH SHA256=396e16d0f5eabdc6a14afddbcfff62a54a7ee75c6da23f32f7a31bc85db23484
            TLS_VERIFY ON)
endif ()
include(${CMAKE_BINARY_DIR}/conan.cmake)

# Подключаем репозиторий зависимостей https://conan.io
conan_add_remote(NAME conan-center INDEX 1 URL https://center.conan.io VERIFY_SSL True)

# Скачиваем нужные нам зависимости, например:
conan_cmake_configure(REQUIRES catch2/2.13.6 spdlog/1.8.5 threadpool/20140926 simdjson/0.9.6 icu/69.1 GENERATORS cmake_find_package)
conan_cmake_autodetect(settings)
conan_cmake_install(PATH_OR_REFERENCE . BUILD missing REMOTE conan-center SETTINGS ${settings})

# Делаем зависимости видимыми для CMake
find_package(Catch2) # фреймворк тестирования
find_package(ICU) # работа с юникодом
find_package(spdlog) # логирование
find_package(simdjson) # парсинг json с помощью simd
find_package(ThreadPool) # ThreadPoolExecutor

# Подключаем файлы с основным кодом
file(GLOB proj_sources src/*.cpp)
add_executable(app ${proj_sources})

# Линкуем зависимости для основного кода (имена для линковки можно вытащить из описания зависимостей)
target_link_libraries(app PRIVATE ThreadPool::ThreadPool spdlog::spdlog simdjson::simdjson ICU::io ICU::i18n ICU::uc ICU::data)

# Подключаем файлы с кодом тестов
list(FILTER proj_sources EXCLUDE REGEX ".*/Main.cpp$")
file(GLOB test_sources test/*.cpp)
add_executable(test ${proj_sources} ${test_sources})

# Линкуем зависимости для тестов
target_link_libraries(test PRIVATE Catch2::Catch2 ThreadPool::ThreadPool spdlog::spdlog simdjson::simdjson ICU::io ICU::i18n ICU::uc ICU::data)

В этом шаблоне задаётся практически все, что нужно вам для сборки:

  1. Устанавливается стандарт C++20

  2. Подключается репозиторий библиотек Conan

  3. Задается соглашение, что файлы с кодом (.cpp, .h) будут лежать в папке src.

  4. Задается соглашение, что файлы с кодом для тестов будут лежать в паке test.

  5. Задаётся соглашение, что точки входа (int main) будут лежать в файлах Main.cpp

Убедитесь что ваш код хранится по этим соглашениям.
В скрипте также сразу подключается для примера несколько библиотек, по аналогии, следуя комментариям, вы можете добавить нужные вам.
Искать их можно тут: conan.io.

3. Как теперь собрать наше приложение в файл?

Мы отказались от кнопочек IDE в пользу более могущественного скрипта. Теперь нам понадобится платформа GCC или LLVM (возможно они уже есть на вашем компьютере), установленная система сборки CMake и менеджер зависимостей Conan. Их легко установить при помощи пакетного менеджера вашей ОС, или вручную, с официальных сайтов.

Откройте терминал в папке, где лежит CMakeLists.txt. Сначала нужно подготовить make-файлы. Воспринимайте это как предварительное действие:

cmake -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" -B bin

После этого можно собрать само приложение и тесты:

cmake --build bin --target all

Приложение будет расположено в директории bin в файле под именем app.
Тесты могут быть запущены при помощи отдельного файла test в той же директории.

Вот и всё! Не так уж сложно, правда? Теперь вы можете распространять свое ПО на разные ОС, легко собирать его и подключать библиотеки. Если вы попробовали и что-то не работает, или есть какие-то вопросы, то их можно задать в комментариях под этой статьёй, постараюсь ответить.

После того как вы разберетесь как это делать, можно пользоваться моим project-wizard'ом для создания кроссплатформенных C++ утилит и библиотек в один клик: github.com/demidko
Однако сперва рекомендую попробовать воспроизвести результат самостоятельно, для лучшего понимания, что именно вы делаете, ведь в процессе приходят осознание и навык.

Комментарии (34)


  1. Ritan
    03.10.2021 13:52
    +8

    add_definitions("-std=c++20")

    Рассказывать про кроссплатформенность и ломать её на пятой строчке приложенного CMakeLists.txt - это сильно.

    Нормальным способом было бы вот такое

    set(CMAKE_CXX_STANDARD 20)

    Или такое

    set_property(TARGET app PROPERTY CXX_STANDARD 20)

    file(GLOB...) использовать не рекоммендуется https://cmake.org/cmake/help/latest/command/file.html#filesystem

    project(library)

    И потом создаётся executable? Если это инструкция для новичка, то пускай она хотя-бы будет консистентной. Я уж не говорю о том, что советовать копипастить какие-то магические заклинания без их понимания - моветон


    1. Reformat Автор
      03.10.2021 13:59
      +3

      Нормальным способом было бы вот такое

      Спасибо, подправил

      file(GLOB...) использовать не рекомендуется

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

      Если это инструкция для новичка, то пускай она хотя-бы будет консистентной.

      Вы правы, привел названия проекта и исполняемых файлов в консистентный вид.

      копипастить какие-то магические заклинания без их понимания - моветон

      Ключевые аспекты я задокументировал, все остальное просто подгружает conan-wrapper


      1. Ritan
        03.10.2021 14:09
        +3

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

        Составить список исходников вручную. Т.е.
        set( SOURCE_LIST
        main.cpp
        component1.cpp
        component2.cpp)
        И потом использовать этот список так же, как сейчас. Это убирает один магический момент и решает проблему необходимости перезапуска cmake вручную для обнаружения новых файлов при их добавлении.


        1. Reformat Автор
          03.10.2021 14:15
          +2

          С точки зрения прозрачности может и так, однако... Подключать ручками каждый раз новые файлы грустновато на мой взгляд. И к тому же это ломает всю возможность "добавить CMakeLists.txt в существующий проект и полетели", что усложнило бы инструкцию.


          1. rwscar
            03.10.2021 14:25
            +2

            Подключение новых файлов ручками в CMake было сделано специально, чтобы CMake мог перегенерировать проект, когда новый файл будет добавлен (или старый удалён). А вот file(GLOB) так не умеет, вернее, не умел, пока в него не добавили костыль под названием CONFIGURE_DEPENDS.

            Чтобы не копипастить списки файлов, можно использовать статическую библиотеку.
            Ещё можно получить список файлов через get_target_properties. Можно сделать список, как выше посоветовали.

            По идее, если вы хотите скомпилироваться с полным списком cpp-файлов, ваш выбор — статическая библиотека.


            1. Chronicler
              03.10.2021 14:36
              +3

              По идее, если вы хотите скомпилироваться с полным списком cpp-файлов, ваш выбор — статическая библиотека.

              Тогда в инструкции для людей вчера запускавших код через IDE придется написать "а теперь мы сделаем статическую библиотеку для своего же кода, потому что пуристы решили что так идиоматически правильнее" =)

              Не находите что это слишком сложновато будет?


              1. Ritan
                03.10.2021 14:40
                +3

                "а теперь мы разобьём ваш main() на 24 функции по 10 строк, т.к. пуристы решили, что так идеоматически правильнее"


                1. Reformat Автор
                  03.10.2021 14:52
                  +6

                  Этот аргумент хорошо работает с кодом, но зачем усложнять конфигурацию? Она обслуживает код и по отношению к нему вторична, считаю лучше сохранять ее простой настолько, насколько возможно, идеально хранить вообще в одном файле, по примеру maven/gradle.


              1. rwscar
                04.10.2021 00:00

                Лично я — нет. В моей юности в учебниках типа «C++ для чайников» писали о том, что представляют собой объектные файлы и чем компилятор отличается от компоновщика. Хотя, признаю, что осознание пришло ко мне не сразу.
                Но, всё же, пуристом я себя не считаю. Да и про идиоматическую правильность готов поспорить: в условиях отсутствия кэша компилятора дважды включённые исходники должны компилиться дважды. А вот в случае статической библиотеки — не должны.


              1. IkaR49
                04.10.2021 00:46

                Тю. Я как-то писал библиотеку, с возможностью сборки и shared и static. Что бы не дублировать ничего я завел отдельный object target, который линковался в конечные статическую и динамическую библиотеку.


            1. Chronicler
              03.10.2021 14:37
              +1

              чтобы CMake мог перегенерировать проект

              Так это и происходит в инструкции каждый раз перед сборкой: перегенерируется проект.


              1. Ritan
                03.10.2021 14:41
                +1

                Нет. Не происходит. Если сгенерировать проект, а потом добавить новый файл, то make в директории билда новые файлы не обнаружит и собирать их не будет


                1. Arhamont
                  03.10.2021 14:51
                  +2

                  Странно. Создал по этому шаблону ради интереса проект, собрал один раз. Добавил файл, подключил в коде, собрал по инструкции снова - все работает.


        1. AndrewJD
          05.10.2021 07:21
          +1

          Составить список исходников вручную. Т.е.set( SOURCE_LIST ...

          Modern CMake рекомендует использовать таргеты. Т.е.:

          target_sources(test PRIVATE main.cpp component1.cpp component2.cpp)

          не дублировать списки файлов с кодом для двух таргетов 

          Можно получить список файлов:

          $<TARGET_PROPERTY:test , SOURCES>


    1. rwscar
      03.10.2021 14:13
      +5

      А ещё можно:

      target_compile_features(app PRIVATE cxx_std_17)

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


      1. Ritan
        03.10.2021 14:38

        О, а вот про такое не слышал. Видимо одно из недавних дополнений


  1. Sazonov
    03.10.2021 15:36
    +2

    Я недавно перешёл на vcpkg в режиме манифеста. Субъективно - проще и шустрее чем conan. Плюс не тянет за собой питон.

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


    1. libroten
      04.10.2021 05:22
      +3

      Ключевое здесь - если понимаешь, что делаешь.

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

      Кому надо - тот прочитает и разберется. Кому это не надо - возьмёт и скопипастит исходный скрипт сборки, чтобы не заморачиваться и просто обеспечить сборку своего проекта.


  1. me21
    03.10.2021 15:53

    Спасибо! Как раз начинаю новый проект и решил попробовать cmake, чтобы не привязывать коллег к одной IDE и упростить подключение библиотек.

    Вот как раз вопрос есть - можно ли для основного проекта использовать один компилятор (кросс), а для тестов - другой (хост)? Или надо два раза запускать cmake с разными каталогами сборки и параметрами?


    1. Reformat Автор
      03.10.2021 16:09

      Или надо два раза запускать cmake с разными каталогами сборки и параметрами?

      Да


    1. Sazonov
      04.10.2021 09:44

      Могу ошибаться, но вроде для систем msbuild (windows) и ninja (cross platform) из коробки можно множественные build type настроить. Но проще действительно два разных каталога. Впрочем это не сложно автоматизировать.


  1. thecove
    03.10.2021 16:08
    +1

    Это проблема отчасти порождается спорной практикой в обучении, когда код учат писать и запускать средствами IDE, таких как Visual Studio, Code::Blocks, и других. Поначалу такой подход работает, но лишь до первой реальной задачи сделать что-то, что запустится не только на вашем компьютере. 

    билдим что то в Visual Studio с static Run-Time Library и наш ехе запускается на любой windows машине. Ок, почти на любой если версия не супер древняя, но там зачастую достаточно поставить последний Redistributable Package.

    Если проект в исходниках то билдится в студии на любой машине. Но опять же при наличии одинаковых версий SDK и DDK

    Почти все популярные либы имеют встроенный солюшен для сборки через студию.

    За 20+ лет ни разу не сталкивался с тем чтобы проект VisualStudio не переносился на другую машину с такой же операционкой windows.

    99 из 100 переносится без каких либо танцев с бубнами. 1 из 100 танцы были связаны с кривыми руками владельца машины приемника когда в windows environment было всякого хлама понапрописано что вызывало конфликты в проекте.

    Под Linux, Android и iOS умеет проекты собирать так же из коробки.

    Поэтому у меня именно эта ваша фраза вызвала недоумение.


    1. Reformat Автор
      03.10.2021 16:12
      +2

      А как собрать ваш проект под macOS, под Ubuntu? Как собрать под той-же Windows в другой IDE, Code::Blocks или CLion например?

      Вы описываете Windows-only решение, а статья про кроссплатформенный C++


      1. thecove
        04.10.2021 06:27

        А как собрать ваш проект под macOS, 

        из "коробки" идет поддержка macOS и iOS

        Вы описываете Windows-only решение, а статья про кроссплатформенный C++

        Windows, Android, iOS, Linux, Azure, macOS, tvOS XBox

        Вот хоть глаза мне выколи но я не вижу Windows-only. 

        У меня, например, мой игровой инди проект сразу билдится под Windows, Android и iOS


    1. Reformat Автор
      03.10.2021 16:14
      +1

      Или надо два раза запускать cmake с разными каталогами сборки и параметрами?

      А я сталкивался с чудной практикой установки библиотек глобально в директорию ОС и установкой зависимости от такого пути. Разумеется эти VS проекты были непереносимы.
      Microsoft наборот продвигает CMake в последних версиях VS, кстати.


    1. Reformat Автор
      05.10.2021 07:48
      +1

      Я вижу разницу между загрузкой всего проекта ради VS solution и указанием зависимости в скрипте одной строчкой. Объясните, как в первом случае вы будете обновлять свой проект? Дайте угадаю, снова загрузите всю зависимость целиком...


  1. TargetSan
    03.10.2021 16:53
    +1

    Этот подход работает, но, увы, только если у вас проект уровня Hello World. Стоит докинуть пару усложнений вроде внешней зависимости не в вашем любимом пакетном менеджере или кодогенератора - и CMakeLists начинает шустро так распухать. Это усугубляется крайне убогим уровнем скриптового языка CMake. Один гигакостыль generator expressions чего стоит. К примеру, задача добавить в PATH или ещё куда пути всех зависимостей при запуске под отладчиком до сих пор решается руками.

    В целом же, одна из основных проблем С++ - крайне фрагментированный тулинг. Интероп между разными системами сборки или пакетными менеджерами отсутствует как класс. Либо используешь фиксированный набор вроде CMake+VCPKG, либо вынужден покупать асбестовый стул.


    1. tarekd
      03.10.2021 21:56
      -1

      большие проекты переезжают на bazel


      1. rwscar
        03.10.2021 23:44
        +1

        Как по мне — это спорное сильное утверждение, не то чтобы чем-то доказанное (хотя, возможно, существует такая тенденция).
        С тем же успехом можно сказать, что они переезжают на conan, или на vcpkg, а самые большие проекты переезжают на собственные велосипеды, ибо никто снаружи компании не обслужит нужды сборки в компании лучше, чем её сотрудник.
        Хотя, не буду скрывать, системы сборки/управления пакетами в C++ мне не нравятся. И да, я с завистью поглядываю в лагерь Rust с их Cargo.


  1. navferty
    04.10.2021 02:39
    +2

    Я не пишу на плюсах, и вообще мимокрокодил, но вот это

    REGEX ".*Main.cpp$"

    не доставит проблем, если будет какой-нибудь файл вроде ProductMain.cpp ?

    Может быть, лучше поменять на ".*/Main.cpp$"


    1. Reformat Автор
      04.10.2021 03:19
      +2

      Спасибо за вашу внимательность


  1. tony-space
    04.10.2021 03:03
    +4

    У Visual Studio, к примеру, сборщиком по умолчанию является MSBuild, а у CLion - CMake.

    CMake не является системой сборки. Это генератор скриптов для системы сборки. Если вы используете Linux, то CMake по-умолчанию будет генерировать Makefile'ы. Если же вы сидите на Windows и у вас стоит VS, то CMake по-умолчанию генерирует скрипты для MSBuild. На маках так вообще две нативные системы: Makefiles и XCode (не знаю как он под капотом работает, может тоже через Makefiles).

    Справедливо и другое: CLion может вызывать MSBuild, а Visual Studio может работать с Ninja.

    Короче, гуглите про ключ -G для CMake.


  1. lepota
    05.10.2021 07:46

    conan есть, а ctest, cpack нет. project() не увидел.


  1. Ingulf
    13.10.2021 16:03
    -1

    и сразу conan который сам по себе тож зависимость? только теперь мы еще и навязываем его остальным.