воскресенье, 9 февраля 2020 г.

[prog.c++] Вероятно, проблема управления зависимостями в C++ не настолько проста, как может показаться

За прошедшие две недели довелось порешать несколько разных задачек и еще раз задуматься о некоторых вещах, связанных с разработкой на C++. Одна из этих вещей -- это подтягивание зависимостей в C++ проект.

Бытует вполне обоснованное мнение, что у современного C++ есть две большие и, по-моему, взаимоувязанные проблемы, касающиеся наполнения C++ проекта зависимостями.

Первая из этих проблем в том, что для C++ нет официальной стандартной системы сборки. Де-факто стандартом потихоньку становится уёбищный CMake. Но именно что становится, однако таковым еще не является. Поэтому запросто можно наткнуться на нужную тебе библиотеку, которая должна собираться древним make вкупе с autotools. Ну да ладно, этой проблемы сейчас касаться не будем. Просто напомним о ее существовании.

Вторая проблема -- это отсутствие де-факто стандартного механизма управления зависимостями для C++ проектов. И чтобы не лезть совсем уж в глухие дебри, проще поговорить о разработке некого прикладного софта под одну платформу. И, поскольку под Linux-ом у C++ вполне себе неплохие перспективы, то за основу возьмем именно Linux, а не Windows или macOS.

Как мне представляется, для некроссплатформенных разработок, заточенных под Linux, сейчас самими распространенными подходами являются: использование штатного менеджера зависимостей конкретного Linux-репозитория, Conan и vcpkg.

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

Поэтому остается вариант с использованием ортогональных к операционной системе менеджеров зависимостей типа Conan и vcpkg. Целью которых, как мне представляется, является снижение сложности управления зависимостей до уровня запуска одной единственной команды, вроде conan install или vcpkg install.

Что, безусловно, хорошо. Но, разве что для проектов уровня HelloWorld-а.

Т.е. если вам нужно быстренько набросать прототип, в котором нужно несколько сторонних библиотек, то Conan/vcpkg как раз то, что нужно. Ну вот, скажем, захотели вы попробовать RESTinio или SObjectizer, написали свою простенькую программку и воспользовались Conan/vcpkg дабы не тратить свое время на настройку зависимостей. Все хорошо.

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

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

Нам самим не выгодно внезапно оказаться с новыми версиями зависимостей. Скажем, мы заложились на X версии 1.2.3, через какое-то время выходит версия X 1.2.5 с какими-то исправлениями и дополнениями. И, вроде как это хорошо, но может оказаться, что X 1.2.5 поломало что-то в другом месте или же начала конфликтовать с Y версии 2.6.8. Или же в X 1.2.5 задействовали фичи языка, которых еще нет в компиляторе, к которому мы прикованы кабальными условиями заказчика.

В Conan-е можно фиксировать версии сторонних библиотек, тогда как vcpkg версионирования зависимостей не поддерживает. И может показаться, что в этом плане vcpkg полное говно.

Но vcpkg предлагает совсем другой подход к решению задачи версионирования, который показывает себя вполне привлекательным когда мы рассмотрим "во-вторых".

А, во-вторых, может оказаться, что нам не нужна "ванильная" версия сторонней библиотеки, а требуется какая-то ее модификация.

Например, в X версии 1.2.3 нет важной для нас фичи и мы сделали форк X в который и добавили новую функциональность. Функциональность, которая нам нужна здесь и сейчас, но которую не факт, что примут в X, а даже если и примут, то еще непонятно когда. Соответственно, нам нужно уметь одним легким движением руки заменить оригинальную X 1.2.3 на пропатченную нами X 1.2.3-our-critical-new-feature.

Другой пример: в X версии 1.2.3 мы обнаружили какую-то проблему и сообщили о ней разработчикам X. Те начали разбираться с ней и в отдельной ветки разработки X сделали, как им кажется, исправление этой проблемы. Но прежде чем выкатывать это исправление в master, они хотят, чтобы мы погоняли исправленную версию у себя. Соответственно, нам нужно добавить к себе в зависимости какую-то промежуточную версию X из какой-то bug-fix-branch.

В случае с vcpkg для этого есть решение -- мы клонируем vcpkg и правим в этом клоне portfiles так, как нам нужно. Запросто можно заменить оригинальный X 1.2.3 на X 1.2.3-our-critical-new-feature, либо же указать в portfile в качестве источника конкретный коммит из конкретной bug-fix-branch.

А вот в ситуации с Conan, как мне представляется, нам нужно будет поднимать собственный сервер Conan, на котором нам нужно будет публиковать свои варианты чужих библиотек. И для промежуточных версий X из bug-fix-branches нужно будет делать какие-то собственные "левые" релизы X.

В общем, если мы не делаем HelloWorld на выброс, а занимаемся разработкой, за которую мы будем нести ответственность в течении года и более, то это означает, что нам нужно будет изначально закладываться на поднятие собственного Conan-сервера, либо на ведение собственного форка vcpkg.

Что, боюсь, означает, что единый репозиторий зависимостей, вроде JavaScript-ового npm, Ruby-нового RubyGems или Rust-ового Crates, будет служить больше целям обеспечения низкого порога входа в C++ную экосистему, нежели для упрощения разработки коммерческого софта на C++.

Но кроме этих двух моментов есть и еще один...

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

BitBucket вот через несколько месяцев тупо удалит все Mercurial репозитории. Перестанут быть доступны не только репозитории с исходниками, но и все tarball-ы, которые размещались в Download-секциях.

Или, скажем, нашумевшие истории с авторами популярных проектов, которые в один прекрасный момент решили послать всё и всех куда подальше и удалить свой проект к херам. Полагаю, случай с leftpad и недавняя история с травлей автора Actix-Web благодарными Rust-оманами -- это хорошие примеры.

Да и нестабильность в мире так же нельзя не принимать во внимание. Сегодня github совершенно не против того, что на него ходят люди из какой-то (не)маленькой, (почему-то еще)независимой и гордой страны. А завтра США накладывают на эту страну (или ее часть) санкции и github вынужденно вводит блокировку доступа. И все, внезапно часть (все?) наших зависимостей может стать для нас недоступной.

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

При этом, опять же, выясняется, что системы управления зависимостями, в которых мы сами можем "опакечивать" чужие библиотеки, дают нам большую безопасность. Мне думается, что править время от времени portfiles в собственном форке vcpkg будет несколько проще и удобнее, чем держать собственный Conan-сервер с опубликованными там собственными версиями чужих библиотек. Но, может быть, это просто потому, что Conan-ом я, к счастью, не пользуюсь и мало что про него знаю.

Тем не менее, думаю, что у меня получилось показать, что проблема управления зависимости для C++, на котором давным-давно делаются проекты со временем жизни в десятки лет, оказывается более сложной, нежели для новорожденных языков с небольшой пользовательской базой и совсем короткой историей промышленного применения.

9 комментариев:

Eugeniy комментирует...

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

В меркуриале из коробки есть сабрепы, они хорошо ложатся на такой стиль.

Pavel Vainerman комментирует...

В golang тоже инетерсно решили эти вопросы (тоже как я понял не сразу, а относительно недавно).

Проблема конкретных версий решается прописыванием версии для нужного модуля в go.mod
Проблема "исчезновения" публичных проектов, решается командой go mod vendor, которая скачивает все зависимости и раполагает их в катлоаг "vendor" (тут же в проекте). И вы становитесь независимы от внешних истояников. Сюда же можно размещать если надо свои какие-то private модули. Т.к. каталог vendor, это имеет приоритет при поиске зависимостей.
Проблема замены официальной версии на свою решается командой replace (в go.mod), где указывается, что вместо этого компонента использовать вот этот "отсюда".

eao197 комментирует...

@Pavel Vainerman

В среде C++ников далеко не всем нравится иметь исходники сторонних компонентов прямо внутри своего проекта. Отчасти это определяется наличием монстроузных зависимостей типа Qt или Boost-а, сборка которых может затянутся на часы.

Хотя лично я уже много лет придерживаюсь именно такого подхода и во многих случаях, даже с зависимостями масштаба ACE, это работает более чем хорошо.

Pavel Vainerman комментирует...

>> Хотя лично я уже много лет придерживаюсь именно такого подхода и во многих случаях, даже с зависимостями масштаба ACE, это работает более чем хорошо.

Да. Похоже это сейчас самый практичный способ управлять зависимостями.
Мы ещё пробуем иметь разные сборочные среды (с нужными версиями) в docker-контейнерах (чтоб в локальной системе не держать зоопарк). Но это не очень удобно для локальной разработки.


@Eugeniy
>> В меркуриале из коробки есть сабрепы
В git это submodules или subtree как я понимаю.

Сергей комментирует...

Боюсь навлечь на себя гнев, но уебищныйна CMake не является системой сборки. И никогда ей не был :)

eao197 комментирует...

@Сергей

1. "никогда" закончилось после того, как стало возможно делать "cmake --build ."

2. Играть совесами не интересно. Если для "система сборки" в отношении CMake оскорбляет чье-то чувство справедливости, то можно обозвать CMake системой управления сборкой.

3. Ничего из вышеперечисленного не может исправить ни врожденную уёбищность CMake, ни тонны наслоений известной субстанции, отложившихся на CMake за годы её пропихивания в мейнстрим.

Сергей комментирует...

@eao197

1. bash/cmd тоже можно назвать системой сборки - из него можно вызывать make/nmake/build/ninja. Ну или какой нибудь скрипт на одном из перечисленных шеллах.

2. Вообще не оскорбляет. Назови хоть преобразователем DSL в системы нативной сборки (ну или генератором). Сообственно мезон делает то же самое, только они не поддерживают make принципиально.

3. Давно тут не был - но изменилось немногое :) Просто хотел подкорректировать тебя в терминах, ну и послушать твои мудрые слова.

eao197 комментирует...

@Сергей

> 1. bash/cmd тоже можно назвать системой сборки - из него можно вызывать make/nmake/build/ninja.

Нет.

> Ну или какой нибудь скрипт на одном из перечисленных шеллах.

А вот скрипт можно. Поскольку управлять сборкой человек будет не с помощью bash/cmd вручную выдавая отдельные команды, а посредством этого самого скрипта.

В точности так и происходит, когда пользователь работает с CMake.

> Просто хотел подкорректировать тебя в терминах

Что-то как-то не очень попытка.

Сергей комментирует...

@eao197

> Что-то как-то не очень попытка.

Да я и не надеялся :)