Автоматизоване створення NuGet-пакетів


Коли захотів ти складання передати
І з ними полум'яний привіт
Нугетом не забудь запакувати
В пакет!


Відразу обмовимося, що в цій статті мова піде про стеку технологій Microsoft .NET.

Часто так буває, що якесь підмножина проектів починає використовуватися в різних рішеннях.

Як правило, програмісти, розгледівши в сусідньому проекті щось корисне, перший час не заморочуються — створюють папку lib (dll, assemblies тощо) і складають туди скомпільовані складання з оригінального рішення. З часом стає зрозуміло, що це не самий зручний варіант і ось чому:

  • оригінальне рішення починає розвиватися в свою власну бік, без урахування «споживачів»: додаються нові залежності, оновлюються версії .net і т. п. «приколи»;
  • якщо навіть про «споживачах» замислюються, то забувають оновити складання у них, коли виходить критичне оновлення або просто нова версія, а потім все стає ще гірше, коли збірок стає більше одного і між ними виникають певні залежності — оновлюючи одну збірку, отримуємо проблеми в момент виконання, т. к. інша збірка може виявитися не тієї версії;
  • оригінальне рішення перестає далі розроблятися.
Відповіддю на всі ці неприємності може служити винесення проектів в окреме рішення і створення NuGet-пакета, що включає загальні збірки, і зміна парадигми розвитку цих збірок. За великим рахунком, все це можна зробити і без NuGet, але задоволення в цьому набагато менше.Як зробити так, щоб NuGet-пакет збирався сам автоматично разом з компіляцією проекту на сервері побудови і включав всі необхідні свистілки і гуделки — про це і буде наша розповідь.

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

У цьому описі ми обмежимося найбільш нагальним питанням упаковки скомпільованих збірок.

Підготовка першого NuGet-пакета
Для того, щоб налагодити автоматизоване створення NuGet-пакетів на сервері побудови, треба «сфабрикувати» першу версію пакету. Самий простий і зрозумілий спосіб створення пакету – це використання NuSpec-файлу, який описує, що це буде за пакет. Отримати даний NuSpec-файл можна різними способами:

  • Взяти чужий приклад і виправити.
  • Згенерувати утилітою NuGet.exe (команда «NuGet.exe spec»).
  • Створити новий пакет або відкрити існуючий чужий пакет GUI-утилітою NuGet Package Explorer, виправити і зберегти командою «Save Metadata As...».
В принципі, можна повністю все створення NuSpec-файлу виконати GUI, але розуміти те, як влаштований NuSpec, все ж буде корисно.

Для прикладу, один з наших NuSpec-файлів зі скороченнями виглядає якось так:

Вміст NuSpec-файлу
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>NewPlatform.Flexberry.ORM</id>
<version>2.1.0-alpha1</version>
<title>Flexberry ORM</title>
<authors>New Platform Ltd</authors>
<!-- ... -->
<description>Flexberry ORM package.</description>
<releaseNotes>
...
</releaseNotes>
<copyright>Copyright New Platform Ltd 2015</copyright>
<tags>Flexberry ORM</tags>
<dependencies>
<dependency id="NewPlatform.Flexberry.LogService" version="1.0.2" />
<!-- ... -->
<dependency id="SharpZipLib" version="0.86.0" />
</dependencies>
</metadata>
<files>
<!-- ... -->
<file src="Debug-Net45\ICSSoft.STORMNET.DataObject.dll" target="lib\net45\ICSSoft.STORMNET.DataObject.dll" />
<file src="Debug-Net45\ICSSoft.STORMNET.DataObject.xml" target="lib\net45\ICSSoft.STORMNET.DataObject.xml" />
<!-- ... -->
</files>
</package>


Ось невеликі пояснення, що стосуються деяких секцій:

  • Id повинен бути унікальним у межах спільного простору імен всіх пакетів, щоб не допускати колізій. Хтось указує у назві пакету назва компанії, потім назва проекту та конкретного продукту, а хтось заморочується.
  • З приводу версій: доброю практикою вважається використання принципів семантичного версионирования. Невелике правило, яке ми виробили у себе в команді – все пререлизные версії (у яких крім 3-х чисел є ще щось в кінці, наприклад, alpha1) ми публікуємо зі збірками, зібраними в Debug-конфігурації, а релізи, відповідно, у Release.
  • Нотатки до релізу (releaseNotes) – дуже корисна річ, обов'язково пишіть там, що змінилося з минулої версії. Користувачі повинні розуміти, що вони отримують з кожним оновленням.
  • Залежності (dependencies). При описі залежностей треба думати про те, як ваш пакет буде встановлюватися: якщо користувачеві досить лише вашого пакета і нічого більше, значить, ніяких залежностей немає. Якщо ж ваші збірки будуть працювати тільки при наявності іншого пакету, наприклад, SharpZipLib, то обов'язково треба прописати цю залежність. Важливо розуміти, що SharpZipLib, в свою чергу, може мати свої залежності, і вони теж «прилетять» користувачу при встановленні, навіть якщо ви їх не вказуєте у себе.
    Установка відбувається рекурсивно, так що користувач в одній з гіпотетичних ситуацій може почати встановлювати один пакет, а йому встановиться більше сотні – як раз через залежності. Під час установки пакетів вибір версії залежного пакету влаштована вельми хитро. Якщо номер версії не вказати, то буде встановлюватися остання релізна версія, інакше та, яка вказана в залежності. До речі, якщо ви використовуєте декілька не пов'язаних між собою пакетів з разу в раз, то ви можете створити порожній пакунок із залежностями від потрібних вам пакетів і встановлювати цей свій пакет – інші встановляться слідом за ним самі.
  • Опис файлів може включати вказівку конкретних імен або масок. Вкрай рекомендуємо дотримуватися правильну структуру пакетів, коли в target пишеться тип контенту, версія .net framework і інші речі, згідно з соглашением. Важливо розуміти, що в атрибуті src при вказівці шляху до файлу треба відштовхуватися від поточного каталогу, в контексті якого буде виконуватися команда упаковки пакета.
Після того, як NuSpec-файл готовий, можна приступити до пробного створення пакета. Для цього виконується проста команда утиліти NuGet.exe: nuget pack MyAssembly.nuspec.

Таким чином, ми повинні отримати заповітний «перший пакет», або «досвідчений зразок пакету», тобто nupkg-файл, який можна використовувати для установки в проекти через NuGet Package Manager або через NuGet.exe.

Виставка готових пакетів
Отже, у нас є пакет, який треба якось доставляти користувачам через який-небудь «канал збуту пакетів». Вважаємо, що більшість користувачів будуть встановлювати пакети через Visual Studio. Вбудований в неї NuGet Package Manager розуміє два варіанти розміщення пакетів:

  • Галерея пакетів, доступна через мережу;
  • Папка Windows (локальна або мережева).
У налаштуваннях можна додавати власні джерела пакетів, вони будуть перебиратися по черзі при встановленні або відновленні пакетів, поки потрібний id не буде знайдений. Варіант, коли один і той же однаковий(!) пакет лежить в декількох джерелах – цілком прийнятний.

Найпростіший варіант для розповсюдження пакетів – створити мережеву папку і складати пакети туди.

Варто відзначити, що NuGet дозволяє працювати не лише з загальною галереєю пакетів https://nuget.org, але і створювати власні галереї, для цього можна розгорнути десь у себе той же движок, що використовується на https://nuget.org. Наша команда віддає перевагу цей варіант, оскільки в цьому випадку з'являється можливість відстеження статистики завантажень, управління повноваженнями через сайт, зрештою, це просто красиво.



Установка галереї може зажадати невеликих танців з бубном, як мінімум, в питанні авторизації, але нічого складного в цьому немає. Публікація пакетів відбувається точно так само, як і на NuGet.org важливо при оновленні сайту галереї не втратити архів з вже завантаженими пакетами – вони зберігаються в каталозі сайту. Налаштування NuGet Package Manager для користувачів в цьому випадку буде виглядати якось так:



Якщо локальний джерело пакетів знаходиться десь поруч з користувачами, наприклад, в одній локальній мережі, то рекомендується закачати в нього всі пакети з залежностями – це скоротить час завантаження пакетів для нових користувачів. Знайти nupkg-файли від залежних пакетів дуже легко – вони завжди є в папці packages, в яку встановлюються ці пакети (зазвичай в каталозі з sln-файлом). Також у вікні налаштувань джерел пакетів важливий порядок – студія буде перебирати джерела в разі відновлення пакетів в тому порядку, який вказаний в налаштуваннях. Отже, якщо ваш пакет доступний тільки локально, то першим поставте своє джерело, щоб не було зайвих запитів на nuget.org.

Фабрика по виробництву NuGet-пакетів
Після того, як «досвідчений зразок пакету» зроблений і «канал збуту пакетів» налагоджений, можна приступати до автоматизації складання пакетів, щоб за першим же клацання мишки ми могли отримати гарячий і свіжий NuGet-пакет.

Розглянемо, як це робиться у випадку з Team Foundation Server 2013/2015. Для інших подібних CI-систем процес буде схожим.
У властивостях Build Definition (XAML) можна вказати PowerShell-скрипт, який виконується в разі успішного виконання побудови. Саме в цьому скрипті і будемо викликати наш «пакувальник», передаючи в якості параметра шлях до NuSpec-файлу.

Є кілька моментів, які слід прояснити для себе: де буде лежати сам NuGet.exe та всі необхідні файли (як мінімум, конфігураційний файл), де буде знаходитися NuSpec-файл? З одного боку, можна покластися на те, що на сервері побудови буде в певному місці розташований NuGet.exe але якщо серверів побудови кілька і займатися їх адмініструванням немає бажання, то найпростіше покласти NuGet.exe в Source Control і додати каталог з його розташуванням у Workspace, з яким буде виконуватися побудова. Що стосується NuSpec, те його зручно тримати поруч з sln-файлом і навіть включити в Solution Items для швидкого доступу до нього через Solution Explorer.

Якщо є кілька солюшенов і планується створювати кілька пакетів, то рекомендується реалізувати один загальний PowerShell-скрипт, який буде в якості параметра отримувати шлях до NuSpec-файлу.

Нижче представлені витяги з такого скрипта:

Витяги з PowerShell-скрипта
# Create NuGet Package after successfully server build.

# Enable -Verbose option for this script call.
[CmdletBinding()]

Param(
# Disable parameter.
# Convenience option so you can debug this script or disable it in 
# build your definition without having to remove it from
# the 'Post-build script path' build process parameter.
[switch] $Disable,

# This script used NuGet.exe from current directory by default.
# You can change this path to meet your needs.
[String] $NuGetExecutablePath = (Get-Item -Path ".\" -Verbose).FullName + "\NuGet.exe",

$BinariesDirectoryPostfixes = @("\Debug", "\Release"),

# Path to the nuspec file. Relative Path TFS project root directory.
[Parameter(Mandatory=$True)]
[String] $NuspecFilePath,

# Disable Doxygen.
[switch] $NoDoxygen
# ...
# Go, go, go!
$nugetOutputLines = & $NuGetExecutablePath pack $realNuspecFilePath -BasePath $basePath
-OutputDirectory $outputDirectory -NonInteractive;
ForEach ($outputLine in $nugetOutputLines) {
Write-Verbose $outputLine;
}
# ...


В скрипті виконуються операції по перетворенню відносних шляхів в абсолютні (можна без праці знайти опис доступних змінних, які означаются CI-системою при запуску скрипта). В деяких випадках потрібна модифікація NuSpec-файлу в цьому скрипті. Наприклад, таким чином можна обробити створення пакетів для різних конфігурацій (Any CPU, x86).

На цьому, власне, настройка автоматичного механізму створення NuGet-пакетів закінчується. Запускаємо збірку на сервері побудови, перевіряємо, що все спрацювало. Для отримання налагоджувальної інформації, якщо щось пішло не так, не забуваємо писати –Verbose параметри скрипта у налаштуваннях визначення побудови. Готові пакети заливаємо в загальний ресурс або галерею і запрошуємо перших користувачів.

Тонкощі процесу
Як говориться, «головне завдання програміста – вбити в собі перфекціоніста». Якщо внутрішній перфекціоніст ще не здався, то йому повинні стати в нагоді наступні пункти.

Крім можливостей по створенню NuGet-пакетів, скрипт для сервера побудови для кожного з пакетів може запускати утиліту генерації автодокументации на основі XML коментарів в коді. Дана можливість зручна в тому плані, що для кожної версії пакету у нас з'являється своя версія автодокументации, це зручно, якщо користувачі застосовують різні версії NuGet-пакетів. Для генерації автодокументации у нас застосовується Doxygen. Ось розділ скрипта, присвячений автодокументации:

Витяги з PowerShell-скрипта для генерації автодокументации
if($NoDoxygen)
{
Write-Verbose "Doxygen option is disabled. Skip generation of the documentation project.";
}
else
{
Write-Verbose "Doxygen option is enabled. Start documentation generation.";

# Copy doxygen config file.
$doxyConfigSourcePath = Join-Path -Шлях $toolsFolderPath -ChildPath "DoxyConfig" -Resolve;
$doxyConfigDestinationPath = Join-Path -Шлях $Env:TF_BUILD_BINARIESDIRECTORY -ChildPath "DoxyConfig";

# Modify doxigen config file according with given nuspec.
$nuspecXml = [xml](Get-Content $NuspecFilePath);
$doxyConfig = Get-Content -Path $doxyConfigSourcePath;

$projectName = $nuspecXml.GetElementsByTagName("title").Item(0).InnerText + " " +
$nuspecXml.GetElementsByTagName("version").Item(0).InnerText;
$doxyConfig = $doxyConfig -replace "FlexberryProjectName", $projectName;

$projectLogoPath = Join-Path -Шлях $toolsFolderPath -ChildPath "logo.png" -Resolve;
$doxyConfig = $doxyConfig -replace "FlexberryProjectLogo", $projectLogoPath -replace "\\", "/";

$doxyConfig = $doxyConfig -replace "FlexberryOutputDirectory", $Env:TF_BUILD_BINARIESDIRECTORY -replace "\\", "/";

$doxyConfig = $doxyConfig -replace "FlexberryInputDirectory", $Env:TF_BUILD_SOURCESDIRECTORY -replace "\\", "/";

$doxyWarnLogFilePath = Join-Path -Шлях $Env:TF_BUILD_BINARIESDIRECTORY -ChildPath "doxygen_log.txt";
$doxyConfig = $doxyConfig -replace "FlexberryWarnLogFile", $doxyWarnLogFilePath -replace "\\", "/";

$doxyConfig | Out-File $doxyConfigDestinationPath default;

# Run doxygen.
$doxygenExecutablePath = Join-Path -Шлях $toolsFolderPath -ChildPath "doxygen.exe" -Resolve;
$doxygenOutputLines = & $doxygenExecutablePath $doxyConfigDestinationPath

ForEach ($outputLine in $doxygenOutputLines) {
Write-Verbose $outputLine;
}

Write-Verbose "Documentation generation done. Packing to the archive.";

# Do archive.
$archiveSourceFolder = Join-Path -Шлях $Env:TF_BUILD_BINARIESDIRECTORY -ChildPath "html" -Resolve;
$archiveFileName = $nuspecXml.GetElementsByTagName("id").Item(0).InnerText + "." +
$nuspecXml.GetElementsByTagName("version").Item(0).InnerText;
$archiveDestinationFolder = Join-Path -Шлях $Env:TF_BUILD_BINARIESDIRECTORY -ChildPath ($archiveFileName + ".zip");

Add-Type -assembly "system.io.compression.filesystem";
[io.compression.zipfile]::CreateFromDirectory($archiveSourceFolder, $archiveDestinationFolder);

# Remove html documentation files.
Remove-Item $archiveSourceFolder -recurse;

Write-Verbose "Done.";
}


Другий пункт буде стосуватися складання проекту в разі, якщо в один пакет упаковуються різні версії збірок під різні версії .net framework.

Хитрощі починаються з того, щоб змусити сервер побудов збирати складання під різні версії .net framework. Розглянемо, проекти, які будуть збиратися в форматі csproj, а не новим json-форматом файлу проекту (ASP.NET5). У Visual Studio підтримується механізм конфігурації збірок. Зазвичай застосовується 2 конфігурації – Debug і Release, але цей же механізм дозволяє налаштувати перемикання версій .net.

Можна створювати свої конфігурації, що ми і робимо. На жаль, щоб виконати «тонке» налаштування всіх необхідних параметрів, доведеться відкрити csproj-файл і, як мінімум, прописати там TargetFrameworkVersion в кожній із секцій конфігурації.Витримки .csproj-файлу
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug-Net35|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug-Net35\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\Debug-Net35\LogService.XML</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release-Net35|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release-Net35\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Release-Net35\LogService.XML</DocumentationFile>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug-Net40|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug-Net40\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DocumentationFile>bin\Debug-Net40\LogService.XML</DocumentationFile>
<DebugType>full</DebugType>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug-Net45|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug-Net45\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DocumentationFile>bin\Debug-Net45\LogService.XML</DocumentationFile>
<DebugType>full</DebugType>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release-Net40|AnyCPU'">
<OutputPath>bin\Release-Net40\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<DocumentationFile>bin\Release-Net40\LogService.XML</DocumentationFile>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release-Net45|AnyCPU'">
<OutputPath>bin\Release-Net45\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<DocumentationFile>bin\Release-Net45\LogService.XML</DocumentationFile>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>


Конфігурації в Visual Studio перемикаються в основному тулбарі, у визначенні складання на сервері можна вибрати кілька конфігурацій, які будуть компілюватися послідовно.

Варто зазначити, якщо у вас код під різні версії .net framework починає змінюватися, то це можна обробляти за допомогою директив:

#if NETFX_35
for (int i = 0; i < resValueLength; i++) 
#else
System.Threading.Tasks.Parallel.For(0, resValueLength, i =>
#endif

При цьому константи повинні бути визначені у відповідній секції csproj-файлу:

<DefineConstants>DEBUG;TRACE;NETFX_35</DefineConstants>

Коли у нас є готові скомпільовані складання, давайте розберемося, як правильно налаштувати nuspec. У nuspec задаються спеціальні каталоги під конкретні версії .net framework.

Приклад секції files в NuSpec-файлі:

<files>
<file src="Debug-Net35\LogService.dll" target="lib\net35\LogService.dll" />
<file src="Debug-Net35\LogService.XML" target="lib\net35\LogService.XML" />
<file src="Debug-Net40\LogService.dll" target="lib\net40\LogService.dll" />
<file src="Debug-Net40\LogService.XML" target="lib\net40\LogService.XML" />
<file src="Debug-Net45\LogService.dll" target="lib\net45\LogService.dll" />
<file src="Debug-Net45\LogService.XML" target="lib\net45\LogService.XML" />
</files>

Ще одна проблема, з якою часто можна зіткнутися при використанні (навіть не при створенні) NuGet-пакетів — проблема підключення одного проекту в кілька солюшенов. Справа в тому, що в csproj-файлі посилання на збірки проставляються аж до конкретних dll, які за замовчуванням відновлюються Visual Studio в папку packages поруч з sln-файлом. Звідси виникає проблема, коли один і той же проект включений в кілька солюшенов, розташованих в різних папках. Для вирішення цієї проблеми можна скористатися NuGet-пакетом, який включає в себе спеціальний Target, який переписує посилання перед білдів: https://www.nuget.org/packages/NuGetReferenceHintPathRewrite.

Ще однією особливістю використання NuGet-пакетів є тема відновлення пакетів при збірці. Справа в тому, що до деяких пір Visual Studio не мала вбудованих засобів відновлення пакетів, тому в csproj дописывался спеціальний Target, який відповідав за відновлення. В сучасних Visual Studio (2013+) це вже не актуально, стежте за чистотою своїх csproj-файлів, ніяких Target-ів для відновлення NuGet-пакетів більше не потрібно.

Ну і наостанок можна розповісти про те, що при використанні TFS папка packages за замовчуванням лізе в Source Control і хто-небудь періодично може пропустити і все-таки зачекинить всі збірки в TFS. Щоб такого не сталося (ми впевнені, що для тих, хто чекинит складання в TFS в пеклі повинен бути окремий котел), можна використовувати файл .tfignore, який має врятувати від цієї напасті.

Результат
Отже, виконавши все, що описано в запропонованій нами інструкції, ви можете отримати готовий механізм упаковки пакетів, який працює без участі людини. Наші пакети збираються саме так. Хіба що, сама публікація вимагає певної уваги.

Корисні посилання:


Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.