Рун не должен резать
Тот, кто в них не смыслит.
В непонятных знаках
Всякий может сбиться.
(Сага об Эгиле)
Многие начинающие программисты, уже освоив синтаксис 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)
В этом шаблоне задаётся практически все, что нужно вам для сборки:
Устанавливается стандарт C++20
Подключается репозиторий библиотек Conan
Задается соглашение, что файлы с кодом (.cpp, .h) будут лежать в папке src.
Задается соглашение, что файлы с кодом для тестов будут лежать в паке test.
Задаётся соглашение, что точки входа (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)
Sazonov
03.10.2021 15:36+2Я недавно перешёл на vcpkg в режиме манифеста. Субъективно - проще и шустрее чем conan. Плюс не тянет за собой питон.
Насчёт file glob не соглашусь с комментариями выше (может и минусов отхвачу) - я не вижу ничего плохого в его использовании, если понимаешь что делаешь. Очень удобно для быстрорастущих проектов куда часто добавляются новые файлы. А на одном большом проекте у нас был простенький скрипт, который пробегался по каталогу с исходниками и считал хэш от суммы всех путей. Если хэш поменялся - значит надо переиндексировать файлы.
libroten
04.10.2021 05:22+3Ключевое здесь - если понимаешь, что делаешь.
Но статья - инструкция для новичков. Мне кажется, статья была бы чуточку лучше, если бы по поводу glob (да и не только его) было рассказано подробнее - что это, зачем это здесь, какие есть еще варианты.
Кому надо - тот прочитает и разберется. Кому это не надо - возьмёт и скопипастит исходный скрипт сборки, чтобы не заморачиваться и просто обеспечить сборку своего проекта.
me21
03.10.2021 15:53Спасибо! Как раз начинаю новый проект и решил попробовать cmake, чтобы не привязывать коллег к одной IDE и упростить подключение библиотек.
Вот как раз вопрос есть - можно ли для основного проекта использовать один компилятор (кросс), а для тестов - другой (хост)? Или надо два раза запускать cmake с разными каталогами сборки и параметрами?
Reformat Автор
03.10.2021 16:09Или надо два раза запускать cmake с разными каталогами сборки и параметрами?
Да
Sazonov
04.10.2021 09:44Могу ошибаться, но вроде для систем msbuild (windows) и ninja (cross platform) из коробки можно множественные build type настроить. Но проще действительно два разных каталога. Впрочем это не сложно автоматизировать.
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 умеет проекты собирать так же из коробки.
Поэтому у меня именно эта ваша фраза вызвала недоумение.
Reformat Автор
03.10.2021 16:12+2А как собрать ваш проект под macOS, под Ubuntu? Как собрать под той-же Windows в другой IDE, Code::Blocks или CLion например?
Вы описываете Windows-only решение, а статья про кроссплатформенный C++
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
Reformat Автор
03.10.2021 16:14+1Или надо два раза запускать cmake с разными каталогами сборки и параметрами?
А я сталкивался с чудной практикой установки библиотек глобально в директорию ОС и установкой зависимости от такого пути. Разумеется эти VS проекты были непереносимы.
Microsoft наборот продвигает CMake в последних версиях VS, кстати.
Reformat Автор
05.10.2021 07:48+1Я вижу разницу между загрузкой всего проекта ради VS solution и указанием зависимости в скрипте одной строчкой. Объясните, как в первом случае вы будете обновлять свой проект? Дайте угадаю, снова загрузите всю зависимость целиком...
TargetSan
03.10.2021 16:53+1Этот подход работает, но, увы, только если у вас проект уровня Hello World. Стоит докинуть пару усложнений вроде внешней зависимости не в вашем любимом пакетном менеджере или кодогенератора - и CMakeLists начинает шустро так распухать. Это усугубляется крайне убогим уровнем скриптового языка CMake. Один гигакостыль generator expressions чего стоит. К примеру, задача добавить в PATH или ещё куда пути всех зависимостей при запуске под отладчиком до сих пор решается руками.
В целом же, одна из основных проблем С++ - крайне фрагментированный тулинг. Интероп между разными системами сборки или пакетными менеджерами отсутствует как класс. Либо используешь фиксированный набор вроде CMake+VCPKG, либо вынужден покупать асбестовый стул.
tarekd
03.10.2021 21:56-1большие проекты переезжают на bazel
rwscar
03.10.2021 23:44+1Как по мне — это спорное сильное утверждение, не то чтобы чем-то доказанное (хотя, возможно, существует такая тенденция).
С тем же успехом можно сказать, что они переезжают на conan, или на vcpkg, а самые большие проекты переезжают на собственные велосипеды, ибо никто снаружи компании не обслужит нужды сборки в компании лучше, чем её сотрудник.
Хотя, не буду скрывать, системы сборки/управления пакетами в C++ мне не нравятся. И да, я с завистью поглядываю в лагерь Rust с их Cargo.
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.
Ingulf
13.10.2021 16:03-1и сразу conan который сам по себе тож зависимость? только теперь мы еще и навязываем его остальным.
Ritan
Рассказывать про кроссплатформенность и ломать её на пятой строчке приложенного CMakeLists.txt - это сильно.
Нормальным способом было бы вот такое
Или такое
file(GLOB...) использовать не рекоммендуется https://cmake.org/cmake/help/latest/command/file.html#filesystem
И потом создаётся executable? Если это инструкция для новичка, то пускай она хотя-бы будет консистентной. Я уж не говорю о том, что советовать копипастить какие-то магические заклинания без их понимания - моветон
Reformat Автор
Спасибо, подправил
glob используется с определенной целью - не дублировать списки файлов с кодом для двух таргетов (приложение и тесты). Если вы знаете как сделать лучше, то было бы здорово услышать.
Вы правы, привел названия проекта и исполняемых файлов в консистентный вид.
Ключевые аспекты я задокументировал, все остальное просто подгружает conan-wrapper
Ritan
Составить список исходников вручную. Т.е.
set( SOURCE_LIST
main.cpp
component1.cpp
component2.cpp)
И потом использовать этот список так же, как сейчас. Это убирает один магический момент и решает проблему необходимости перезапуска cmake вручную для обнаружения новых файлов при их добавлении.
Reformat Автор
С точки зрения прозрачности может и так, однако... Подключать ручками каждый раз новые файлы грустновато на мой взгляд. И к тому же это ломает всю возможность "добавить CMakeLists.txt в существующий проект и полетели", что усложнило бы инструкцию.
rwscar
Подключение новых файлов ручками в CMake было сделано специально, чтобы CMake мог перегенерировать проект, когда новый файл будет добавлен (или старый удалён). А вот file(GLOB) так не умеет, вернее, не умел, пока в него не добавили костыль под названием CONFIGURE_DEPENDS.
Чтобы не копипастить списки файлов, можно использовать статическую библиотеку.
Ещё можно получить список файлов через get_target_properties. Можно сделать список, как выше посоветовали.
По идее, если вы хотите скомпилироваться с полным списком cpp-файлов, ваш выбор — статическая библиотека.
Chronicler
Тогда в инструкции для людей вчера запускавших код через IDE придется написать "а теперь мы сделаем статическую библиотеку для своего же кода, потому что пуристы решили что так идиоматически правильнее" =)
Не находите что это слишком сложновато будет?
Ritan
"а теперь мы разобьём ваш
main()
на 24 функции по 10 строк, т.к. пуристы решили, что так идеоматически правильнее"Reformat Автор
Этот аргумент хорошо работает с кодом, но зачем усложнять конфигурацию? Она обслуживает код и по отношению к нему вторична, считаю лучше сохранять ее простой настолько, насколько возможно, идеально хранить вообще в одном файле, по примеру maven/gradle.
rwscar
Лично я — нет. В моей юности в учебниках типа «C++ для чайников» писали о том, что представляют собой объектные файлы и чем компилятор отличается от компоновщика. Хотя, признаю, что осознание пришло ко мне не сразу.
Но, всё же, пуристом я себя не считаю. Да и про идиоматическую правильность готов поспорить: в условиях отсутствия кэша компилятора дважды включённые исходники должны компилиться дважды. А вот в случае статической библиотеки — не должны.
IkaR49
Тю. Я как-то писал библиотеку, с возможностью сборки и shared и static. Что бы не дублировать ничего я завел отдельный object target, который линковался в конечные статическую и динамическую библиотеку.
Chronicler
Так это и происходит в инструкции каждый раз перед сборкой: перегенерируется проект.
Ritan
Нет. Не происходит. Если сгенерировать проект, а потом добавить новый файл, то make в директории билда новые файлы не обнаружит и собирать их не будет
Arhamont
Странно. Создал по этому шаблону ради интереса проект, собрал один раз. Добавил файл, подключил в коде, собрал по инструкции снова - все работает.
AndrewJD
Modern CMake рекомендует использовать таргеты. Т.е.:
target_sources(test PRIVATE main.cpp component1.cpp component2.cpp)
Можно получить список файлов:
$<TARGET_PROPERTY:test , SOURCES>
rwscar
А ещё можно:
Это один из наиболее правильных путей, поскольку настройка будет связана с одной конкретной целью, а не всем проектом целиком.
Ritan
О, а вот про такое не слышал. Видимо одно из недавних дополнений