- PVSM.RU - https://www.pvsm.ru -

Коль захотел ты сборки передать
И с ними пламенный привет
Нугетом не забудь запаковать
В пакет!
Сразу оговоримся, что в этой статье речь пойдёт о стеке технологий Microsoft .NET.
Часто так бывает, что какое-то подмножество проектов начинает использоваться в разных решениях.
Как правило, программисты, разглядев в соседнем проекте что-то полезное, первое время не заморачиваются — создают папку lib (dll, assemblies и т.п.) и складывают туда скомпилированные сборки из оригинального решения. Со временем становится понятно, что это не самый удобный вариант и вот почему:
Ответом на все эти неприятности может служить вынесение проектов в отдельное решение и создание NuGet-пакета, включающего общие сборки, и смена парадигмы развития этих сборок. По большому счёту, всё это можно сделать и без NuGet, но удовольствия в этом гораздо меньше.Как сделать так, чтобы NuGet-пакет собирался сам автоматически вместе с компиляцией проекта на сервере построения и включал все необходимые свистелки и гуделки — об этом и будет наш рассказ.
Процесс изготовления NuGet-пакетов довольно прост. Вся общая теоретическая часть доступна [1] и, в целом, понятна. В пакеты можно упаковывать различный контент, не только скомпилированные сборки, но и отладочные символы, картинки и т.п. ресурсы, и даже исходный код.
В данном описании мы ограничимся наиболее насущным вопросом упаковки скомпилированных сборок.
Для того, чтобы наладить автоматизированное создание NuGet-пакетов на сервере построения, надо «состряпать» первую версию пакета. Самый простой и понятный способ создания пакета – это использование NuSpec-файла [2], который описывает, что это будет за пакет. Получить данный NuSpec-файл можно разными способами:
В принципе, можно полностью всё создание NuSpec-файла выполнить в GUI, но понимать то, как устроен 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-Net45ICSSoft.STORMNET.DataObject.dll" target="libnet45ICSSoft.STORMNET.DataObject.dll" />
<file src="Debug-Net45ICSSoft.STORMNET.DataObject.xml" target="libnet45ICSSoft.STORMNET.DataObject.xml" />
<!-- ... -->
</files>
</package>
Вот небольшие пояснения, касающиеся некоторых секций:
После того, как NuSpec-файл готов, можно приступить к пробному созданию пакета. Для этого выполняется простая команда утилиты NuGet.exe: nuget pack MyAssembly.nuspec.
Таким образом мы должны получить заветный «первый пакет», или «опытный образец пакета», то есть nupkg-файл, который можно использовать для установки в проекты через NuGet Package Manager или через NuGet.exe [1].
Итак, у нас есть пакет, который надо как-то доставлять пользователям через какой-нибудь «канал сбыта пакетов». Считаем, что большинство пользователей будут устанавливать пакеты через Visual Studio. Встроенный в неё NuGet Package Manager понимает два варианта размещения пакетов:
В настройках можно добавлять собственные источники пакетов, они будут перебираться по очереди при установке или восстановлении пакетов, пока нужный id не будет найден. Вариант, когда один и тот же одинаковый(!) пакет лежит в нескольких источниках – вполне приемлем.
Самый простой вариант для распространения пакетов – создать сетевую папку и складывать пакеты туда.
Стоит отметить, что NuGet позволяет работать не только с общей галереей пакетов https://nuget.org [12], но и создавать собственные галереи, для этого можно развернуть где-то у себя тот же движок [13], что используется на https://nuget.org [14]. Наша команда предпочитает этот вариант, поскольку в этом случае появляется возможность отслеживания статистики загрузок, управление полномочиями через сайт, в конце концов, это просто красиво.

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

Если локальный источник пакетов находится где-то рядом с пользователями, например, в одной локальной сети, то рекомендуется закачать в него все пакеты с зависимостями – это сократит время скачивания пакетов для новых пользователей. Найти nupkg-файлы от зависимых пакетов очень легко – они всегда есть в папке packages, в которую устанавливаются эти самые пакеты (обычно в каталоге с sln-файлом). Также в окне настроек источников пакетов важен порядок – студия будет перебирать источники в случае восстановления пакетов в том порядке, который указан в настройках. Следовательно, если ваш пакет доступен только локально, то первым поставьте свой источник, чтобы не было лишних запросов на nuget.org.
После того, как «опытный образец пакета» сделан и «канал сбыта пакетов» налажен, можно приступать к автоматизации сборки пакетов, чтобы по первому же щелчку мышки мы могли получить горячий и самый свежий 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-файла.
Ниже представлены выдержки из такого скрипта:
# 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
# your build 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. Path relative 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 [15]. Вот раздел скрипта, посвящённый автодокументации:
if($NoDoxygen)
{
Write-Verbose "Doxygen option is disabled. Skip generation of the project documentation.";
}
else
{
Write-Verbose "Doxygen option is enabled. Start documentation generation.";
# Copy doxygen config file.
$doxyConfigSourcePath = Join-Path -Path $toolsFolderPath -ChildPath "DoxyConfig" -Resolve;
$doxyConfigDestinationPath = Join-Path -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 -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 -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 -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 -Path $Env:TF_BUILD_BINARIESDIRECTORY -ChildPath "html" -Resolve;
$archiveFileName = $nuspecXml.GetElementsByTagName("id").Item(0).InnerText + "." +
$nuspecXml.GetElementsByTagName("version").Item(0).InnerText;
$archiveDestinationFolder = Join-Path -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 в каждой из секций конфигурации.
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug-Net35|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>binDebug-Net35</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>binDebug-Net35LogService.XML</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release-Net35|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>binRelease-Net35</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>binRelease-Net35LogService.XML</DocumentationFile>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug-Net40|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>binDebug-Net40</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DocumentationFile>binDebug-Net40LogService.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>binDebug-Net45</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DocumentationFile>binDebug-Net45LogService.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>binRelease-Net40</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<DocumentationFile>binRelease-Net40LogService.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>binRelease-Net45</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<DocumentationFile>binRelease-Net45LogService.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-Net35LogService.dll" target="libnet35LogService.dll" />
<file src="Debug-Net35LogService.XML" target="libnet35LogService.XML" />
<file src="Debug-Net40LogService.dll" target="libnet40LogService.dll" />
<file src="Debug-Net40LogService.XML" target="libnet40LogService.XML" />
<file src="Debug-Net45LogService.dll" target="libnet45LogService.dll" />
<file src="Debug-Net45LogService.XML" target="libnet45LogService.XML" />
</files>
Ещё одна проблема, с которой можно часто столкнуться при использовании (даже не при создании) NuGet-пакетов — проблема подключения одного проекта в несколько солюшенов. Дело в том, что в csproj-файле ссылки на сборки проставляются вплоть до конкретных dll, которые по умолчанию восстанавливаются Visual Studio в папку packages рядом с sln-файлом. Отсюда возникает проблема, когда один и тот же проект включён в несколько солюшенов, располагающихся в разных папках. Для решения этой проблемы можно воспользоваться NuGet-пакетом, который включает в себя специальный Target, который переписывает ссылки перед билдом: https://www.nuget.org/packages/NuGetReferenceHintPathRewrite [16].
Ещё одной особенностью использования NuGet-пакетов является тема восстановление пакетов при сборке. Дело в том, что до некоторых пор Visual Studio не имела встроенных средств восстановления пакетов, поэтому в csproj дописывался специальный Target, который отвечал за восстановление. В современных Visual Studio (2013+) это уже не актуально, следите за чистотой своих csproj-файлов, никаких Target-ов для восстановления NuGet-пакетов больше не требуется.
Ну и напоследок можно рассказать о том, что при использовании TFS папка packages по умолчанию лезет в Source Control и кто-нибудь периодически может проморгать и всё-таки зачекинить все сборки в TFS. Чтобы такого не случилось (мы уверены, что для тех, кто чекинит сборки в TFS в аду должен быть отдельный котёл), можно использовать файл .tfignore [17], который должен спасти от этой напасти.
Итак, выполнив всё, что описано в предложенной нами инструкции, вы можете получить готовый механизм упаковки пакетов, который работает без участия человека. Наши пакеты [18] собираются именно так. Разве что, сама публикация требует некоторого внимания.
Автор: seregamatin
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/107634
Ссылки в тексте:
[1] доступна: http://docs.nuget.org/Create/Creating-and-Publishing-a-Package
[2] NuSpec-файла: http://docs.nuget.org/Create/NuSpec-Reference
[3] NuGet.exe: https://github.com/NuGet/NuGet.Client/releases
[4] NuGet Package Explorer: http://docs.nuget.org/Create/using-a-gui-to-build-packages
[5] указывает: http://www.nuget.org/packages/NewPlatform.Flexberry.ORM/
[6] название компании: http://www.nuget.org/packages/Microsoft.AspNet.Mvc
[7] кто-то: http://www.nuget.org/packages/Unity/
[8] заморачивается: http://www.nuget.org/packages/Npgsql/
[9] семантического версионирования: http://semver.org/lang/ru/
[10] хитро: http://docs.nuget.org/Create/versioning#Specifying-Version-Ranges-in-.nuspec-Files
[11] соглашением: http://docs.nuget.org/Create/NuSpec-Reference#specifying-files-to-include-in-the-package
[12] https://nuget.org: https://nuget.org/
[13] движок: https://github.com/NuGet/NuGetGallery
[14] https://nuget.org: https://nuget.org
[15] Doxygen: http://www.stack.nl/~dimitri/doxygen/
[16] https://www.nuget.org/packages/NuGetReferenceHintPathRewrite: https://www.nuget.org/packages/NuGetReferenceHintPathRewrite
[17] .tfignore: https://msdn.microsoft.com/en-us/library/ms245454.aspx
[18] Наши пакеты: https://www.nuget.org/profiles/Flexberry
[19] Что такое NuGet: http://docs.nuget.org/consume/overview
[20] Top 10 NuGet (Anti-) Patterns: https://msdn.microsoft.com/en-us/magazine/jj851071.aspx
[21] Источник: http://habrahabr.ru/post/274283/
Нажмите здесь для печати.