# Пакет с NuGet-пакетами в GitLab: от общих DLL к внутреннему NuGet Repository

### А у вас есть пакет с пакетами?

Почти в каждом доме есть пакет с пакетами. На первый взгляд странная традиция, но все знают, куда складывать пакеты, которые ещё пригодятся. С NuGet-пакетами у нас получилось примерно так же. 

К своему **пакету с пакетами** мы пришли не сразу. Плагины для Revit, в том числе созданные разными компаниями, загружаются в один процесс и могут принести разные версии одних и тех же DLL. Какая сборка загрузится первой, та и может определить поведение остальных. Иногда плагин не загружается сразу, а иногда ошибка может проявиться уже в рантайме.

---

### От DLL на сетевом диске к NuGet-пакетам

Первые конфликты у нас возникли вокруг UI-библиотек **Prism**, **Microsoft.Xaml.Behaviors** и **MaterialDesign**. Для них сделали форки с префиксом неймспейсов **MarksDigital**. Первоначально они жили в одном репозитории, а собранные DLL лежали на сетевом диске.

Время от времени в форки требовалось вносить отдельные правки, и схема с общей папкой DLL становилась всё менее удобной. **MarksDigital.Prism**, **MarksDigital.Microsoft.Xaml.Behaviors** и **MarksDigital.MaterialDesign** переехали в отдельные репозитории, в каждом настроили сборку NuGet-пакета. Сначала пакеты собирались и публиковались локально батником, позже тот же процесс перенесли в GitLab CI. Версия пакета берётся из Git-тега, а сборка и публикация выполняются на runner.

---

### Почему GitLab

К тому моменту GitLab уже использовался для исходного кода, а встроенный NuGet Package Registry позволял обойтись без ещё одного сервера. Не нужно было разворачивать, обновлять и резервировать отдельную систему хранения пакетов. Права доступа, проекты, runner и резервное копирование уже были частью имеющейся инфраструктуры.

Первоначально в репозитории находились только наши пакеты. Сторонние зависимости восстанавливались напрямую с nuget.org, и версии пакетов в плагинах постепенно начали расходиться. Аудит и выравнивание версий исправляли ситуацию только на конкретный момент: уже на следующий день в одном из проектов могла появиться другая версия или новая библиотека.

Поэтому в GitLab стали переносить и сторонние пакеты. Перед публикацией проверялись конкретная версия, её лицензия и транзитивные зависимости. GitLab Registry сам не проверяет лицензии — это делается до помещения пакета во внутренний репозиторий.

---

### Когда версия меняет лицензию

Open source даже для невирусных лицензий не означает "бесплатно для любых целей и навсегда": условия могут меняться между версиями. Примером может служить пакет **Extended.Wpf.Toolkit**, содержащий сборку **Xceed.Wpf.Toolkit.dll**.

В README [официального репозитория](https://github.com/xceedsoftware/wpftoolkit) Xceed указывает, что Extended WPF Toolkit распространяется по Xceed Community License начиная с версии 4.0.0. Однако история репозитория и опубликованные NuGet-пакеты показывают более сложную картину:

- [3.6.0](https://www.nuget.org/packages/Extended.Wpf.Toolkit/3.6.0) - последняя версия, в которой репозиторий и NuGet-пакет согласованно указывают открытую лицензию MS-PL;
- [3.7.0](https://www.nuget.org/packages/Extended.Wpf.Toolkit/3.7.0) - тег в Git еще содержит MS-PL, но опубликованный пакет уже снабжён Xceed Community License с ограничением на коммерческое использование. Сразу после создания тега лицензию заменили и в [репозитории](https://github.com/xceedsoftware/wpftoolkit/commit/32c71eb1e7c83812bdb1dc99a7efee018321c6d5);
- [3.8.0](https://www.nuget.org/packages/Extended.Wpf.Toolkit/3.8.0) и [3.8.1](https://www.nuget.org/packages/Extended.Wpf.Toolkit/3.8.1) также опубликованы по Xceed Community License;
- [3.8.2](https://www.nuget.org/packages/Extended.Wpf.Toolkit/3.8.2) снова объявлена пакетом под MS-PL. В release notes прямо сказано, что это эквивалент 3.8.1 с лицензией MS-PL. По странице nuget.org и метаданным пакет может выглядеть подходящим для использования, но внутри архива остался файл LICENSE.txt с текстом Community License. Это неочевидный риск: проверка только метаданных не покажет противоречие в самом пакете;
- [4.0.0](https://www.nuget.org/packages/Extended.Wpf.Toolkit/4.0.0) распространяется по Xceed Community License и соответствует формулировке из README.

Первые три из перечисленных версий - 3.6.0, 3.7.0 и 3.8.0 - сейчас помечены на nuget.org как delisted. Это не означает, что пакеты удалены: по прямой ссылке остаются доступны страница версии и её метаданные, а restore по точно заданной версии продолжает работать. Такие версии скрыты из обычного поиска и общего списка версий.

Последняя версия без таких противоречий - 3.6.0 распространявшаяся по MS-PL.

---

### Пакеты тянут за собой пакеты

Перенести во внутренний репозиторий только нужный прямой пакет недостаточно. У него есть транзитивные зависимости, у них - свои и так далее. Если хотя бы одного пакета из этой цепочки нет внутри, изолированный restore падает.

Сначала недостающие пакеты выявлялись по ошибкам сборки: загрузил, опубликовал, запустил ещё раз.

Потом проект стали восстанавливать на компьютере с доступом к nuget.org: NuGet автоматически загружал в локальный кэш как прямые, так и все транзитивные зависимости. Их всё равно приходилось вручную собирать из кэша и публиковать во внутренний репозиторий.

Чтобы не собирать пакеты из кэша вручную, мы разработали утилиту **MarksDigital.NuGetUtils**. Она загружает заданный пакет вместе с транзитивными зависимостями с nuget.org, а затем сверяет подготовленный набор со внутренним репозиторием, чтобы не публиковать повторно то, что уже есть.

Лицензии утилита не проверяет. Решение о публикации принимает человек, ответственный за допуск пакетов во внутренний репозиторий.

Позже мы посмотрели, можно ли заменить утилиту готовым сервером пакетов. Например, [BaGet](https://github.com/loic-sharma/BaGet) и [Nexus Repository Community Edition](https://www.sonatype.com/products/nexus-community-edition-download) умеют работать как прокси к nuget.org: во время restore NuGet запрашивает прямые и транзитивные зависимости, а сервер сохраняет их локально.

Но для нас в такой схеме теряется важная граница: новый пакет попадает во внутренний контур до того, как ответственный проверит конкретную версию и её лицензию.

Поэтому мы планируем развивать **MarksDigital.NuGetUtils** как инструмент подготовки пакетов к допуску.

---

### Внутренний репозиторий как единственный источник

На GitLab Runner источник nuget.org отключён, а сетевой доступ к нему закрыт. Если разработчик добавит несогласованную зависимость, чистая сборка не сможет её восстановить и restore завершится ошибкой.   

Версии прямых зависимостей вынесли в общие **.props**-файлы. Внутренний репозиторий определял, какие пакеты доступны, а **.props**-файлы - какие именно версии используются в проектах.

Когда nuget.org оказался недоступен из-за сбоя или сетевых ограничений, наши сборки продолжили работать с внутренними копиями пакетов.

---

### Что в итоге

К моменту подготовки плагинов к выходу на рынок во внутреннем репозитории уже были конкретные проверенные версии пакетов. Не пришлось в авральном режиме выяснять, какие библиотеки входят в поставку, разбирать конфликты версий и с нуля собирать сведения о сторонних лицензиях.

Так у нас и появился свой **пакет с пакетами**: мы точно знаем, какая версия лежит внутри и почему она там оказалась.