В данном цикле статей я больший упор хотел сделать на историю разработки некой open source библиотеки, безотносительно к конкретной cpprt. Историю от написания исходников (с акцентом на какие-то интересные вещи, которые интересно почитать людям вообще, безотносительно к самой библиотеке), до формирования репозитория (с уроком CMake) и продвижения библиотеки (где часть продвижения подразумевает публикацию данного цикла статей). Такой себе учебный демо-проект для людей, которые подумывали выложить свой open source, но либо боялись, либо не знали как.
Я, конечно, был бы не против, если бы библиотека как-нибудь ожила и в статьях есть минимальное количество рекламы библиотеки (я старался прятать её под спойлеры). Но всё-таки цели данного цикла я рассматривал скорее учебные и, как я надеюсь, применимые вообще, без связи с моей библиотекой.
Просьба учитывать это при чтении цикла статей.
Эта статья является второй в цикле о библиотеке cpprt, предназначенной для добавления и использования минимальной метаинформации о классах С++.
В отличие от первой статьи, здесь почти ничего не будет о самой библиотеке cpprt. Я постарался подробно и максимально абстрагируясь от своей библиотеки изложить историю оформления библиотеки для её цивилизованной публикации на GitHub.
В статье затрагиваются вопросы лицензирования, структуры проекта и достаточно много внимания уделяется CMake.
0. Вступление
Чтобы оттянуть необходимость говорить какие-то вступительные слова, представлю структуру данной публикации:
Раздел №0. Данный раздел. Описание структуры публикации и вступительные слова.
Раздел №1. Немного о лицензировании.
Раздел №2. Немного о создании отдельного git-репозитория подпроекта из git-репозитория основного проекта.
Раздел №3. Мысли о структуре проекта вообще и попытки анализа структуры нескольких существующих репозиториев.
Раздел №4. Уроки CMake на базе хроники разработки cmake-конфигурации для проекта cpprt.
Раздел №5. Несколько слов и много ссылок по поводу документации.
Раздел №6. Краткий раздел с мыслями о продвижении библиотеки.
Раздел №7. Заключение и титры.
Так. Структура есть и по-прежнему нужно с чего-то начать… Ладно, давайте так.
Цель статьи: Рассказать о том, как я готовил библиотеку к публикации на GitHub.
Я работал над статьёй, параллельно исследуя некоторые аспекты мира отрытого ПО. На мой взгляд, благодаря этому статья может быть ценна людям, который находятся только в начале пути: им не придётся лазить по всей сети в поисках материала, я постарался сохранить здесь по максимуму ссылки на сайты, по которыми пришлось пройтись мне чтобы собрать репозиторий.
Я прекрасно отдаю себе отчёт в том, что репозиторий, как и статья, далеки от идеала. Пишите в личку, пишите комментарии, смотрите библиотеку, коммите в библиотеку – я не забуду упомянуть автора внесённых предложений в титрах к статье. Каждая ваша правка поможет людям, которые будут идти в мир открытого ПО по вашим стопам!
Много пафоса нагнал… Ладно, теперь чуть истории. В первой статье цикла я упоминал, что классы и макросы библиотеки cpprt использовались в рамках моего большего проекта. Они не были выделены отдельно и жили просто как часть исходников. Но в какой-то момент я заметил, что у данных классов почти нет зависимостей от основного проекта, а их функционал самодостаточен и применим для некоторых типичных задач.
И вот тогда-то я задумал выделить часть проекта в отдельный подпроект и осуществить давнюю мечту: опубликовать библиотеку с открытым исходным кодом.
Перед тем как читать дальше, вам, вероятно, будет интересно ознакомиться со следующими мыслями (если вы не читали первую статью): признание в истинных коварных мотивах данных публикаций (ссылка).
И ещё два предупреждения читающему:
Если мне так и не удалось отбить у вас желание читать сей опус – поехали!
1. Выбор лицензии
Есть одна штука, которая независимо от качества кода делает исходники весомее в моих глазах. Возможно, кому-нибудь это покажется смешным, но для меня это шапка с указанием лицензии. Вот смотрю: есть шапка – значит проект действительно продвинутый. Нет шапки – не так круто. Это как-то автоматически работает, на подсознании.
Поэтому когда я решил опубликовать библиотеку, первым делом руки чесались включить в неё информацию о лицензировании в исходники. А для этого, в свою очередь, нужно было выбрать лицензию.
Я начинал исследование вопроса лицензирования с опаской. Всегда побаивался этой темы. Думал: выберу какую-то не ту лицензию – и вот уже обязан безвозмездно поддерживать код до скончания дней, не имея при этом никаких прав, включая права голоса.
На практике всё оказалось не так страшно – во всяком случае, если говорить о лицензиях для открытого ПО (с проприетарными лицензиями я пока не разбирался). Единственное, что важно помнить при выборе – так это то, что подобные лицензии бывают двух типов: open source и free software. Звучит похоже, но между ними есть, как говорят в Одессе, две большие разницы. Разберёмся подробнее:
Open source. Осторожно, open source «open» неспроста! Open source превращает в open source любой код, который его использует — и при этом исходники любого open source кода должны быть открыты по первому требованию.
Вот вы, например, используете в своём проекте библиотеку под какой-нибудь open source лицензией. Используете год, используете два… И вдруг у вас просят, чтобы вы показали весь свой код. И вы не имеете права отказаться! Принудительный IT-коммунизм. «От каждого по исходнику, каждому по исходнику». Если вас подобная перспектива пугает — следите за лицензиями проектов, которые подключаете.
Набор пунктов, требующих открывать код в рамках семейства лицензии open source, имеет своё название: копилефт. Основной лицензией, включающей в себя копилефт, является лицензия GPL (General Public License) и родственные ей.
Преимущества: Полная открытость кода позволяет повысить его качество за счёт более широкой аудитории читателей и ускорить его разработку за счёт более широкой аудитории писателей. Ну и, конечно, ещё одно преимущество – вы чувствуете, что приносите миру добро. Make open source, not war!
Недостатки: Ограничена возможность применения продуктов под GPL-лицензией в проприетарном коде и/или в коде, содержащем коммерческую тайну. Какая ж это тайна, если о ней нужно рассказать по первому требованию?
Free software. Лицензии free software разрешают пользователю делать с кодом что угодно без каких-либо ограничений, иногда с какими-нибудь чисто условными требованиям (например, с обязательством указывать автора оригинальной библиотеки). Одной из таких лицензий является, например, лицензия MIT. Сюда также входит семейство лицензий BSD.
Преимущества: Дополнительные качественные коммиты от щедрых коммерческих компаний.
Недостатки: Жадные компании могут утащить код себе, форкнуть в корпоративный репозиторий и тихонько развивать его только для себя, не делясь с сообществом.
Library public license. Особняком среди лицензий для открытого исходного кода стоит лицензия LGPL (Library General Public License) и родственные ей. Данный тип лицензий включает в себя признаки как open source, так и free software.
LGPL позволяется свободно использовать код, скомпилированный в бинарное представление (исполняемый файл либо динамическую библиотеку) где угодно, в том числе в ваших коммерческих проектах без ограничений и без требования открывать ваш код.
Но если вам захочется статического связывания исходников проекта под лицензией LGPL с кодом своего проекта – добро пожаловать в мир open source со всеми его плюсами и минусами. Извольте делиться кодом своего проекта.
В качестве примера проекта, использующего LGPL, можно привести Qt. За счёт LGPL-лицензирования его динамическими библиотеками можно пользоваться в проприетарном ПО без ограничений (обсуждение этого на официальном форуме Qt и аналогичное обсуждение на сайте qtcentre).
Преимущества: Косвенная заинтересованность в качестве кода коммерческих организаций (им же хочется, чтобы код собранной библиотеки работал хорошо) и, одновременно, запрет на развитие LGPL-проекта «втихую», как это возможно с лицензиями free sofrware.
Недостатки:Чтобы использовать библиотеку под LGPL лицензией в проприетарном ПО, придётся самому организовывать связывание своего продукта с собранной в dll (либо в so) библиотекой, что потребует пусть небольших, но всё же усилий.
Если знаете про какие-нибудь ещё интересные лицензии – можете написать о них в комментариях. Думаю, это всем будет интересно.
Изучив существующие лицензии, я остановил свой выбор на лицензии MIT, как на одной из самых либеральных лицензий. Среди данных ссылок в конце данного раздела есть несколько про лицензию MIT. Там ней рассказано достаточно хорошо, я лучше не буду пересказывать, чтобы не искажать суть.
В конце раздела хочу коротко рассказать о том, как я добавил лицензионную информацию в свой проект (один из вариантов оформления):
1. Положил в корень репозитория файл licence.txt (либо LICENCE.txt, чтобы заметнее было) с текстом лицензии, который скопировал вот с этого сайта.
2. Во все файлы исходного кода добавил шапку следующего вида:
//
// <имя файла>
//
// Copyright © <года действия лицензии> <имя автора> (<опционально, почта автора>)
//
// Distributed under <название лицензии> (See accompanying file LICENSE.txt or copy at
// <путь к сайту лицензии>)
//
Примечание: Я встречал несколько репозиториев, в рамках которых в шапки для исходного кода добавляли полный текст лицензий. Возможно, так вы защитите код надёжнее, но будете раздражать пользователя, который будет пролистывать это ваше долгое лицензионное вступление для каждого файла, с которым имеет дело.
Закончу раздел я, как и обещал, списком ссылок на интересные материалы:
2. Про основные open source лицензии в одной картинке.
3. Про лицензии откытого ПО чуть подробнее.
4. Про open source лицензии совсем подробно. Информация про лицензию MIT в рамках той же статьи.
5. Совсем подробно про MIT-лицензию, с историческими экскурсами.
6. Подробная статья про MIT-лицензию. Помимо этого, интересна некоторыми мыслями про открытое ПО вообще.
7. Очень подробная статья с философией от GNU-сообщества на тему разницы между free software и open source.
8. Любопытная статья о том, что не всё так просто в мире открытого ПО.
9. Статья со смелой и интересной идеей: создать лицензию, требующую открывать не исходный код, а тесты и результаты работы тестов.
10. Вопрос, который я когда-то задавал на тостере (забавно его было перечитать сейчас, в марте 2016-ого года). В этом вопросе пользователь с ником @littleguga посоветовал мне лицензию MIT, которой я, в конце концов, воспользовался.
2. Отцепление кода от основного проекта
Бывает так, что код, который хочется превратить в библиотеку, не изолирован в отдельный репозиторий. Он может жить с начала работы над проектом в рамках какой-нибудь папки основного репозитория и, кажется, история редактирования этих исходников намертво сцеплена с историей основного проекта и шансов отцепить её нет никаких.
К счастью, всё не так печально. В подобных случаях можно использовать механизм git subtree split. Я научился этому трюку вот тут (ещё тут упоминается, а вот рассказ об альтернативном методе, filter-branch). Если будет интересно, я могу как-нибудь подробнее рассказать о том, какие приключения пережил с этим каменным молотом в руках, раскалывая монолитные репозитории на подмодули и склеивая из них разные штуки с помощью submodule. Здесь же я лишь коротко опишу, как можно переселить историю редактирования какой-то отдельной папки основного репозитория в отдельный репозиторий:
{submodule-branch} — ветка, в которую скопируется вся история работы с исходниками папки, расположенной по пути {submodule-relative-path}.
После вызова этой команды в локальный репозиторий добавится ветка {submodule-branch}, содержащая все коммиты, связанные с изменениями в папке {submodule-relative-path}. Вызов данной команды лишь создаст новую ветку и скопирует в неё коммиты, ничего другого изменено не будет.
Замечание: Стоит отметить, что коммит-месседжи, привязанные к скопированным коммитам, останутся не изменёнными. Они могут сохранить лишнюю для истории редактирования папки информацию (в случае если в каких-то коммитах были изменения и в папке и вне её). Я не знаю, как этого избежать и не думаю, что можно вообще как-то этого избежать.
{submodule-repo-URL} — URL ремоут-репозитория, в который мы хотим поселить историю изменений папки.
{master} — имя, которое получит ветка {submodule-branch} в ремоут-репозитории {submodule-repo-URL}. Если это первая ветка, добавляемая в репозиторий, то, подчиняясь древней традиции именования главных веток git, лучше всего называть её «master».
После вызова этой команды, в репозиторий, расположенный по URL {submodule-repo-URL} будет добавлена новая ветка с именем {master}, содержащая в себе историю изменений папки {submodule-relative-path}.
В общем, всё. Проделав описанные шаги для своего основного проекта, я изолировал историю редактирования исходников будущей библиотеки cpprt, получив таким образом очень черновой репозиторий и сохранив всю историю изменений её файлов (пусть и с некоторым мусором в коммит-месседжах).
Репозиторий был готов… Но до публикации было ещё далеко. Ещё полагалось привести репозиторий к подобающему виду. Для этого одних исходников и лицензии было мало. Нужно было озаботиться созданием кучи сопроводительных материалов: тестов, примеров, продумать механизм сборки всего что умеет собираться, создать всякие read me файлов, документации, и т.д… Для всего этого следовало продумать структуру репозитория. Следующий раздел статьи посвящён структуре проекта.
Полезные ссылки:
1. Вопрос, который я задавал когда-то, выбирая между submodule и subtree. Там есть достаточно длинное обсуждение и много ссылок (часть из них я уже использовал выше). Возможно, тем, кто интересуется темой модульной организации проекта, может быть интересно почитать дискуссию и полазить по ссылкам. Кстати, хочу, воспользовавшись случаем, поблагодарить пользователя Andy_U за активную дискуссию в обсуждении этого вопроса – несмотря на то, что я, в конце концов, выбрал submodule.
2. Ещё один вопрос, в котором я описал трудности, с которыми столкнулся при попытке работы с subtree… Кстати, возможно, кто-нибудь сможет объяснить что я делал не так?
3. Мысли по поводу структуры репозитория
Итак, структура репозитория… На мой взгляд, это нечто из разряда архитектуры. Архитектура кода задаёт принципы взаимодействия его элементов друг с другом. Архитектура – она же структура – репозитория вашего проекта задаёт принципы взаимодействия с ним других проектов. Под понятием «другие проекты» я понимаю здесь не только кодобазы, системы сборок или бизнес процессы. Я имею в виду также людей, которые будут пользоваться вашим кодом и будут вносить в него свои изменения.
Я попробовал в силу своего понимания сформулировать требования к проекту с точки зрения пользователя и с точки зрения контрибутора.
Взгляд с позиции пользователя библиотеки
Перечисление требований в порядке их возникновения при знакомстве с библиотекой:
1. Хочу понимать, с чем имею дело. Какой-нибудь ридмик с кратким описанием о чём это в корне проекта, и чтобы он отображался на странице проекта в GitHub. Ну и, конечно, документация. Желательно, с картинкам.
2. Я хочу глянуть как работать с кодом. Тесты, примеры использования кода, и чтобы при этом было понятно, как оно собирается.
3. Про сам проект… Хочется тратить минимум нервов и времени на сборку, настройку и интеграцию. Было бы круто, если бы имелась папка с хедерами и собранная библиотека для моего компилятора и моей платформы. Подключу, слинкую – и в продакшн и не хочу я разбираться в этой вашей билд-системе.
Примечание: Тут и дальше речь идёт о специфических для С++ вещах. Для прочих языков всё несколько проще… Наверно.
4. У меня платформонезависимый проект! Я хочу, чтобы была возможность собрать библиотеку самому и без необходимости подстраиваться под вашу систему сборки. Если говорить по-другому, мне нужен CMake (если не ясно, что такое CMake – ничего страшного, о нем будет сказано дальше)!
Дополнительные требования:
5. Так, ваша библиотека содержит слишком много возможностей. Можно какие-нибудь тулзы в поставке? И желательно, чтобы сразу собранные.
6. Я уже давно использую ваш проект. Слышал, он обновился недавно? Хочу знать, что изменилось.
Итого, с учётом представленных требований, получается следующая структура:
file_in_root
Файл, находящийся в корне дерева файлов. К любому элементу файлового дерева могут быть добавлены комментарии. Данный текст является комментарием к файлу file_in_root. Комментарий пишется под записью файла/папки, либо, если помещается в одной строке, справа от записи.
/folder_in_root
Папка, находящаяся в корне дерева файлов.
-file_in_folder
Файл, находящийся в некой папке. Чтобы понять, в какой папке лежит файл, нужно глянуть элементы файлового дерева выше. Количество дефисов задаёт уровень вложенности. В данном случае один дефис означает, что файл лежит в какой-то папке, которая, в свою очередь, находится в корне файлового дерева.
-/folder_in_folder
Папка, находящаяся в другой папке, лежащей в корне файлового дерева.
1. Что за библиотека?
README.txt — короткое описание проекта.
/doc — тут лежат файлы, детально описывающие проект и его API.
2. Тесты и примеры.
/tests — тестовые проекты, которые максимально просто запустить.
/examples — примеры, которые тоже должны запускаться как можно проще.
3. Мне бы быстренько подключить...
/include — интерфейс для доступа к API собранной библиотеки.
/lib — сборка исходников в статическую библиотеку.
/bin — сборка исходников в динамическую библиотеку.
Примечание: Папки lib и bin, желательно, должны содержать сборки основных компиляторов и платформ.
4. Хочу собрать сам!
/src — исходные коды проекта.
cmake_readme.txt — опциональная информация о том, как работать с cmake-файлами для генерации конфигураций проекта.
CMakeLists.txt — файл с конфигурацией сборки.
5. Инструменты? Давайте вот сюда:
/tools — тулзы, облегчающие работу с библиотекой. Либо собранные, либо в виде исходников.
6. Обновления
change_list.txt — информация о последних изменениях в проекте.
Взгляд с позиции контрибутора библиотеки
Перечисление требований в порядке их возникновения при знакомстве с библиотекой:
0. Скорее всего, на первом этапе знакомства с библиотекой я не контрибутор – я пользователь. Поэтому, как минимум, в мои требования входят указанные выше требования пользователя.
1. Мне нравится ваш проект, и я хочу его развивать. Я хотел бы почитать какой-нибудь ридмик по поводу того, как влиться в дружную семью контрибуторов.
2. Я хочу понять, как устроена библиотека изнутри. Мне нужна углублённая документация для разработчиков. В идеале документация нужна также к системе сборки проекта.
3. Я не хочу лазить по всему проекту при работе с билд-системой. Если сборка требует какого-то обилия конфигурационных файлов, либо дополнительных программ – пусть лежит в отдельной папке… Вообще, во всём должен быть порядок, файлы должны быть сгруппированы по назначению.
Дополнительные требования:
4. Пользовательские тулзы – это, конечно, хорошо… Но проект реально большой. Нужны какие-то инструменты и для разработчиков.
Итого, с учётом требований, получаются следующие уточнения структуры идеального проекта:
1. Вводная информация:
contributors_README.txt — краткое описание проекта для желающих вложить силы в его развитие.
2. Документация:
/doc
-/developer_doc — документация с деталями реализации проекта.
Примечание: При такой структуре пользовательскую документацию стоит также отложить в отдельную папку в рамках папки doc (в какую-нибудь user_doc). Такая организация документации, по моей задумке, помимо структурной упорядоченности, интриговала бы любопытного читателя доки поглядывать: «что же там внутри доки для посвящённых?» — и, таким образом, делала бы читателя вероятным контрибутором.
3. Порядок в проекте:
/build
Папка с билд-системой. Желательно, чтобы вне этой папки по проекту болталось минимум файлов билд-системы.
4. Инструменты для разработки:
/tools
-/developer_tools
Аналогично документации, собрать тулзы по их назначению: пользовательские отдельно, разработчиские отдельно.
Перечисленные здесь требования касаются субъективно моего восприятия. Если у вас есть какие-нибудь свои мысли по поводу — поделитесь.
Во время анализа проектов с GitHub я заметил, что в них часто упоминаются файлы для работы с утилитой CMake. Больше того, судя по некоторым материалам, именно CMake повлиял на формирование классической структуры репозитория открытого кроссплатформенного ПО на С++. Назревало чувство, что его не обойти. Предстоял…
4. Путь к познанию CMake
Благодарности: Спасибо Станиславу Макарову (Nipheris), общение с которым послужило для меня толчком к изучению CMake.
CMake — утилита, позволяющая генерировать конфигурационные файлы конкретных make-систем (и/или проекты некоторых IDE) для сборки С/С++ проектов на основании универсальной, абстрактной конфигурации (список поддерживаемых систем сборки/IDE). После генерирования уже конфигурации и/или файлы проектов, получаемые на выходе из CMake, используются конкретными механизмами сборки; только в результате их работы получается собранный продукт.
Можно сказать, что CMake — это meta-make tool, абстракция над системами сборок C/C++ проектов.
Признаюсь, при формировании структуры репозитория для библиотеки cpprt я всячески увиливал от использования CMake. Типичная прокрастинация… Я придумывал разнообразные отговорки, сочинял свои билд-системы на питоне, батниках и ещё фиг знает на чём, городил какие-то хрупкие конструкции на подмодулях. Апофеозом всего этого безумия стала целая теория, обосновывающая мой отказ от CMake. Мол, так как библиотека cpprt очень маленькая (всего два файла), для её интеграции достаточно вставить исходный код «библиотеки» прямо в проект пользователя как подмодуль. А исходники примеров и тулзов тоже нужно рассовать по подмодулями — чтобы пользователь мог их по желанию подтягивать в репу библиотеки.
Причём, так как у примеров и тулзов есть зависимости от библиотеки cpprt, саму библиотеку (внимание!) нужно тоже встроить в эти подрепозитории как подмодуль. Примеры ведь, должны показывать как полагается встраивать библиотеку в проекты…
Таким образом, на основании такой еретической теории, я сшил репозиторий, словно чудище Франкенштейна, из нескольких мини-репозиториев, соединённых воедино механизмом git. Это было настоящее трешище (можете полюбоваться здесь). И делалось всё это, если отбросить отговорки, с единственной целью – лишь бы не учить CMake.
Но совесть не телевизор, ей звук не выключишь. Натыкаясь на случайные материалы в процессе свободного поиска, я постепенно проникался осознанием: CMake — один из столпов современного мира открытого кроссплатформенного ПО для С++. Делать подобный проект без использования CMake – полная ерунда. Это совсем неправильно, а рассказывать людям о таком решении в статье, помимо прочего, означает учить людей дурному.
Поэтому во вторник, двенадцатого числа месяца апреля, я засел за изучение CMake. К вечеру следующего дня я уже имел работающую CMake-конфигурацию проекта и смеялся над своими страхами. Это оказалось проще, чем я ожидал и очень даже удобно.
Излагаю дальше хронику моего погружения в CMake. Надеюсь, кому-нибудь это поможет.
Вначале давайте вспомним, в чём заключается одно из серьёзных отличий С++ от языков, имеющих дело с байткодом (Java, C#, Python, и т.д.)? Верно, С++ собирается в платформозависимые бинарники (здесь платформа = операционная система). Это, с одной стороны, даёт возможность выполнять более тонкие оптимизации кода и делает код, собираемый из плюсов, очень эффективным.
Но, с другой стороны, платформозависимость означает наличие своих тулчейнов (тулчейн = компилятор + линковщик + дебаггер + ещё какие-то утилиты) для каждой из платформ. А так как стандарта, задающего принципы конфигурирования тулчейнов для сборки исходников C++ нет, при создании кроссплатформенного кода возникает необходимость задавать специфические конфигурации сборки проекта для каждого тучлейна и IDE. Как, звучит не очень страшно? Давайте рассмотрим эту ситуацию на реальном примере.
Вот есть автор кроссплатформенной библиотеки с открытым исходным кодом. Он хочет выпустить своё творение в мир, наполненный жутким хаосом, и хочет угодить всем потенциальным пользователям его библиотеки, дав им возможность просто и без лишних настроек собрать как библиотеку, так и всякие сопроводительные проекты.
Допустим, наш автор библиотеки хочет дать возможность собирать библиотеку через Visual Studio. Он добавляет в репозиторий библиотеки соответствующий солюшен, добавляет проекты для сборки тестов, примеров и тулзов, настраивает всю эту красоту – всё отлично, библиотека собирается через студию, тесты-примеры-тулзы тоже.
Но вот незадача – у некоторых пользователей его библиотеки стоит MinGW, который запускается через Eclipse IDE и требует совершенно другого конфигурирования для сборки. Исходники те же, принципы их сборки те же — но задавать их нужно в рамках другой системы, со своими правилами их описания. И пользователи MinGW+Eclipse недовольны, они не понимают, почему для них конфигурацию сборки не предоставили. Разработчик библиотеки, вздохнув, добавляет файлы проектов и для Eclipse IDE, удовлетворяя таким образом имеющийся запрос… Однако теперь негодуют фанаты NMake с его системой сборки. Нужно и для них конфигурацию писать… А также ещё для нескольких прочих тулчейнов и IDE. Куда ни ткнись – везде напорись.
И самое жуткое, что в случае любых изменений в проекте — добавления или удаления файлов, изменения настроек сборки — нужно вручную менять все эти конфиги и проекты IDE. Нехорошо разработчику библиотеки, очень даже грустно ему становится.
Но, к счастью, есть CMake! Достаточно создать в корне репозитория файл CMakeLists.txt, описать в этом файле с помощью языка CMake универсальную конфгиурацию сборки проекта – и после этого любой пользователь, у которого установлена утилита CMake.exe, сможет сам легко и просто сгенерировать конфиги для конкретной используемой им системы сборки кода (либо файлы проектов для IDE), а дальше выполнить нужную сборку в рамках используемой им любимой билд-системы/IDE самостоятельно. Все счастливы, ура!
Вот зачем нужен CMake. Во имя всеобщего удобства сборки.
P.S.: Что круто, концепция CMake подразумевает не только генерацию конфигов, но и их автоматическое обновление при изменении файла CMakeLists.txt. Это может быть очень полезно при работе с часто обновляемыми репозиторием. Достаточно забрать изменения, после чего не надо ничего настраивать: достаточно просто запустить сборку проекта в рамках вашего тулчейна (или в вашей IDE). CMake самостоятельно обновит конфигурацию перед стартом непосредственно сборки.
На последок — несколько необязательных замечаний. Прячу их под спойлеры.
Эта история распространённого забивания гвоздей микроскопами напомнила мне известную грустную байку о мужике, который изобрёл длинные пакетики с сахаром чтобы было удобнее чай в чашку сыпать, но никто не использовал эти пакетики правильно и мужик от этого покончил с собой.
Помните, каждый раз, когда вы игнорируете возможность использования CMake в открытом ПО для С++, в мире плачет один автор библиотеки. Используйте инструменты правильно.
CMake — это не система сборки. CMake над системами сборки. Он ими правит.
Мёду-то сколько, мёду, без единой молекулы дёгтя… Но минусы тоже есть, конечно, и о них мы поговорим на практике.
Думаю, теперь, когда вы знаете что такое CMake, зачем CMake и насколько CMake это хорошо — пришло время с ним познакомиться. Итак, хроника…
Все события, изложенные ниже, действительно имели место двенадцатого апреля 2016 года, во вторник. Восстановлено по истории запросов страниц в моём браузере.
(0:35) Первичный поиск материалов
С чего мы обычно начинаем? Верно, начинаем мы с поиска уроков.
По запросу "cmake tutorial" первая же ссылка вела на официальную доку. В основном я разбирался по ней. Так как cpprt — проект пока очень небольшой, понимания первых пяти шагов доки хватило для описания конфигураций всех возможных целей сборки библиотеки (включая сборки тулзов и примеров).
По запросу "CMake habrahabr" на первой странице Google нашлись три статьи, задуманные как обучающие. Вот эта статья понравилась мне больше всего (кстати, она же выпадает второй по запросу «cmake tutorial»). Хорошая обучающая статья, в чём-то смахивающая местами на перевод доки – но более лаконичная. Разбита на разделы, в каждом из которых рассказывается о возможностях CMake и даются простые примеры по делу, без лишней шелухи. Я использовал эту статью в качестве дополнительного обучающего материала.
2. Небольшой разбор CMake-сборки библиотеки LZ4 (относительно небольшого проекта с GitHub). Возможно, для имевших опыт с CMake разбор весьма неплох – но меня, как совсем-совсем новичка в этом деле, он отпугнул небольшим объёмом комментариев при большом объёме кода и обилии каких-то специфических для LZ4 переменных.
3. Хороший вводный урок.
Из ссылок, которые нашёл уже во время сёрфа по хабру, весьма позабавила вот эта упоротая статья: стрелочные часы на CMake . Из кода не понял ничего — но круто в той же степени, в которой странно.
(2:00) Сборка первой CMake-конфигурации
К CMake я вернулся к двум часам ночи. Коротко ознакомился с уроками и решил потренироваться на каком-нибудь тестовом, максимально простом проекте. Я создал папку (пусть она дальше называется {cmake_test}) со следующим содержимым:
main.cpp
Тестовый файл, для сборки которого мы будем генерировать конфигурации.
#include <iostream>
int main() {
std::cout << "Hello from CMake!" << std::endl;
return 0;
}
CMakeLists.txt
Файл с описанием конфигурации сборки исходника main.cpp. Тут и дальше к каждой новой команде CMake я даю развёрнутый комментарий.
Данная команда задаёт минимальную версию CMake, которая может собирать данную конфигурацию. Используется, чтобы сразу сказать пользователю, чтобы он обновил CMake, если его версия слишком старая.
Так как это первая команда CMake, с которой мы встречаемся, обращу внимание на то, как тут принято передавать аргументы. Разделителем между аргументами служат whitespace-ы (пробелы, табы, символы новой строки). При этом в CMake бывают именованные аргументы. Так, например, в представленном вызове «VERSION 2.8.8» — это передача значения 2.8.8 для аргумента именованного аргумента VERSION команды cmake_minimum_required. В вызове следующей команды (project) аргумент передаётся без имени.
Честно говоря, я не очень понял принцип, по которому CMake знает, является ли переданное значение аргументом или это значение аргумента. Не понял этого я даже после прочтения официальной доки по поводу синтаксиса вызова команд… Впрочем, как мне кажется, в 95% случаев без этого можно жить.
cmake_minimum_required(VERSION 2.8.8)
Данная команда задаёт информацию о проекте CMake. Это не то, что обычно называется проектом для IDE. Имеется в виду то, что находится в проектной иерархии на уровень выше. В Visual Studio это называют решением (solution), а в Eclipse – рабочим пространством (workspace).
В рамках проекта CMake могут быть заданы конфигурации для сборки нескольких библиотек и исполняемых файлов, а также правила их связывания друг с другом.
# В данном случае мы задаём имя проекту test_project
project(test_project)
Добавляет в проект цель для сборки исполняемого файла. Целями для сборки (build target) в CMake могут быть исполняемые файлы и библиотеки (детальнее про то, что такое цель для сборки – в начале раздела доки про систему сборки CMake).
Первый аргумент – имя цели сборки, после чего перечисляются пути к файлам исходного кода, из которых будет собираться исполняемый файл.
Для более тонкой настройки есть ряд дополнительных аргументов, можете почитать про этом на официальной доке подробнее. Для простого проекта указанной конфигурации достаточно.
# В данном случае мы добавляем на сборку один исходник – main.cpp
add_executable(test_executable main.cpp)
/build
Пустая папка. В неё мы будем создавать конфигурационные файлы с помощью команды CMake.
После создания всех этих тестовых файлов, я открыл консоль и сделал следующее:
cd {cmake_test}/build
Перешёл в папку, в которой планировал генерировать конфигурацию сборки.
cmake -G «MinGW Makefiles» ../
Вызов для генерирования конфигурационных файлов сборки через тулчейн MinGW.
-G «MinGW Makefiles» — выбор генератора для создания файлов конфигурации. Генератор – программа, формирующая файлы конфигурации для конкретного тулчейна (или IDE) из абстрактной CMake-конфигурации.
В данном случае, я хотел получить make-файл для тулзы MinGW32-make, идущей в поставке тулчейна MinGW.
../ – путь к папке, в которой лежит CMakeLists.txt с описанием конфигурации.
После вызова описанной команды, в папке, из которой осуществляется данный вызов, должны были появиться файлы конфигурации для сборки проекта. В данном случае, ожидалась конфигурация сборки одного исполняемого файла из исходника main.cpp.
Я сделал этот вызов в 2:25 ночи. Время восстановлено исходя из истории запросов, а именно исходя из времени первого запроса по поводу следующих возникших проблем.
(2:25) Проблемы
Ничего не собралось. CMake выдал следующую ошибку (оставляю только ту часть сообщения, которая касается ошибки, и прячу свои пути):
CMake Error at {my cmake_path}/share/cmake-2.8/Modules/CMakeMinGWFindMake.cmake:20 (MESSAGE): sh.exe was found in your PATH, here: {some path}
Run cmake from a shell that does not have sh.exe in your PATH.
Я погуглил по поводу и нашёл вот такую информацию. В двух словах: если в системных путях доступен sh.exe, то генератор для MinGW не будет работать. Почему – я так и не понял до конца, и в полтретьего ночи не особо хотелось разбираться, особенно с учётом того, что в моём случае исправить данную проблему можно было без особого труда.
Я временно убрал системный путь, из которого добавлялся sh.exe (в моём случае это был git), снова запустил CMake и… снова получил ошибку, но уже другую. Новая ошибка выглядела куда более удручающе (оставляю только ту часть сообщения, которая касается ошибки):
Building C object CMakeFiles/cmTC_ebbab.dir/testCCompiler.c.obj
C:MinGWbingcc.exe -o CMakeFilescmTC_ebbab.dirtestCCompiler.c.obj -c C:UserstestDesktopтмпcpprt_FINALcurrentgithubbuildCMakeFilesCMakeTmptestCCompiler.c
gcc.exe: error: C:UserstestDesktopтмпcpprt_FINALcurrentgithubbuildCMakeFilesCMakeTmptestCCompiler.c: No such file or directory
Снова почитав разные источники, я понял, что непосредственно данная ошибка возникала по следующим причинам. Перед формированием файлов конфигурации для конкретного тулчейна, CMake выполняет проверку наличия компонентов этого тулчейна (компилятора, линковщика, и т.д.), а также проверяет корректно ли эти компоненты работают. Для этого он создаёт во временной папке исходник testCCompiler.c и пробует его собрать с помощью компилятора. И в моём случае CMake почему-то не создавал такой файл.
Увы, я не могу дать конкретной ссылки по этому поводу, вот некоторые отголоски этого механизма.
Вообще, сама ошибка-то, конечно, возникала по изложенным выше причинам. Но было понятно, что это следствие, а не причина. Чтобы найти причину, я гуглил минут сорок, до начала четвёртого ночи. Это было типичное зависалово в духе «на мужика»: сделать, чтобы этот грёбанный код работал, наконец, и потом уже пойти спать… Оно меня победило. Я сдался первым. Вот одна из самых разумных ссылок, которые я успел нарыть к полчетвёртого ночи.
Решение проблемы: В чём было дело выяснилось, уже в процессе написании данной статьи. Дело в том, что путь к папке, в которую выполнялась сборка, содержал русские символы. Стоило выполнить сборку в папку, путь к которой не обладал подобным изъяном, как сборка make-файла для MinGW прошла успешно.
Вывод: Берегитесь путей с юникодом! Если что-то не работает и не знаете почему – посмотрите, нет ли юникода в путях ваших и попробуйте сделать пути без юникода!
(10:38) Сборка для Visual Studio
Проснулся я с уверенностью, что больше не желаю долбаться со сборкой для MinGW. У меня была установлена Visual Studio, так почему бы не попробовать в начале собрать конфиги для неё (точнее, в случае со студией, собирать solution и projects), а потом потестировать генерацию конфигов для других тулчейнов.
Я открыл консоль, перешёл в папку {cmake_test}/build и вызвал CMake, указав другой генератор:
cmake -G «Visual Studio 14 2015» ../
О чудо! В папке build образовался солюшн test_project.sln, несколько проектов (вот тут я немного удивился – почему несколько) и ещё ворох всяких вспомогательных файлов, нужных для того, как я понял, чтобы CMake мог обновлять настройки солюшена и проекта в случае изменения конфигурации, без необходимости каждый раз заново генерировать конфиги.
Я открыл солюшен. Да, мне не показалось. Помимо ожидаемого проекта test_executable.vcxproj для сборки main.cpp в исполянемый файл, в солюшене лежали ещё два каких-то левых проекта: ALL_BUILD.vcxproj и ZERO_CHECK.vcxproj. Я погуглил по поводу. Нашёл вот этот ответ на stack overflow. Из него я понял, что это как раз те самые проекты, с помощью которых CMake обновляет файлы проектов перед каждой сборкой в случае, если поменялся файл CMakeLists.txt, из которого эти файлы проектов были порождены. В общем, всё правильно, они и должны были создаться.
Например, если вы в проекте описываете include paths через CMake, он построит абсолютный путь к переданной папке, даже если в конфигурации CMake вы указали путь относительным. И это касается всех путей для всех конфигураций сборки, включая путей к папкам, в которые записывают результаты сборки (build output directort)!
Из-за описанных особенностей генерации путей, становится невозможным сохранять генерированные CMake конфигурации и файлы проектов для IDE в репозиториях, ведь абсолютные пути задают привязку к конкретным путям какого-то одного пользователя.
Я пока не нашёл как можно решить данную проблему. Нашёл опцию CMAKE_USE_RELATIVE_PATHS, но в официальной доке к ней есть приписка «May not work!» и, чёрт возьми, как обычно официальная дока не врёт. It's not work!
В этом старом обсуждении говориться, что возможность использовать относительные пути не характерна для CMake и потому её нет и не предвидеться, а в чуть более свежем обсуждении на форуме игрового движка ogre3d, вот в этом ответе предлагается один-в-один то же решение, что пытался использовать я и что мне, что форумчанам с ogre3d.
В скажу, что я удивлён тому, насколько данная проблема слабо освещена в интернете. На мой взгляд, это весьма чувствительная проблема. Опция генерации конфигураций, не зависимых от cmake через cmake, мой взгляд, аналогична опции import репозитория для систем контроля версий. Она должна быть.
Может, я просто плохо искал, или искал как-нибудь не так? Если вы как-нибудь решили для себя данную проблему, расскажите в комментариях как именно вы это сделали. Я переделаю эти спойлеры в отдельный раздел данной статьи, а вас укажу как автора этого раздела.
В Visual Studio я выбрал проект test_executable как startup project и нажал ctrl+f5. В консоль напечаталось: «Hello from CMake!». Урашечки ура!
С начала работы прошло около часа.
(11:24) Подготовка репозитория для сборки через CMake
Теперь можно было попробовать подключить CMake к основному проекту. До CMake, как я уже рассказывал, мой проект имел адскую организацию на подмодулях. С учётом перспектив, которые открывались благодаря CMake, от использования подмодулей можно было отказаться. Я сделал отдельный клон репозитория, забрал исходники всех подмодулей и начал думать, как буду настраивать сборку с помощью CMake.
Я слегка перестроил репозиторий, отчего он стал выглядеть по-человечески, как виденные мною «взрослые» репозитории. Вот часть структуры проекта, которая касается непосредственно работы с CMake:
/build — пустая папка, в которой полагается собирать исходники.
/include — папка с хедерами для доступа к API библиотеки.
/src — папка с исходным кодом библиотеки.
/examples — папка с исходным кодом разных примеров.
-/__example_hierarchies__ — папка с тестовыми иерархиями классов (см. дальше).
-/simple_examples — папка с исходниками небольшого тестового примера.
/tools — папка с инструментами для работы с проектом.
-/console — в данный момент есть только одна тулза, и имя ей консоль.
CMakeLists.txt — CMake-файл, с помощью которого можно собрать разные элементы проекта.
Текущий репозиторий библиотеки
То, что излагается в данной статье, верно для коммита e9c34bb.
Я решил, что в рамках поставки библиотеки можно будет собирать следующие штуки:
Чёрный цвет для библиотеки.
Синий цвет для исполняемых файлов.
Зелёный цвет для папок с исходниками, шареных между целями сборки.
библиотека
1. cpprt – собственно, сама библиотека. Собирается из исходного кода, который находится в папках /src и из интерфейса, который лежит в папке /include.
Собирается в статическую библиотеку, зависимостей не имеет.
пример
2. simple_examples – небольшой пример, состоящий из единственного файла, в котором описывается классическая для ознакомления с ООП иерархия классов животных и на базе этой иерархии демонстрируется использование API библиотеки.
Собирается в исполняемый файл, зависит от библиотеки cpprt.
инструмент
3. console – в данный момент это, фактически, тоже пример демонстрации возможностей библиотеки, но в планах – превратить его в самостоятельную тулзу.
Через консоль можно просматривать информацию о деревьях наследования регистрированных классов, а также создавать и удалять объекты классов по строковым именам как объектов, так и классов.
Собирается в исполняемый файл, зависит от библиотеки cpprt и от тестовой иерархии классов, на примере которой можно посмотреть работу консоли.
Структура была готова. Оставалось «всего ничего» – описать конфигурации для сборки этих артефактов с помощью CMake.
(11:30) Сборка библиотеки cpprt
Несмотря на то, что библиотека cpprt содержала всего два файла, я решил поработать на перспективу и сразу узнать, как подключать все файлы, содержащиеся в какой-либо папке рекурсивно (не буду же я перечислять десятки путей к исходникам когда их станет больше). Загуглил и нашёл вот эту ссылку, в которой рассказывалось про использование команды file с аргументом GLOB_RECURSE.
Я ещё мельком глянул CMake-конфигурацию небольшого проекта Project-OSRM и написал следующий конфигурационный код с использованием найденной информации:
Данная команда предназначена для установки или доустановки значений переменных. Первый аргумент – имя переменной. Потом задаётся её значение. В данном случае мы передаём путь ".", что, как обычно, означает текущую папку – то есть папку, в которой находится CMakeLists.txt (в данном случае – корень репозитория).
Примечание: Можно ли передавать переменные через командную строку во время вызова CMake? Можно, если задавать переменную с передачей CACHE (вот ответ со stack overflow по поводу). Выглядит громоздко — поэтому, на мой взгляд, передавать флаги из командной строки лучше через команду option (о ней будет дальше).
set(CPPRT_ROOT_PATH .)
# Задаём значение корневой папки для исходников библиотеки.
CMake использует распространённый механизм доступа к значениям переменных. Как сказано в доке, CMake подставляет значение переменной прямо в место, где упоминается ссылка на переменную (либо вставляет пустую строку, если переменная с таким именем не была определена). За счёт этого можно использовать подобные вставки в текстовые переменные (Quoted Argument) и даже в другие ссылки на переменные. Пример из доки:
${outer_${inner_variable}_variable}
Как я понял, здесь значение переменной inner_variable выступает частью имени другой переменной. Чудеса, до и только.
set(CPPRT_SOURCES_PATH ${CPPRT_ROOT_PATH}/src)
# Задаём значение корневой папки для интерфейса библиотеки.
set(CPPRT_INCLUDE_PATH ${CPPRT_ROOT_PATH}/include)
Я тут расскажу только об использовании команды file с аргументом GLOB_RECURSE. Такая команда выполняет рекурсивный поиск путей файлов и папок, подчиняющихся задаваемым в вызове команды правилам поиска. Правила задаются аргументами, следующими за именем переменной, в которую запишется список найденных путей.
Наверно, есть ещё хитрые правила – но для моих целей хватило правила, задающего принцип поиска по расширению файлов. Для задания подобных правил нужно указать путь к папке, начиная с которой выполнять рекурсивный поиск файлов по данной и вложенным папкам, и указать расширение файлов, пути к которым мы ищем, оставив вместо имени файлов звёздочку (так, как это принято в большинстве операционных систем).
Команду file можно вызывать не только с опцией GLOB_RECURSE, но и просто с GLOB. Такой вызов отличается лишь тем, что не будет выполнять поиск по вложенным папкам.
Замечание №1: При генерировании конфигурации, CMake может добавлять некоторые свои исходные файлы в рамках проекта. При неосторожном использовании команды file GLOB_RECURSE (например, если искать с её помощью исходники, начиная прямо с корня репозитория) могут возникать некоторые проблемы. С одной из них я тоже столкнулся (вот тут рассказывается о подобной проблеме).
Замечание №2: Вообще, опытные пользователи CMake не советуют ни file GLOB, ни file GLOB_RECURSE. Вот тут объясняется причина. Использование этих команд приводит к необходимости обновлять генерируемые конфигурации (либо солюшены IDE) через CMake вручную при добавлении/удалении файлов.
А теперь представим, что мы добавили файл в папку, информацию из которой CMake собирает с помощью команды file GLOB или с помощью команды GLOB_RECURSE. Проблема в том, что при этом файл CMakeLists.txt изменён не будет и при сборке обновление конфигураций (солюшенов) не выполнится автоматически. Неудобно.
Для моих скромных задач данная проблема пока не актуальна, но когда разберусь в ней – добавлю ссылку на более приемлемое решение данной проблемы.
# Здесь мы вызываем команду для получения путей ко всем исходникам
# библиотеки. В переменной CPPRT_SOURCES будет храниться список
# полученных путей.
file(GLOB_RECURSE CPPRT_SOURCES
${CPPRT_SOURCES_PATH}/*.h
${CPPRT_SOURCES_PATH}/*.cpp
)
# Собираем пути к хедерам, которые входят в интерфейс библиотеки.
# Список путей к хедерам будет храниться в переменной CPPRT_HEADERS.
file(GLOB_RECURSE CPPRT_HEADERS
${CPPRT_INCLUDE_PATH}/*.h
)
Мы уже добавляли исполняемый файл в качестве цели сборки. Библиотеки как цели сборки добавляются аналогичным образом. Единственная разница – до перечисления путей к исходникам библиотеки указывается тип библиотеки, в которую собирается данная цель сборки: STATIC (для сборки в статическую библиотеку) или DYNAMIC (соответственно, для сборки в динамическую библиотеку).
# Здесь мы добавляем в проект цель для сборки статической библиотеки cpprt.
add_library(cpprt STATIC ${CPPRT_SOURCES} ${CPPRT_HEADERS})
После того, как файл был готов, я перешёл в папку /build и собрал файлы конфигурации:
cmake -G «Visual Studio 14 2015» ../
Всё собралось сразу: появился солюшн с проектом cpprt.vcxproj. Я открыл солюшн и запустил сборку через студию… Да. Библиотека собралась без лишних проблем.
Это случилось в 12:41, о чём я могу говорить исходя из времени времени, в которое открыл ссылку на фейсбучек.
После фейсбучика я временно переключился на другие задачи.
(16:09) Сборка примера simple_examples
К настройке cmake я вернулся через два с половиной часа. Теперь предстояло сделать возможной генерацию конфигов для сборки примера к библиотеке. Для этого было нужно разобраться с организацией зависимостей между проектами – ведь проект simple_examples статически линковался с библиотекой cpprt:
Команды для линковки статической библиотеки упоминались в уроках CMake на хабре (второй пример). Почитав эти уроки и ещё несколько источников, я написал следующий конфигурационный код:
# Head
#
cmake_minimum_required(VERSION 2.8.8)
Данная команда задаёт значение переменной-флажка.
Первый аргумент – название переменной, для которой задаётся значение.
Вторым аргументом можно передать строковое имя флага — я пока не очень понял, как его можно использовать. Думал, можно как-то распечатать все доступные в рамках CMakeLists.txt опции с красивыми объяснениями их назначения, поискал, но не нашёл подобного встроенного в CMake решения, только разные велосипеды.
Третий аргумент – значение флага по умолчанию. Значение может быть установлено пользователем при вызове генерации конфигов из командой строки.
Данная команда удобна тем, что во время вызова генерации конфигов (вызов cmake -G и т.д. по тексту) значения опций можно передавать как аргумент командной строки (прочитал про это вот тут). Передавать его можно вот так:
-D<имя опции>=ON/OFF
Примечание: Как писалось в комментарии к команде set выше, значения из командной строки можно задавать через CACHE-переменные. Но, на мой взгляд, команда option выглядит более изящно и поэтому считаю её использование для передачи булевых значений предпочтительнее.
# Здесь мы задаём флаг, хранящий информацию о том,
# нужно ли генерировать конфиги для сборки примеров
option(BUILD_EXAMPLES «Build cpprt examples» OFF)
#-----------------------------------------------------------------------------------------------
# Сборка библиотеки cpprt. Этот код уже был подробно разобран выше.
project(cpprt_projects)
set(CPPRT_ROOT_PATH .)
set(CPPRT_SOURCES_PATH ${CPPRT_ROOT_PATH}/src)
set(CPPRT_INCLUDE_PATH ${CPPRT_ROOT_PATH}/include)
file(GLOB_RECURSE CPPRT_SOURCES
${CPPRT_SOURCES_PATH}/*.h
${CPPRT_SOURCES_PATH}/*.cpp
)
file(GLOB_RECURSE CPPRT_HEADERS
${CPPRT_INCLUDE_PATH}/*.h
)
include_directories(${CPPRT_INCLUDE_PATH})
add_library(cpprt STATIC ${CPPRT_SOURCES} ${CPPRT_HEADERS})
#-----------------------------------------------------------------------------------------------
###- Examples
#
Данная команда добавляет пути для поиска хедеров (оно же inlcude paths) для всех целей сборки в рамках текущего файла конфигурации CMakeLists.txt.
Данную команду можно вызывать несколько раз, добавляя новые пути – то есть она не устанавливает, она именно добавляет пути. Можно даже выбирать куда именно добавлять новые пути – в конец или в начало списка путей, передачей опций BEFORE и AFTER в команду.
Замечание: В процессе поиска информации про данную команду, я наткнулся ещё вот на этот ответ. В нём говорится, что если не хочется добавлять пути поиска хедеров для всех целей сборки проекта, можно использовать команду target_include_directories, которая добавляет пути для поиска хедеров только для указанной цели сборки. Ниже мы ещё поговорим про данную команду.
include_directories(${CPPRT_INCLUDE_PATH})
С этой командой всё очевидно – она нужна для реализации ветвления. Думаю, с неё и так всё ясно.
Замечание: Передавать условие проверки в команды else() и endif(), как это сделано у меня, не обязательно (официальная документация: «Note that the expression in the else and endif clause is optional»), но, как я понял, считается хорошим тоном передавать простые условия в эти команды, чтобы легче было читать код. Это вроде того, как принято добавлять комментарии к закрывающим макросные проверки #endif в плюсах.
# Если при сборке опция BUILD_EXAMPLES была выставлена
# значением ON – выполняем сборку примера.
if(BUILD_EXAMPLES)
#- — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - -
### — Simple examples
# Сохраняем путь к папке с примерами.
set(SIMPLE_EXAMPLE_ROOT ${CPPRT_ROOT_PATH}/examples/simple_examples)
# Добавляем таргет сборки тестового проекта.
# Пример должен собираться в исполняемый файл.
add_executable(simple_examples
${SIMPLE_EXAMPLE_ROOT}/SimpleExamples.cpp
${CPPRT_SOURCES}
)
Вызов данной команды задаёт правила линковки статических библиотек к цели сборки. Цель сборки, к которой линкуются библиотеки, передаётся первым аргументом. Дальше передаются цели сборки для библиотек. Данную команду можно вызывать несколько раз для одной и той же цели сборки – все передаваемые статические библиотеки будут добавлены в список в порядке вызовов команд target_link_libraries (из доки: «Repeated calls for the same {target} append items in the order called»).
Примечание: В доке сказано, что можно также передавать флаги для линковки библиотек, уже собранных вне CMake. Сам такого не делал. Когда доберусь до данного вопроса, оставлю тут ссылку.
target_link_libraries(simple_examples cpprt)
endif(BUILD_EXAMPLES)
#========================================================
Я попробовал сгенерировать решение студии из конфигов CMake: вначале с передачей опции для сборки примеров, потом без неё. Например, вот какой командой генерировал солюшен для студии вместе с проектом примера:
cmake -G «Visual Studio 14 2015» -DBUILD_EXAMPLES=ON ../
Когда генерация с примером прошла, я открыл решение и хотел запустить пример – но получил сообщение о том, что для проекта нет собранного исполняемого файла. При этом не было ни одной ошибки.
К счастью, всё было совсем просто – я забыл выставить проект примера в качестве startup project (CMake после сборки по умолчанию выставляет BUILD_ALL, в рамках которого не собирается ни одного исполняемого файла). После того, как я всё выставил, на экране напечаталось всё что полагалось для теста.
Это случилось, как я смог восстановить по истории, к 17:14 – сужу по тому, во сколько я открыл проект FastDelegate (это одна хорошая микробиблиотечка для работы с коллбеками в плюсах). Я её использовал для своего основного проекта и захотелось глянуть как там билд-система сделана. Выяснилось, что никак… Впрочем, возможно, и правильно.
(17:14) Сборка тулзы-примера console
Оставалось собрать консоль. В принципе, все команды, нужные для этой сборки, я уже знал. Вспомним структуру проекта:
Консоль может собираться с тестовыми иерархиями классов. Тут под понятием «собирается с иерархиями классов» подразумевается включение исходников из папки репозитория /examples/__example_hierarchies__ в перечень исходников, из которых собирается консоль (не как header search paths, а именно как ещё один источник исходников).
Конфигурация с описанием сборки консоли. Привожу конфигураций без описанных выше конфигураций для сборки примеров, чтобы не засорять код:
# Head
#
cmake_minimum_required(VERSION 2.8.8)
# Я решил что генерирование конфигов для сборки тулзы-примера
# console можно конфигурировать двумя опциями:
# Нужно ли вообще генерировать конфиги для сборки консоли.
option(BUILD_TOOLS «Build cpprt tools» OFF)
# Нужно ли добавлять к компилируемым с консолью файлам
# тестовые иерархии классов
option(CONSOLE_WITH_EXAMPLE «Add examples to console» ON)
#-----------------------------------------------------------------------------------------------
### — cpprt code
#
project(cpprt_projects)
set(CPPRT_ROOT_PATH .)
set(CPPRT_SOURCES_PATH ${CPPRT_ROOT_PATH}/src)
set(CPPRT_INCLUDE_PATH ${CPPRT_ROOT_PATH}/include)
file(GLOB_RECURSE CPPRT_SOURCES
${CPPRT_SOURCES_PATH}/*.h
${CPPRT_SOURCES_PATH}/*.cpp
)
file(GLOB_RECURSE CPPRT_HEADERS
${CPPRT_INCLUDE_PATH}/*.h
)
include_directories(${CPPRT_INCLUDE_PATH})
add_library(cpprt STATIC ${CPPRT_SOURCES} ${CPPRT_HEADERS})
#- — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - -
# Тут мы собираем пути ко всем файлам, которые находятся в
# папке __example_hierarchies__. Комментарии, думаю, не требуются.
set(EXAMPLE_HIERARCHIES_ROOT
${CPPRT_ROOT_PATH}/examples/__example_hierarchies__
)
file(GLOB_RECURSE EXAMPLE_HIERARCHY_PATHES
${EXAMPLE_HIERARCHIES_ROOT}/*.h
${EXAMPLE_HIERARCHIES_ROOT}/*.cpp
)
#-----------------------------------------------------------------------------------------------
### Тут мы будем собирать консоль
if(BUILD_TOOLS)
# Путь к папке с инструментами – пригодится на будущее,
# когда инструментов будет больше одного.
set(TOOLS_ROOT ${CPPRT_ROOT_PATH}/tools)
#- — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - -
# Путь к исходному коду самой консоли
set(CONSOLE_ROOT ${TOOLS_ROOT}/console)
# Если мы собираем код с тестовой иерархией, то:
if (CONSOLE_WITH_EXAMPLE)
# Добавляем цель сборки для консоли
add_executable(cpprt_console
${CONSOLE_ROOT}/CPPRTConsole.cpp
${EXAMPLE_HIERARCHY_PATHES}
)
# Чтобы было удобнее инклудить хедеры тестовой иерархии –
# добавляем её в пути для поиска хедеров (include path).
# Так как данный путь поиска хедеров используется только
# консолью, чтобы не засорять общие для всех проектов
# системные пути, я использую target_include_directories
# (вот ссылка, по которой я разобрался с этой командой)
target_include_directories(cpprt
PUBLIC
${EXAMPLE_HIERARCHIES_ROOT}
)
# Линкуем консоль с собранной библиотекой cpprt
target_link_libraries(cpprt_console cpprt)
else(CONSOLE_WITH_EXAMPLE)
# Без подключения папки с тестовыми иерархиями -
# всё аналогично сборке simple_example
add_executable(cpprt_console
${CONSOLE_ROOT}/CPPRTConsole.cpp
)
target_link_libraries(cpprt_console cpprt)
endif(CONSOLE_WITH_EXAMPLE)
endif(BUILD_TOOLS)
Консоль собралась и заработала сходу. Всё было готово.
К тому времени было (19:23) – сужу по времени, в которое я загуглил "мятный чай успокаивает".
Результат всей работы: конфиг, такой, какой сейчас используется в проекте:
# Head
#
cmake_minimum_required(VERSION 2.8.8)
option(BUILD_ALL «Build cpprt tools» OFF)
# Единственный новый трюк. Заслуживает комментария.
# Здесь в качестве значения по умолчанию для опции
# передаётся значение другой опции. За счёт этого можно
# задать одинаковые значения для целой группы опций из
# значения одной опции, а потом для некоторых отдельно
# уточнить это значение тоже из консоли.
option(BUILD_TOOLS «Build cpprt tools» ${BUILD_ALL})
option(CONSOLE_WITH_EXAMPLE «Add examples to console» ON)
option(BUILD_EXAMPLES «Build cpprt examples» ${BUILD_ALL})
#-----------------------------------------------------------------------------------------------
### — cpprt code
#
project(cpprt_projects)
set(CPPRT_ROOT_PATH .)
set(CPPRT_SOURCES_PATH ${CPPRT_ROOT_PATH}/src)
set(CPPRT_INCLUDE_PATH ${CPPRT_ROOT_PATH}/include)
file(GLOB_RECURSE CPPRT_SOURCES
${CPPRT_SOURCES_PATH}/*.h
${CPPRT_SOURCES_PATH}/*.cpp
)
file(GLOB_RECURSE CPPRT_HEADERS
${CPPRT_INCLUDE_PATH}/*.h
)
include_directories(${CPPRT_INCLUDE_PATH})
add_library(cpprt STATIC ${CPPRT_SOURCES} ${CPPRT_HEADERS})
#- — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — # Global examples setup
#
set(EXAMPLE_HIERARCHIES_ROOT ${CPPRT_ROOT_PATH}/examples/__example_hierarchies__)
file(GLOB_RECURSE EXAMPLE_HIERARCHY_PATHES
${EXAMPLE_HIERARCHIES_ROOT}/*.h
${EXAMPLE_HIERARCHIES_ROOT}/*.cpp
)
#-----------------------------------------------------------------------------------------------
### — Tools
#
if(BUILD_TOOLS)
set(TOOLS_ROOT ${CPPRT_ROOT_PATH}/tools)
#- — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — ### — Console project
set(CONSOLE_ROOT ${TOOLS_ROOT}/console)
if (CONSOLE_WITH_EXAMPLE)
add_executable(cpprt_console
${CONSOLE_ROOT}/CPPRTConsole.cpp
${EXAMPLE_HIERARCHY_PATHES}
)
target_include_directories(cpprt_console
PUBLIC
${EXAMPLE_HIERARCHIES_ROOT}
)
target_link_libraries(cpprt_console cpprt)
else(CONSOLE_WITH_EXAMPLE)
add_executable(cpprt_console
${CONSOLE_ROOT}/CPPRTConsole.cpp
)
target_link_libraries(cpprt_console cpprt)
endif(CONSOLE_WITH_EXAMPLE)
endif(BUILD_TOOLS)
#-----------------------------------------------------------------------------------------------
###- Examples
#
if(BUILD_EXAMPLES)
#- — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — - — ### — Simple examples
set(SIMPLE_EXAMPLE_ROOT examples/simple_examples)
include_directories(${CPPRT_INCLUDE_PATH})
add_executable(simple_examples
${SIMPLE_EXAMPLE_ROOT}/SimpleExamples.cpp
${CPPRT_SOURCES}
)
target_link_libraries(simple_examples cpprt)
endif(BUILD_EXAMPLES)
#============================================================
Таким образом, я легко и без особых нервов за четыре часа чистого времени разобрался на рабочем базовом уровне с утилитой CMake. По-моему, приобщение к мощными знаниям CMake стоят четырёх часов времени.
Заканчивая, выскажусь по поводу CMake:
Что мне понравилось:
Мощная утилита. Она действительно снимает необходимость париться с конфигурацией под кучу платформ и компиляторов. Я почувствовал это, когда после студии сгенерировал через CMake make-файл для mingw32-make + Eclipse IDE и собрал исходники через него так же легко, как до этого собирал то же самое через Visual Studio.
Что мне не понравилось:
Очень странный дизайн языка. Аргументы с whitespace в качестве разделителей, странный принцип использования именованных аргументов, не всегда прозрачный механизм конфигурирования свойств проектов и целей сборки (иногда свойства задаются глобально для всех целей сборки, иногда локально для конкретной), странная политика использования команд (например, команда file на самом деле сочетает в себе все операции с файловой системой, а конкретная операция передаётся с помощью аргумента команды).
В какие-то моменты мне казалось, что язык для CMake писали инопланетяне: очень умные, но мыслящие немного не по-нашему.
5. Про документацию
Скажу сразу –мне не хватило духу составить англоязычную документацию для библиотеки cpprt. Сейчас вся документация сводится к отсылке читать данный цикл статей, что, пожалуй, означает отсутствие документации. Я постараюсь исправить этот недочёт как только переведу дух после четырёхнедельного марафона, в который вылилось написание данного цикла статей.
Не смотря на то, что на практике я не написал доку, я провёл некоторые теоретические исследования по поводу того, на базе чего лучше всего делать документацию. Привожу здесь информацию, которую удалось найти. Информация в основном бралась из этой статьи и комментариев к ней:
Преимущества:
— Вики удобнее для редактирования и чтения дизайнерами, художниками, и прочими членами команды, которые не являются программистами.
— Вики задаёт свои стандарты формирования статей. За счёт этого документацию легче привести к единому виду.
— Сохраняется история правок и есть возможность комментирования статей.
— Вики «из коробки» даёт возможность поиска по статьям.
— Вики просто настраивать.
Недостатки:
— Трудности создания схем в вики. Я так понимаю, под схемами имеются в виду UML, генерируемые из текстовых файлов. В комментариях люди писали, что схемы нужно загружать в вики в виде отрендеренных картинок. Другие же упоминали плагины, которые позволяют описать схемы без этих сложностей.
— По утверждению некоторых людей, к вики неудобно приделывать роботов. Думаю, под роботами имеются в виду какие-то программы для анализа и/или генерации статей?
Преимущества:
— Удобный редактор.
— Доступ отовсюду, командная работа, хранение версий.
— Расшаривание (ридонли и с возможностью редактирования).
— Комментирование участков текста.
— Встроенные схемы (со всеми вышеперечисленными плюшками).
— Экспорт в doc, pdf или публикация в виде html.
Недостатки:
— Зависимость от сервисов goolge. sistemshik: «Для среднекрупных компаний это решение неприемлемо просто в силу факта хранения данных на серверах google».
— Некоторые трудности в привязывании роботов для работы с документами. Кроме того, как я уже писал, документация частично пишется роботами. Насколько просто прицепить роботов к googledocs?
На мой взгляд, о Doxygen весьма достойно рассказывается в этой статьей под авторством Norserium. Единственная претензия к автору: сделав хороший цикл статей, но не сделал между ними ссылки. Вот ссылка на последнюю статью цикла, в начале которой даются ссылки на предыдущие.
С учётом темы данной статьи, меня заинтересовал вот этот комментарий: «В корпоративной среде отдельную документацию сделать можно, но Doxygen прежде всего ориентирован на разработчиков свободного ПО, где несколько иные традиции. Одно дело вставить комментарии в код, и совсем другое — писать документацию. А ведь её ещё нужно поддерживать в актуальном состоянии»
Преимущества:
— Главное преимущество это, естественно, возможность писать документацию параллельно с кодом. Это подслащивает пилюлю необходимости документирования для программистов, которые, как правило, не любят писать доку — в коде писать доку как-то уютнее.
— Поддержка генерации диаграмм и latex-формул прямо из комментариев к коду.
Недостатки:
— Совмещение исходного кода и подробных комментариев для документации раздражает некоторых программистов.
— Стиль оформления документации по умолчанию некоторым кажется архаичным, и поменять его не очень просто.
Цитата автора приведённой выше статьи: «Документацию я веду преимущественно текстовую. <...> Я люблю текст. Скажу даже больше, я люблю plaintext. Он быстро набирается и достаточно выразителен». Судя по комментариям к статье, хранение доки в Plain text это экзотика и достаточно распространённый способ хранения простой документации.
Преимущества:
— Отсутствие зависимостей от каких бы то ни было систем рендера, хранения или генерации документации. Как следствие — простота добавления в проект и отсутствие необходимости настройки систем для ведения подобной документации.
— Повествование без картинок вынуждает авторов точнее выражать свои мысли.
Недостатки:
— Часто при документировании почти невозможно обойтись без диаграмм или хотя бы без систем ссылок. В Plain text всё это вставить, ясное дело, нельзя.
— Чтение документации в таком виде может быть не очень удобно непрограммистам.
Как вы, вероятно, поняли, после такого первичного ознакомления с темой документирования открытого ПО Doxygen оказался пока фаворитом. Вероятно, это связано с тем, что я программист, который любит писать код и не любит корпеть над всякими скучными документами.
Тем не менее, как по мне, одним Doxygen-ом хорошая документация не обойдётся. Это хорошее решение для ознакомления с API и, возможно, для отображения каких-то простых диаграмм. Но часто нужны более подробные статьи, в деталях описывающие поведение кода, с большими и сложными картинками и иллюстрациями помимо UML. Это всё равно придётся делать самому, ручками, одними комментариями к исходникам не обойтись.
По поводу документации мне пока больше нечего сказать. Пишите – дополню.
6. О продвижении библиотеки
Тут у меня пока нет никакого опыта, кроме данного цикла статей. Это и есть экспериментальная попытка продвижения библиотеки. Рассказать людям о своём опыте. Посмотрим, насколько это хороший подход.
Если на данную публикацию и на саму библиотеку будет положительная реакция читателей, я адаптирую данный цикл статей для англоязычной аудитории и попробую опубликовать где-нибудь у них (знаю, есть reddit… может, кто-нибудь знает ещё хорошие ресурсы?). А там посмотрим…
По поводу продвижения мне пока, пожалуй, больше нечего сказать.
7. Заключение
Надеюсь, мои изыскания, допущенные ошибки и, главное, замечательные комментарии других людей и внесённые благодаря им правки позволят тебе, уважаемый читатель, пройти этот путь скорее, чем это сделал я.
Я выкладываю статью перед публикацией на GitHub, после чего буду вносить изменения в статье на GitHub и когда будет накапливаться определённый массив изменений — переносить эти изменения в статью на хабре (такой себе релиз статьи). Если найдутся какие-нибудь ещё коммитеры помимо меня и статья, таким образом, обретёт коллективное авторство — я был бы не против передать статью в коллективное авторство про открытое ПО (есть такой на хабре?).
Статью на GitHub я выкладываю прямо сейчас, но всё остальное — пока только планы. Пишите насколько это будет вообще соответствовать формату хабры и если не соответствует — куда имеет смысл опубликовать статью для подобного принципа работы над ней?
Автор заранее благодарит читателей за указания на ошибки в статье, конструктивные советы и пожелания.
Автор: semenyakinVS