cfNugetTargets – Auto-Deploy NuGet mit Versionierung

NuGet nimmt einen immer größeren Stellenwert in unserem Entwickleralltag ein. Das ist alles gut und schön, nur das Deployment und Versionsmanagement ging uns immer mehr auf den Zeiger. Nach einigem Tüfteln ist ein NuGet-Package heraus gekommen, das wir heute in einer ersten Alpha-Version auf nuget.org released haben. Dieser Artikel erklärt die Hintergründe und den Einsatz.

NuGet-Link



Vorbemerkungen

NuGet-Pakete machen vor allem immer dann Sinn, wenn man Logik entwickelt, die man Solution-übergreifend wiederverwenden möchte. Ich rede hier bewusst nicht über den Use Case, dass man Pakete für die Allgemeinheit schreibt und dann bei nuget.org hostet. Das ist zwar auch kein Problem, der Alltag in der Programmierung ist aber meist weniger prominent. Man hat im Visual Studio eine unternehmenseigene Package Source auf einer File Share o.ä. und dort versammeln sich mit der Zeit die ganzen Pakete inkl. der unvermeidlichen Versionen.

Das Problem ist nun, wie man den Prozess der Paketierung und Versionierung möglichst komfortabel gestaltet. Ich beginne hier mit den üblichen Vorgehensweisen, um dann die Vorteile der cfNugetTargets zu erklären und deren Einsatz zu beleuchten.

Wer direkt zur Beschreibung des Paketes springen möchte, kann hier klicken.

Pakete erstellen

Prinzipiell kann man für alles mögliche NuGet-Pakete erstellen. Das System ist extrem mächtig und kann z.B. auch eine Sammlung von Dateien irgendwohin verteilen. Es gibt dabei grundsätzlich 3 Möglichkeiten, ein Paket zu erstellen:

  1. Man ruft „nuget.exe pack“ auf und übergibt alle Einstellungen für das neue Paket als Parameter in der Konsole.
  2. Man erstellt eine *.nuspec-Datei, die die gesamten Einstellungen versammelt und verwendet dann „nuget pack“ unter Verweis auf diese Datei.
  3. Man verwendet das kostenlose Programm „NuGet Package Explorer“ (Download hier).

Variante 3 ist dabei einfach nur ein UI für Variante 2.

Gerade für Beginner eignet sich Variante 3 hervorragend, um erste Übungen mit dem Erstellen von Packages durchzuführen. Ich empfehle aber hiermit auch allen anderen, das Tool zu installieren, weil es die Generierung der NuSpec-Dateien so einfach macht. Legen wir mal damit los.

NuGet Package Explorer

Stellen wir uns vor, wir hätten eine kleine Tool-Library erstellt und möchten Sie in unterschiedlichen Projekten unserer Firma einsetzen. Der Einfachheit halber werden wir diese mal besonders simpel halten.

namespace codingfreaks.MyLogic
{
    using System;

    public static class DateTimeExtensions
    {
        #region methods

        public static string ToShortDateString(this DateTime? date)
        {
            return date.HasValue ? date.Value.ToShortDateString() : string.Empty;
        }

        #endregion
    }
}

Meine Library besteht aus einem ClassLibrary-Projekt mit genau einer Klasse „DateTimeExtensions“, die eine simple Implementierung hat. Daraus will ich nun ein Nuget-Package erstellen.

Im ersten Schritt erstelle ich einen Ordner (in meinem Beispiel ein lokaler, im Unternehmen eher eine Netzwerkfreigabe) und nenne ihn „C:\NuGet“. Nachdem ich dann mein Projekt in der Konfiguration „Release“ gebaut habe, starte ich den NuGet Package Explorer.

Sofort nach dem Start wähle ich die Option „Create a new package“. Es erscheint ein Fenster, das zweigeteilt ist. Im linken Bereich definiert man die Metadaten des Paketes und im rechten Bereich fügt man Dateibasierte Elemente hinzu.

Für unser Beispiel richte ich das Projekt wie folgt ein:

Abb. 1: Paket eingerichtet
Abb. 1: Paket eingerichtet

In Abb. 1 ist der Metadaten-Bereich noch im Editier-Modus. Mit einem Klick auf den grünen Haken darüber kann dieser wieder verlassen werden. Im rechten Bereich ist genau eine Datei in einem Ordner enthalten. Diesen Ordner habe ich durch Rechtsklick in den Bereich und Auswahl von „Add Lib Folder“ erzeugt. Dort hinein habe ich dann eine existierende Datei, nämlich meine DLL aus bin/Release hinzugefügt.

Achtung! Der Editor ist zwar einfach zu bedienen, wenn man aber keine Detailkenntnisse zur Funktionsweise von NuGet-Paketen hat, kann man eine Menge Überraschungen erleben und Unsinn machen. Ich habe meine Version mit dem Suffix „-alpha“ versehen, damit NuGet automatisch erkennt, dass ich erstmal nur ein Prerelease erstellen möchte.

Jetzt ist es an der Zeit, das Package zu erzeugen:

Abb, 2: Paket speichern
Abb, 2: Paket speichern

Ich wähle als Ordner den Zuvor angelegten und das Programm erzeugt automatisch einen passenden Dateinamen. Wer sich übrigens für die Versionierungs-Logik von NuGet interessiert, der sollte sich SemVer ansehen. NuGet bezieht deren Grundsätze komplett mit ein.

Finale Einstellungen im Visual Studio

Zunächst stelle ich im Visual Studio über „Tools->NuGet Package Manager->Package Manager Settings“ sicher, dass mein Ordner „C:\NuGet“ eine bekannte Paket-Quelle wird:

Abb. 3: NuGet-Paket-Quelle einrichten
Abb. 3: NuGet-Paket-Quelle einrichten

Wichtig ist hier zu wissen, dass die entsprechende Einstellung User-spezifisch in der Datei

%APPDATA%\Roaming\NuGet\NuGet.config

vorgenommen wird. Meine entsprechende Datei sieht nach der Änderung so aus:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageRestore>
    <add key="enabled" value="True" />
    <add key="automatic" value="True" />
  </packageRestore>
  <packageSources>
    <add key="nuget.org" value="https://www.nuget.org/api/v2/" />
    <add key="My" value="c:\NuGet" />
  </packageSources>
  <disabledPackageSources />
  <activePackageSource>
    <add key="All" value="(Aggregate source)" />
  </activePackageSource>
</configuration>

Das wird im späteren Verlauf noch wichtig werden.

Das Package einbinden

Die Installation ist nun relativ einfach. Ich verwende der Einfachheit halber mal das UI in einem neuen Projekt vom Typ Console Application.

Abb. 4: Paket von meiner Quelle installieren
Abb. 4: Paket von meiner Quelle installieren

Die neue Paketquelle ist nun unter dem Namen „My“ vorhanden. Von dort kann ich das Paket nun installieren, allerdings nur, wenn ich in der Combo oben einstelle, dass auch Alpha-Versionen gezeigt werden dürfen. Am rechten Rand werden mir dann auf den Metadaten basierende Zusatzinfos gezeigt. Nach der Installation habe ich einfach eine Referenz zu meiner DLL in den Verweisen des Projektes und kann die Extension-Methode benutzen:

Abb. 5: Paket installiert und genutzt
Abb. 5: Paket installiert und genutzt

Abb. 5 hebt die einzelnen Bestandteile in Quellcode und Solution Explorer hervor, die durch den Einsatz von NuGet verfürgbar geworden sind. In der packages.config steht nun ein Eintrag auf unser Paket inkl. der Versionsnummer. Super!

Nachteile

Jetzt, wo wir so schön ein Paket erstellt haben, stellt sich womöglich die Frage, warum ich den Artikel damit nicht einfach abschließe und „Viel Spaß“ wünsche. Ganz einfach deshalb, weil gerade in eigenen Paketen immer wieder auch Updates gemacht werden müssen und das vor allem sicher funktionieren muss.

Ein NuGet-Update erstellt man, indem man basierend auf einem Paket ein neues erstellt und dabei folgendes tut:

  1. Man ändert die Version auf eine Nummer, die im Ergebnis höher ist, als die bisherige (siehe SemVer).
  2. Man ändert auf keinen Fall die PackageId.
  3. Man erstellt einen Release-Kommentar (optional und gern weggelassen).
  4. Man ersetzt die DLL im lib-Ordner.
  5. Man speichert das Paket im gleichen Ordner (C:\NuGet) mit der neuen Version im Namen und belässt (wichtig) die bisherige Version dort.

Wenn danach ein beliebiger Client ein update-package durchführt, wird die neue Version gefunden und das NuGet-Update greift. Die alte Version muss deshalb (zumindest eine gewisse Zeit) im Ordner bleiben, weil wir ja nicht wissen, ob bereits alle Clients ein Update gemacht haben. Würden wir also beispielsweise jetzt Version 0.1.2-alpha bereit stellen und ein Client hätte sich vor 5 Minuten das Paket „0.1.1.-alpha“ besorgt, würde z.B. sein NuGet-Package-Restore nicht mehr funktionieren und ein Kommando, wie „update-package -reinstall MyLogic“ löuft auf einen Fehler.

Der Nachteil unserer Vorgehensweise ist also, dass wir eine Menge manueller Schritte unternehmen müssen und schön zwischen verschiedenen Ordnern hin- und her springen. Diese Arbeiten sind so schön dämlich, dass man in der Eile immer wieder Fehler macht und die Kollegen genervt sind. Bei solchen Problemen hilft i.d.R. immer die gleiche Technologie.

MSBuild

MSBuild greift hier nahezu perfekt, weil wir im Prinzip nach einem Release-Build eine neue Version verteilen möchten. Es gibt ein paar Bedingungen, die erfüllt sein sollten, bevor man eine solche Automatisierung aufsetzt:

  • Man sollte in seinem Lib-Projekt Unit-Tests integriert haben und diese sollten auch laufen.
  • Man sollte über spezielle Konfigurationen steuern, welche davon zu einem NuGet-Package führt.
  • Man sollte die Versionierung zwischen dem Projekt und dem NuGet-Paket stringent halten.
  • Man sollte MSBuild verstehen.

Meiner Erfahrung nach wird gerade der letzte Anstrich gern weg geredet. Man sieht oft recht komplexe MSBuild-Skripte bei Leuten im Einsatz, die nach einigem Nachbohren endlich zugeben, dass sie das ganze in einer Nacht-und-Nebel-Aktion aus dem Netz kopiert und ein wenig angepasst haben. Davon kann ich nur dringend abraten. So gut, wie jedes NuGet- oder Visual-Studio-Update wirft solche Konstrukte gern aus der Bahn und wehe, wenn das Ganze dann auch noch im Team funktionieren soll.

cfNugetTargets

Die cfNugetTargets basieren auf einer MSBuild-targets-Datei, die durch das NuGet-package in den Ordner „.nuget“ eingebunden wird. Ich habe mich für diesen Ordner entschieden, weil der durch das NuGet-Package-Restore erstellt wird und wir die NuGet.exe sowieso brauchen. Sollte das Restore noch nicht eingestellt und somit der .nuget-Ordner noch nicht da sein, kümmert sich unser Package auch darum.

Das Package kann über

Install-Package cfNugetTargets -Pre

eingebunden werden.

Gehen wir für unser Beispiel einmal davon aus, dass wir das Paket in eine komplett neu erstellte Solution mit einem Projekt einbinden. Nach der Installation sollte folgendes im Solution Explorer angezeigt werden:

Abb. 6: cfNugetTargets installiert
Abb. 6: cfNugetTargets installiert

Zur einfacheren Einrichtung geht auch gleich eine readme.txt auf, die in Englisch erläutert, was als nächstes zu tun ist. Hier nun der Reihenfolge nach.

Anpassung der NuGet.config

Die bereits oben erwähnte NuGet.config sollte angepasst werden. Es gibt eine Option, die dem lokalen NuGet mitteilt, dass bei einem Deployment (in NuGet-Sprache ein „Push“) eine bestimmte Package Source verwendet werden soll. Mein Beispiel aus Listing 2 sollte nach der Anpassung so aussehen:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageRestore>
    <add key="enabled" value="True" />
    <add key="automatic" value="True" />
  </packageRestore>
  <packageSources>
    <add key="nuget.org" value="https://www.nuget.org/api/v2/" />
    <add key="My" value="c:\NuGet" />
  </packageSources>
  <config>
	<add key="DefaultPushSource" value="c:\NuGet" />
  </config>
  <disabledPackageSources />
  <activePackageSource>
    <add key="All" value="(Aggregate source)" />
  </activePackageSource>
</configuration>

Die neu hinzu gekommenen Zeilen sind markiert. Wichtig ist dies vor allem deshalb, weil die aktuelle Version von cfNugetTargets nur mit Dateibasierten Paket-Quellen umgehen kann. Sie kann z.B. einen für den nuget-Upload notwendigen API-Key noch nicht mit liefern. Das ist für spätere Versionen in Planung.

NuGet Package Restore einschalten

Unser Paket geht davon aus, dass innerhalb einer Solution im Ordner „.nuget“ eine aktuelle NuGet.exe zu finden ist. Dies erreicht man durch Rechtsklick auf die Solution im Solution Explorer und Auswahl von „Enable NuGet Package Restore“. Dies ist ohnehin eine sehr gute Idee (zumindest im professionellen Einsatz) und daher verwenden wir die entsprechende Struktur gleich mit.

Eine Konfiguration erstellen oder die targets-Datei anpassen

Damit unser Paket weiß, wann es überhaupt loslegen soll, etwas zu tun, haben wir als Konvention festgelegt, dass es eine Build-Konfiguration namens „ReleaseNuget“ geben soll, damit wir ein Package automatisch erstellen. Man kann jetzt entweder eine solche Konfguration auswählen oder man editiert die Datei „.nuget/codingfreaks.nuget.targets“ in Zeile 5. Die Variable „NugetReleaseName“ kann z.B. einfach auf „Release“ geändert werden.

Eine nuspec-Datei für das zu verteilende Projekt anlegen

Hinweis In der aktuellen Version des Paketes fehlt dieser Schritt in der readme.

Unser Paket sucht innerhalb eines Projektordners später nach einer nuspec-Datei, die im Dateinamen genau so heißt, wie die *.csproj-Datei. In unserem Beispiel oben wäre das z.B. dann „MyLogic.nuspec“. Ist diese Datei nicht vorhanden, wird auch keine Verteilung vorgenommen.

An dieser Stelle kommt wieder der NuGet Package Explorer ins Spiel. Nachdem man hier einmal wie oben gezeigt alle Einstellungen für ein Paket bequem vorgenommen hat, kann man, statt es als nupkg zu speichern, auch die Option „File->Export“ wählen. Nun ändert man den Dateinamen in „{Projektname}.nuspec und speichert diese Datei im gleichen Ordner, wie die csproj-Datei. Jetzt fügt man die Datei am besten noch dem Projekt hinzu.

Im Visual Studio kann man nun diese XML-Datei editieren. Das Problem an der Standard-Nuspec ist, dass die Pfade für die einzubindenden Dateien aus Sicht von MSBuild nicht stimmen. Hier erstmal meine nuspec für das Beispielprojekt:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
    <metadata>
        <id>MyLogic</id>
        <version>0.1.1-alpha</version>
        <title>codingfreaks Logic sample</title>
        <authors>sprinter</authors>
        <owners>codingfreaks</owners>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <description>This package is a test for the codingfreaks blog post describing NuGet.</description>
        <summary>This is a simple NuGet test</summary>
        <releaseNotes>This is the initial release.</releaseNotes>
        <copyright>codingfreaks 2014</copyright>
    </metadata>
    <files>
        <file src="lib\MyLogic.dll" target="lib\MyLogic.dll" />
    </files>
</package>

Das Problem ist hervorgehoben. Der virtuelle Ordner „lib“ muss bei src ersetzt werden. Zeile 16 würde dann so aussehen:

<file src="bin\ReleaseNuget\MyLogic.dll" target="lib\MyLogic.dll" />

Das muss man für alle Dateien des Paketes nachziehen.

csproj-Datei des Projektes editieren

Jetzt kommt der schwierigste Schritt: Die Einbindung der in der targets-Datei bereit gestellten Schritte in die eigene Projektdatei. Als erstes öffnet man die csproj-Datei in einem Editor oder über Rechtsklick im Solution-Explorer und dann „Unload“ gefolgt von erneutem Rechtsklick und „Edit..“ im Studio direkt. Direkt unterhalb des „“-tags fügt man nun eine Referenz auf die targets-Datei ein:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <Import Project="$(SolutionDir)\.nuget\codingfreaks.nuget.targets" Condition="Exists('$(SolutionDir)\.nuget\codingfreaks.nuget.targets')" />
  <PropertyGroup>

Etwas weiter unten in dieser Datei befinden sich immer schon 2 auskommentierte Einträge für die Build-Targets „BeforeBuild“ und „AfterBuild“. Das Ende der Datei sollte nach der Anpassung so aussehen:

  <Target Name="BeforeBuild" DependsOnTargets="cfUpgradeVersion">
  </Target>
  <Target Name="AfterBuild" DependsOnTargets="cfNugetPackage">
  </Target>
  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
      <ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
    </PropertyGroup>
    <Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
  </Target>
</Project>

Die markierten Zeilen sind zu bearbeiten. Wer die folgenden Zeilen nicht sieht, hat wahrscheintlich NuGet Package Restore noch nicht eingebunden.

Wer jetzt das entladene Projekt im Solution Explorer rechts anklickt, „Reload“ wählt und dann in der Konfiguration „Debug“ baut, sollte im Output u,a, folgende Meldung sehen:

1>cfUpgradeVersion:
1>  Upgrading version excluding build.

Diese beiden Zeilen entstehen, weil wir im Standard selbst dann etwas tun, wenn wir kein Package erstellen. Wir setzen in jedem Fall vor jedem Build die Revisions-Nummer um eins nach oben, wie man durch einen Blick in die AssemblyInfo.cs leicht feststellen kann. Einfach mal diese Datei öffnen und mehrfach hintereinander „Rebuild“ machen.

Wer das nicht möchte, der kann in der AfterBuild aus Listing 8 einfach das DependsOnTargets-Attribut inkl. des Wertes entfernen. Dann ist die Versionierung wieder ausgeschaltet. Wer hier eigene Logiken fährt, kann das ohnehin tun.

Für alle anderen ist das der erste Schritt zum Glück. In jeder Einstellung, die nicht zu einem Packaging führt (also z.B. „Debug“) wird nur der letzte Teil der Version hochgezählt. Wenn man in der „ReleaseNuget“ oder welcher anderen festgelegten Config auch immer baut, wird zusätzlich auch die Build-Nummer um eins erhöht.

Sobald man nun einfach die Konfiguration auf ReleaseNuget umstellt sollte im Ordner (bei mir c:\nuget) auch entsprechend ein neues Package entstehen.

Bekannte Fehler

Aktuell sind folgende Probleme mit der ersten Version bekannt:

  • Leerzeichen in den Pfadangaben führen zu Problemen
  • Es können keine Versionskommentare erzeugt werden
  • Uploads zu anderen Quellen, als Freigaben im Dateisystem funktionieren noch nicht
  • Die readme ist lückenhaft

EDIT: Inzwischen sind einige der Fehler behoben und das Paket steht in einer neueren Version bereit.

Wir wollen das Projekt Stück für Stück verbessern und werden immer wieder Updates heraus bringen.

Fazit

Selbst, wenn der ein oder andere sagen wird, dass er unsere Implementierung nicht 1:1 in seine bestehenden Projekte aufnehmen kann, denken wir uns, dass mit unserem Package zumindest so eine Art HowTo für das beschriebene Szenario entsteht. Wir werden in einem der folgenden Artikel noch auf das NuGet-package selbst eingehen, weil da einige Techniken enthalten sind, die womöglich von Interesse sein können (Stichwort: Solution-Ordner anlegen). Für Feedback und Anregungen sind wir wie immer dankbar.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.