Фундаментальная проблема пакетных менеджеров для языков программирования
Почему существует столько много различных пакетных менеджеров? Их можно встретить как во многих операционных системах (apt, yum, pacman, Homebrew), так и работая со многими языками программирования (Bundler, Cabal, Composer, CPAN, CRAN, CTAN, EasyInstall, Go Get, Maven, npm, NuGet, OPAM, PEAR, pip, RubyGems, и т.д. и т.п.). «Каждый язык программирования нуждается в собственном пакетном менеджере, это уже стало общепризнанной истиной». Что за необъяснимое притяжение заставляет языки программирования, один за другим, скатываться в этот обрыв? Почему бы нам просто не использовать уже существующие пакетные менеджеры?
У вас, вероятно, уже имеются некоторые предположения, почему использование apt для управления пакетами Ruby является не самой хорошей идеей. «Системные менеджеры пакетов и менеджеры пакетов для языков программирования — абсолютно разные вещи. Централизованное распостранение всех пакетов это замечательно, но совершенно не подходят для большинства библиотек, выложенных на GitHub. Централизованное распостранение пакетов слишком медленное. Все языки программирования разные и их комьюнити никак не взаимодействуют между собой. Такие пакетные менеджеры устанавливают пакеты глобально, а я хочу управлять версиями используемых библиотек» Эти недостатки, безусловно, присутствуют в данном решении. Но в них упускается сама суть всех этих проблем.
Фундаментальная проблема заключается в том, что пакетные менеджеры для различных языков программирования являются децентрализованными.
Эта децентрализация подразумевается даже в самом определении пакетного менеджера: это некая программа, устанавливающая из удаленных источников программы и библиотеки, которые не были доступны локально, на момент установки. Даже если представить идеальный централизованный пакетный менеджер, даже там будут существовать две копии данной библиотеки: одна — где-то на сервере, вторая — расположенная локально у программиста, который пишет приложение, используя эту библиотеку. Однако, в реальности экосистема библиотек сильно страдает от фрагментации — она объединяет множество библиотек, созданных разными разработчиками. Конечно, все библиотеки могут загружаться и индексироваться в одном месте, но это все равно не означает того, что авторы библиотек будут знать о любых других случаях использования. И затем мы получим то, что в мире Perl называют DarkPAN: бесчисленной количество кода, которое, вроде-бы существует, но о котором мы не имеем ни малейшего представления, так как оно зашито где то в проприетарном коде или функционирует где-то на корпоративных серверах. Обойти децентрализацию можно только когда вы контролируете абсолютно весь код вашего приложения. Но в этом случае вам вряд ли понадобится пакетный менеджер, не так ли? (Кстати, коллеги рассказывали мне, что подобное является обязательным для больших проектов, например, таких как операционная система Windows или браузер Google Chrome.)
Децентрализованные системы сложны. Серьезно, очень сложны. Если вы тщательно не продумаете архитектуру такой системы, то вас непременно ожидает dependency hell. Не существует одного «правильного» решения этой проблемы: я могу назвать, как минимум, три различных подхода к решению данной проблемы, применяемых в различных поколениях пакетных менеджеров, и каждый из них имеет свои плюсы и минусы.
Закрепляемые версии. Пожалуй, самым популярным, является мнение, что разработчик должен строго указывать используемую версию пакета. Этот подход продвигается такими менеджерами как Bundler для Ruby, Composer для PHP, pip в связке с virtualenv для Python и любым другим, вдохновленным подходом Ruby/node.js (к примеру, Gradle для Java или Cargo для Rust). Воссоздаваемость сборок в них правит балом — эти пакетные менеджеры решают проблему децентрализованности, просто предполагая, что вся экосистема пакетов перестает существовать, как только вы закрепили версии. Основным преимуществом данного подхода является то, что вы можете указывать версии библиотек, которые используете в коде. Конечно, это же является и минусом — вам всегда придется контролировать версии этих библиотек. Обычно версии просто фиксируют, благополучно забывая о них, даже если выходит какое-нибудь важное обновление безопасности. Чтобы иметь обновленные версии всех зависимостей необходимы циклы разработки, но это время чаще всего тратится на другие вещи (например на разработку новых фич).
Стабильные версии. Если управление пакетами требует, чтобы каждый индивидуальный разработчик приложения тратил время и усилия на поддержку всех зависимостей в актуальном состоянии и проверял чтобы они продолжали корректно работать с приложением и друг с другом, мы могли бы задаться вопросом — а существует ли способ централизовать эту работу? Это приводит нас к еще одному подходу: создать централизованный репозиторий с одобренными пакетами, работа вместе которых проверена, и выпускать для них багфиксы и обновления безопасности, пока мы будем поддерживать обратную совместимость. Для различных языков программирования существуют реализации и таких пакетных менеджеров. По крайней мере два, о которых я знаю, это Anaconda для Python и Stackage для Haskell. Но если приглядеться, мы увидим, что точно такая же модель используется в пакетных менеджерах операционных систем. Как системный администратор, я часто рекомендую пользователям отдавать предпочтение библиотекам, распостраняемым в репозиториях операционных систем. Они не сломают обратную совместимость пока мы не перейдем на новую релизную версию ОС, и в то же время, вы всегда будете использовать последние багфиксы и обновления безопасности. (Да, вы не сможете воспользоваться фичами из новых версий, но, само по себе, это идет вразрез с понятием стабильности.)
Рассматривая децентрализацию. До этого пункта мы старались вообще не рассматривать децентрализацию как приемлемый подход. Говорили о том, что необходим центральный репозиторий и контроль за обновлениями со стороны разработчика. Но не выплескиваем ли мы ребенка вместе с водой? Главным минусом централизованного подхода является огромное количество работы, которое необходимо провести для того, чтобы добиться стабильной работы всех пакетов и поддержки этих пакетов в актуальном состоянии. Кроме того, никто не ожидает что абсолютно все пакеты будут совместимы друг с другом, но, тем не менее, это не мешает использовать определенные категории пакетов вместе с другими. Идеальная децентрализованная система перекладывает задачу определения, какие пакеты могут работать вместе на каждого, кто принимает участие в этой системе, что опять возвращает нас к фундаментальному вопросу: Каким образом мы можем создать экосистему децентрализованных пакетных менеджеров, которая будет работать?
Вот ряд принципов, которые могут нам помочь:
- Строгая инкапсуляция зависимостей. Одна из причин, которая делает dependency hell такой коварной проблемой, заключается в том, что зависимости пакета часто являются неразрывной частью с его основным API: таким образом, выбор зависимости в большей степени явлется глобальным выбором, влияющим на все приложение. Если библиотека использует какие либо зависимости внутри, и этот выбор полностью обусловлен только деталями внутренней реализации этой библиотеки, он не должен приводить к каким либо глобальным ограничениям. NPM для NodeJS доводит этот принцип до логического предела — по умолчанию он не ограничивает дублирование зависимостей, позволяя каждой библиотеке загрузить свой собственный экземпляр зависимого пакета. Хотя я и сомневаюсь что стоит дублировать абсолютно все пакеты (это встречается в экосистеме Maven для Java), я, конечно, согласен, что такой подход повышает компонуемость зависимостей.
- Продвижение семантического версионирования. В децентрализованных системах особенно важно, чтобы разработчики библиотек предоставляли как можно более точную информацию о библиотеке, для того чтобы пользователи и утилиты работающие с пакетами могли принимать обоснованные решения. Различные форматы версий и диапазонов версий только усложняют, и без того непростую задачу (как я уже писал в предыдущем посте). Если у вас есть возможность использовать семантические версии, или даже лучше, вместо семантических версий использовать более правильный подход, указывая зависимости на уровне типов в своих интерфейсах, наши утилиты смогут сделать лучший выбор. «Золотой стандарт» информации в децентрализованных системах это «Совместим ли пакет А с пакетом Б», и эта информация, зачастую, очень сложна для анализа (или невозможна, для систем с динамической типизацией).
- Централизация для особых случаев. Один из принципов децентрализованной системы заключается в том, что каждый участник может собрать наиболее подходящие для себя окружение. Это подразумевает определенную свободу в выборе центрального источника или же создание и использование своего собственного — централизация для особых случаев. Если мы предполагаем, что пользователи будут создавать свои собственные репозитории, в стиле используемых в операционных системах, мы должны предоставить им инструменты с помощью которых можно будет легко и безболезненно создавать и использовать эти репозитории.
В течении длительного времени экосистема управления исходным кодом была полностью построена вокруг централизованных систем. Распостранение таких систем контроля версий как Git в корне изменило ситуацию: хотя Git и может казаться более сложным чем Subversion для освоения далекими от технологий людьми, достоинства децентрализации гораздо масштабнее и разнообразнее. Но такой же Git для управления пакетами создать пока никому не удалось. Если кто-то будет уверять вас, что проблема управления пакетами решена и все просто изобретаю заново Bundler, я прошу вас — подумайте о децентрализации как следует.