18. Sep 2017
Lesedauer 8 Min.
Ein Installer für .NET
Squirrel
Das Setup-Framework Squirrel vereint die Vorteile von ClickOnce mit einem umfangreichen API.

Die Installation? Stimmt, da war was. Auf der einen Seite wollen Anwender Windows-Anwendungen schnell und einfach installieren. Auf der anderen Seite sollte für die Entwickler die Erstellung eines Installers und von Programm-Updates unkompliziert sein. Gegensätzlich? Unvereinbar?Das Erreichen dieser beiden Ziele haben sich die Macher von Squirrel [1] gesetzt, die ihr Tool mit dem Spruch „Squirrel: It’s like ClickOnce but Works“ beschreiben. Squirrel ist eine Bibliothek und eine Sammlung von Tools, um sowohl Installation als auch Updates von Windows-Anwendungen zu verwalten. Unter .NET besteht die Möglichkeit, innerhalb der Anwendung auf die zu installierenden Updates Einfluss zu nehmen. Doch welche Möglichkeiten bietet Squirrel und wie lässt es sich in die eigene .NET-Anwendung integrieren?Hier einige Features ohne Vollständigkeitsgarantie:
- Überprüfung auf vorhandene Updates und Bereitstellung eines Change-Logs,
- Eingreifen in die Installations- und Update-Routinen,
- Delta-Pakete zur Speicherplatzreduzierung,
- Möglichkeit zur Integration in den Build-Prozess zur automatisierten Erstellung des Setups,
- keine UAC-Dialoge (User Account Control),
- keine Neustarts,
- Aktualisierung, während die Anwendung läuft.
Überblick
Sie binden Squirrel am einfachsten über das NuGet-Paket squirrel.windows in das eigene Projekt ein. Beim Schreiben des Artikels war Version 1.6.0 aktuell. In dieser Version ist jedoch ein Bug beim Anlegen der Desktop-Verknüpfung vorhanden, sodass entweder die etwas ältere Version 1.4.4 [2] oder die neuere Version 1.7.7 verwendet werden muss. Der Fix zu dem Problem ist unter [3] dokumentiert. Mit der Installation des NuGet-Pakets werden zudem mehrere benötigte Bibliotheken installiert, wie zum Beispiel DeltaCompressionDotNet oder Mono.Cecil.Für eine vollständige Integration von Squirrel in eine .NET-Anwendung sind folgende Aspekte zu betrachten:- Integration: Sie binden Squirrel in die Anwendung ein.
- Paketierung: Sie führen die Dateien zu einem Setup-Paket zusammen, das verteilt werden kann.
- Verteilung: Sie stellen die Setup-Datei(en) an einem für den Benutzer erreichbaren Ort bereit.
- Installation: Sie installieren die Anwendung auf dem Rechner des Benutzers.
- Aktualisierung: Eine neue Version der Anwendung wird bereitgestellt.
Integration
Die zentrale Klasse, über die der Update-Prozess gesteuert wird, ist UpdateManager. Diese Klasse nimmt im Konstruktor den Pfad, in dem die Updates liegen, entgegen. Danach kann beispielsweise über die CheckForUpdate-Methode geprüft werden, ob zu installierende Updates vorhanden sind.Listing 1: Prüfung auf zu installierende Updates
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CheckForUpdate_Click</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">object</span> sender, RoutedEventArgs e</span>) </span>{<br/> <span class="hljs-keyword">try</span> { <br/> <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> mgr = <br/> <span class="hljs-keyword">new</span> UpdateManager(UPDATE_FOLDER)) { <br/> UpdateInfo info = <span class="hljs-keyword">await</span> mgr.CheckForUpdate(); <br/> <span class="hljs-keyword">if</span> (info.ReleasesToApply.Any()) { <br/> <span class="hljs-keyword">string</span> availableUpdates = <span class="hljs-keyword">string</span>.Join(<span class="hljs-string">"\n"</span>, <br/> info.ReleasesToApply.OrderBy(x =&gt; x.Version)<br/> .Select(x =&gt; x.Version.ToString())); <br/> LogText(<span class="hljs-string">"Update(s) available:\n"</span> + <br/> availableUpdates); <br/> SetReleaseNotes(info); <br/> } <br/> <span class="hljs-keyword">else</span> { <br/> LogText(<span class="hljs-string">"No update available"</span>); <br/> } <br/> } <br/> } <br/> <span class="hljs-keyword">catch</span> (Exception ex) { <br/> LogText(<span class="hljs-string">"Error during check for updates: "</span> + <br/> ex.Message); <br/> } <br/>}
Diese Methode gibt ein Objekt der Klasse UpdateInfo zurück, das unter anderem die anzuwendenden Updates in der Property ReleasesToApply bereitstellt. Hierüber lassen sich zudem die Release-Notes abfragen. Eine beispielhafte Implementierung einer Routine zum Überprüfen auf vorhandene Updates kann Listing 1 entnommen werden. Das Auslesen der Release-Notes ist in Listing 2 dargestellt.
Listing 2: Ermittlung der Release-Notes
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">SetReleaseNotes</span>(<span class="hljs-params">UpdateInfo info</span>) </span>{ <br/> <span class="hljs-keyword">var</span> releaseNotes = info.FetchReleaseNotes(); <br/> StringBuilder sb = <span class="hljs-keyword">new</span> StringBuilder(); <br/> <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> entry <span class="hljs-keyword">in</span> releaseNotes.Keys<br/> .OrderByDescending(x =&gt; x.Version)) { <br/> sb.AppendLine(<span class="hljs-string">"Version "</span> + entry.Version); <br/> sb.AppendLine(<span class="hljs-string">"~~~~~~~~~~~~~~~~~~~~~~~~~~"</span>); <br/> sb.AppendLine(releaseNotes[entry]); <br/> } <br/> LogText(sb.ToString()); <br/>}
Für das Herunterladen und Anwenden von Updates ist ebenfalls der UpdateManager zuständig. Dieser bietet die Möglichkeit, mit nur einem Aufruf der UpdateApp-Methode die Anwendung zu aktualisieren. Um das Herunterladen und Anwenden von Updates zu trennen, können alternativ aber auch die Methoden DownloadReleases und ApplyReleases hintereinander aufgerufen werden (siehe Listing 3).
Listing 3: Update herunterladen und anwenden
private async void DownloadUpdate_Click(<br/> object sender, RoutedEventArgs e) {<br/> <span class="hljs-keyword">try</span> <br/> { <br/> using (var mgr = <span class="hljs-keyword">new</span> UpdateManager(UPDATE_FOLDER)){ <br/> UpdateInfo info = <span class="hljs-keyword">await</span> mgr.CheckForUpdate(); <br/> <span class="hljs-regexp">//</span> download <span class="hljs-keyword">and</span> install updates with a single <br/> <span class="hljs-regexp">//</span> line <span class="hljs-keyword">of</span> code <br/> <span class="hljs-regexp">//</span><span class="hljs-keyword">await</span> mgr.UpdateApp(); <br/> <span class="hljs-regexp">//</span> <span class="hljs-keyword">if</span> you want to separate download <span class="hljs-keyword">and</span> <br/> <span class="hljs-regexp">//</span> installation, call these methods separately <br/> LogText(<span class="hljs-string">"Start downloading update(s)"</span>); <br/> <span class="hljs-keyword">await</span> mgr.DownloadReleases(info.ReleasesToApply); <br/> LogText(<span class="hljs-string">"Applying update(s)"</span>); <br/> <span class="hljs-keyword">await</span> mgr.ApplyReleases(info); <br/> LogText(<span class="hljs-string">"Update finished. Please restart the </span><br/><span class="hljs-string"> application"</span>); <br/> } <br/> } <br/> <span class="hljs-keyword">catch</span> (Exception ex) { <br/> LogText(<span class="hljs-string">"Error during update: "</span> + ex.Message); <br/> } <br/>}
Paketierung
Nachdem die Anwendung für eine Aktualisierung mit Squirrel vorbereitet wurde, muss diese nun in ein Setup-Paket überführt werden. Squirrel nutzt NuGet-Pakete, sodass hierfür der NuGet Package Explorer verwendet werden kann. Eine Integration in den Visual-Studio-Buildprozess wird später vorgestellt. Für die Beispielanwendung entsteht ein neues NuGet-Paket und wird mit den gewünschten Metadaten befüllt. Die Binärdateien der Anwendung müssen, unabhängig davon, für welche .NET-Version die Anwendung geschrieben wurde, im Ordner lib\net45 abgelegt werden. Dieser Ordner lässt sich über das Kontextmenü anlegen.
Das fertige NuGet-Paket der Beispielanwendung zeigt Bild 2. Es sind alle Dateien hinzuzufügen, die sowohl zur Anwendung als auch zu Squirrel gehören.Bei der Versionsnummer ist Vorsicht geboten, da diese nur drei Teile haben darf. Das ist technisch bedingt [4].Nachdem das NuGet-Paket erstellt ist, folgt nun die Bereitstellung dieses Pakets. Das erreichen Sie durch den Befehl
<span class="hljs-selector-tag">Squirrel</span> <span class="hljs-selector-tag">--releasify</span> <span class="hljs-selector-tag">dotnetproSquirrel</span><span class="hljs-selector-class">.1</span><span class="hljs-selector-class">.0</span><span class="hljs-selector-class">.0</span><span class="hljs-selector-class">.nupkg</span>
in der Package Manager Console. Das Kommando Squirrel bezieht sich auf Squirrel.exe, das bei der Installation des Squirrel-NuGet-Pakets in die Anwendung mitgeliefert wird. Der Dateiname ist dem Paket entsprechend anzupassen. Sollte ein Fehler erscheinen, dass das Kommando Squirrel nicht gefunden wird, reicht ein Neustart von Visual Studio.Der Release-Schritt führt zu folgendem Output:
- Releases-Order: Squirrel legt einen Ordner Releases standardmäßig unterhalb des Solution-Ordners an.
- Setup.exe: Es entsteht eine Setup-Datei mit der aktuellsten Version der Anwendung.
- RELEASES-Datei: Die Textdatei enthält eine Auflistung aller Releases, die während des Aktualisierungsvorgangs verwendet werden.
- dotnetproSquirrel.1.0.0-full.nupkg: Dies ist das erstellte NuGet-Paket mit dem vollständigen Inhalt.
- dotnetproSquirrel.*.*.*-delta.nupkg: Bei einer neuen Version wird eine Delta-Datei mit der Differenz zum vorherigen Paket erstellt, um die Update-Größe so gering wie möglich zu halten.
Verteilung
Das durch Squirrel.exe erstellte und gefüllte Releases-Verzeichnis kann nun auf einem Netzlaufwerk oder im Internet bereitgestellt werden. Den Inhalt des Verzeichnisses für die Beispielanwendung mit zwei Updates zeigt Bild 3. Eine Bereitstellung auf eigenem Webspace ist ebenso möglich wie die Bereitstellung in Amazon S3 [5] oder Microsoft Azure.Installation
Die Erstinstallation erfolgt durch die über den Squirrel --releasify-Schritt erstellte Setup.exe. Während des Installationsprozesses laufen folgende Schritte ab:- Erstellung des Verzeichnisses %LocalAppData%\dotnet
proSquirrel. - Erstellung des Unterverzeichnisses dotnetproSquirrel-1.0.0 für die Version 1.0.0 und Ablage aller Dateien des NuGet-Pakets in diesem Ordner.
- Ausführen der Datei dotnetproSquirrel-1.0.0\dotnetpro
Squirrel.exe.
Aktualisierung
Um eine neue Version der Anwendung bereitzustellen, muss in einem ersten Schritt die AssemblyVersion hochgesetzt werden, zum Beispiel auf 1.0.1. Danach sind die Schritte aus dem Abschnitt Paketierung durchzuführen. Dies hat folgende Auswirkungen:- Aktualisierung der Setup.exe, die nun die jetzt aktuelle Version enthält.
- Hinzufügen des neuen NuGet-Pakets mit dem kompletten Inhalt.
- Erzeugung eines Delta-Pakets mit den Unterschieden zur Vorversion.
- Erweiterung der RELEASES-Datei um die neue Version.
Visual-Studio-Build-Integration
Das manuelle Erstellen der NuGet-Pakete und der Aufruf von Squirrel über die Kommandozeile sind lästige Schritte, die Sie nicht notwendigerweise manuell ausführen müssen. Es besteht auch die Möglichkeit, diese beiden Schritte in den Build-Prozess von Visual Studio zu integrieren. Es können wahlweise beide Schritte oder auch nur das Erstellen des NuGet-Pakets integriert werden.Listing 4: Visual-Studio-Build-Integration
<span class="hljs-tag">&lt;<span class="hljs-name">ItemGroup</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">NuGetCommandLine</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">"..\packages\</span></span><br/><span class="hljs-tag"><span class="hljs-string"> NuGet.CommandLine.*\tools\nuget.exe"</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">InProject</span>&gt;</span>False<span class="hljs-tag">&lt;/<span class="hljs-name">InProject</span>&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">NuGetCommandLine</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">Squirrel</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">"..\packages\Squirrel.Windows.*</span></span><br/><span class="hljs-tag"><span class="hljs-string"> \tools\squirrel.exe"</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">InProject</span>&gt;</span>False<span class="hljs-tag">&lt;/<span class="hljs-name">InProject</span>&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">Squirrel</span>&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">ItemGroup</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">Target</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"AfterBuild"</span> <span class="hljs-attr">Condition</span>=<span class="hljs-string">" '</span></span><br/><span class="hljs-tag"><span class="hljs-string"> $(Configuration)' == 'Release'"</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">GetAssemblyIdentity</span> <span class="hljs-attr">AssemblyFiles</span>=<span class="hljs-string">"$(TargetPath)"</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">Output</span> <span class="hljs-attr">TaskParameter</span>=<span class="hljs-string">"Assemblies"</span> </span><br/><span class="hljs-tag"> <span class="hljs-attr">ItemName</span>=<span class="hljs-string">"myAssemblyInfo"</span> /&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">GetAssemblyIdentity</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">Exec</span> <span class="hljs-attr">Command</span>=<span class="hljs-string">""</span>@(<span class="hljs-attr">NuGetCommandLine-</span>&gt;</span>'%(FullPath)')" <br/> pack dotnetproSquirrel.nuspec -Version <br/> %(myAssemblyInfo.Version) -Properties <br/> Configuration=Release -OutputDirectory <br/> $(OutDir)..\..\..\Install -BasePath $(OutDir)" /&gt; <br/> <span class="hljs-tag">&lt;<span class="hljs-name">Exec</span> <span class="hljs-attr">Command</span>=<span class="hljs-string">""</span>@(<span class="hljs-attr">Squirrel-</span>&gt;</span>'%(FullPath)')" <br/> --releasify $(OutDir)..\..\..\Install<br/> \dotnetproSquirrel.$([System.Version]::Parse(<br/> %(myAssemblyInfo.Version)).ToString(3)).nupkg <br/> --releaseDir=$(OutDir)..\..\..\Releases" /&gt;<br/> <span class="hljs-tag">&lt;/<span class="hljs-name">Target</span>&gt;</span>
Legen Sie dazu zunächst ein Build-Target wie in Listing 4 dargestellt an. Bearbeiten Sie hierzu die .csproj-Datei am besten in einem Texteditor. Das Beispiel zeigt einen Build-Schritt, der im Release-Build ausgeführt wird und in dem zwei Kommandos ausgeführt werden. Das erste Kommando erzeugt anhand einer .nuspec-Datei ein NuGet-Paket. Eine solche .nuspec-Datei enthält die für das Erstellen des NuGet-Pakets notwendigen Metainformationen, wie zum Beispiel Autor, Titel und Beschreibung, ebenso aber auch die hinzuzufügenden Dateien (Beispiel siehe Listing 5) . Damit Visual Studio den NuGet-Befehl erkennt, muss das NuGet.CommandLine-Package installiert werden. Erscheint beim Build eine Fehlermeldung zu Fehlercode 9009, reicht ein Neustart von Visual Studio oder das Öffnen der Package Manager Console.
Listing 5: Definition des NuGet-Paketinhaltes für die Build-Integration
<span class="php"><span class="hljs-meta">&lt;?</span>xml version=<span class="hljs-string">"1.0"</span> encoding=<span class="hljs-string">"utf-8"</span><span class="hljs-meta">?&gt;</span></span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">package</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://schemas.microsoft.com</span></span><br/><span class="hljs-tag"><span class="hljs-string"> /packaging/2010/07/nuspec.xsd"</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">metadata</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>dotnetproSquirrel<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span> <br/> <span class="hljs-comment">&lt;!-- version will be replaced by MSBuild --&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>0.0.0.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>title<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">authors</span>&gt;</span>authors<span class="hljs-tag">&lt;/<span class="hljs-name">authors</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>description<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">requireLicenseAcceptance</span>&gt;</span>false<br/> <span class="hljs-tag">&lt;/<span class="hljs-name">requireLicenseAcceptance</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">copyright</span>&gt;</span>Copyright 2017<span class="hljs-tag">&lt;/<span class="hljs-name">copyright</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span> /&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">metadata</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">files</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">file</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"*.*"</span> <span class="hljs-attr">target</span>=<span class="hljs-string">"lib\net45\"</span> </span><br/><span class="hljs-tag"> <span class="hljs-attr">exclude</span>=<span class="hljs-string">"*.pdb;*.nupkg;*.vshost.*;*.log"</span>/&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">file</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"**\*.*"</span> <span class="hljs-attr">target</span>=<span class="hljs-string">"lib\net45\"</span> </span><br/><span class="hljs-tag"> <span class="hljs-attr">exclude</span>=<span class="hljs-string">"*.pdb;*.nupkg;*.vshost.*;*.log"</span>/&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">files</span>&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">package</span>&gt;</span>
Das zweite Kommando führt den Releasify-Schritt von Squirrel aus und aktualisiert den Releases-Ordner mit einer neuen Setup.exe und den Komplett- und Delta-Paketen.
Fazit
Squirrel hat viel Ähnlichkeit zu ClickOnce, bietet jedoch insbesondere für den Entwickler einige interessante Features, um in den Installationsprozess einzugreifen. Das macht es sehr attraktiv, die eigene Anwendung damit zu verteilen.Fussnoten
- GitHub-Repository, http://www.dotnetpro.de/SL1710Squirrel1
- Falsches Verknüpfungstarget, http://www.dotnetpro.de/SL1710Squirrel2
- Korrektur falsches Verknüpfungstarget, http://www.dotnetpro.de/SL1710Squirrel3
- Versionsnummern-Format, http://www.dotnetpro.de/SL1710Squirrel4
- Amazon S3, http://www.dotnetpro.de/SL1710Squirrel5