Burn your Application

Artikel von Ivan Appert und Stefan Wermelinger in der Zeitschrift windows.developer Nr. 1/2014:

pdf «Burn your application» als pdf lesen

Mit Burn bietet das WiX Toolset ein einfaches, aber mächtiges Tool zur Erstellung moderner und flexibler Installationsprogramme für Windows. Burn erlaubt die einfache Installation von Abhängigkeiten, die für die eigene Software unabdingbar sind. Zusätzlich kann die Installer-Oberfläche komplett neu geschrieben und somit vom alten Korsett aus Windows-XP-Zeiten befreit werden.

Das WiX Toolset hat seit seinem Entwicklungsstart 2004 viele Fortschritte gemacht. Aus einer XML-Abstraktion für MSI-Pakete hat sich ein eigenes Ökosystem entwickelt, das heute alle Installationsanforderungen erfüllen kann. Wenn es um die Benutzeroberfläche oder die Installation von Abhängigkeiten geht, war man allerdings bisher ziemlich eingeschränkt. In der Version 3.6 des WiX Toolsets gibt es nun mit Burn elegante Mechanismen, diese Einschränkungen zu überwinden. Burn erlaubt mit seiner Bootstrapper-Funktionalität die Implementierung eigener Installationsoberflächen und übernimmt die Kontrolle des Installationsablaufs.

Das WiX Toolset bietet eine entwicklerfreundliche Integration in Visual Studio, die Unterstützung von Continuous Integration und Bibliotheken zur Einbindung von Standardaktionen in eigene Installationspakete. Unter [1] kann das WiX Toolset direkt heruntergeladen werden. Eine ausführliche Dokumentation und weitere Informationen zum Projekt sind unter [2] zu finden.

Zur Veranschaulichung stellen wir uns folgendes Szenario vor: Wir haben ein Projekt mit dem Namen Application. Der Einfachheit halber besteht es nur aus Textdateien, die auf dem Zielrechner installiert werden sollen. Weiter haben wir eine Vorbedingung PrerequisiteApplication, ebenfalls ein aus Textdateien bestehendes Projekt. Als Letztes gibt es noch ein Projekt ExtraApplication, zu dem unsere Application keine direkte Abhängigkeit besitzt, das aber auch mit installiert werden soll. Falls seine Installation fehlschlägt, soll die Installation der Application und ihrer Abhängigkeiten nicht rückgängig gemacht werden.

In Abbildung 1 sehen wir die Struktur unserer Applikationen. Man kann sich unschwer vorstellen, dass die Projekte in einer realen Situation vermutlich sehr viel größer sind und möglicherweise sogar unabhängig voneinander entwickelt werden. Der Einfachheit halber sind alle drei Projekte unseres Szenarios in einer einzigen Visual-Studio-Solution erstellt worden. Die komplette Lösung ist auf GitHub unter [3] öffentlich bereitgestellt.
Applikationsstruktur

Abbildung 1: Applikationsstruktur

MSI-Pakete mit WiX

Nach der Installation des WiX Toolset kann direkt im Visual Studio ein neues Installationsprojekt erstellt werden. Wir werden für das Projekt Application exemplarisch aufzeigen, wie ein einzelnes MSI-Paket erstellt wird. In unserer Solution fügen wir ein neues Projekt hinzu: ein Set-up-Projekt namens ApplicationInstaller. Dabei wird bereits die Datei Product.wxs mit einem Grundgerüst für den Installer erzeugt. Um uns die Arbeit zu erleichtern, fügen wir zudem eine Projektreferenz zu Application hinzu. So können wir innerhalb des Set-up-Projekts auf die zu installierenden Ressourcen verweisen.

Der konzeptionelle Aufbau eines MSI-Pakets sieht so aus, dass man ein Product definiert, bestehend aus einem bis mehreren Features. Ein Feature enthält Komponenten. Eine Komponente beinhaltet eine einzelne Datei, einen Registry-Eintrag usw. (Abb. 2).

WiXOverview

Abbildung 2: Konzeptioneller Aufbau eines MSI-Pakets

Zuallererst definieren wir, welche Eigenschaften unser Programm hat. Dazu benutzen wir den Block Product, der den Namen des zu installierenden Produkts sowie dessen Version und IDs enthält. Das Attribut Upgrade-Code des Produkts ist essenziell: Es muss eine für jede Software eindeutige GUID sein und darf, wenn einmal veröffentlicht, nicht mehr verändert werden. Diese GUID dient zur Identifikation des Produkts durch den Windows Installer und ermöglicht unter anderem Upgrades unseres installierten Produkts:

Product
Id="*"
Name="Windows Developer Application"
Language="1033"
Version="1.0.0.0"
Manufacturer="bbv Software Services AG"
UpgradeCode="{GUID}">

In einem separaten Fragment definieren wir, wie die Ordnerstruktur auf dem Zielrechner erstellt werden soll. Im Programmverzeichnis des Betriebssystems soll ein Ordner Windows Developer Application mit einem Unterordner Files angelegt werden:

<!– Step 1: Define directory structure –>
<Directory Id=”TARGETDIR” Name=”SourceDir”>
<Directory Id=”ProgramFilesFolder”>
<Directory Id=”INSTALLFOLDER” Name=”Windows Developer Application”>
<Directory Id=”Files” Name=”Files”/>
</Directory>
</Directory>
</Directory>

Jetzt können wir die Komponenten definieren. Um die Upgrade- und Repair-Fähigkeit der Installation zu gewährleisten, verpacken wir jede zu installierende Datei in eine Komponente. In diesem Fall bündeln wir die Komponenten in einer Komponentengruppe:

<!– Step 2: Components of the application –>
<ComponentGroup Id=”ProductComponents” Directory=”INSTALLFOLDER”>
<Component Id=”Main” Guid =”{GUID}”>
<File
Id=”Main.txt”
Source=”$(var.Application.TargetDir)Main.txt”
KeyPath=”yes”/>
</Component>
</ComponentGroup>

Im letzten Schritt müssen wir diese Komponenten noch einem Feature zuordnen. Ein Feature ist ein Bestandteil einer Applikation und kann unabhängig von anderen Features installiert werden. Falls eine Oberfläche für den Installer verwendet wird, kann man die Auswahl der zu installierenden Features auch dem Benutzer überlassen. Wir fügen unserem einzigen Feature ProductFeature die beiden Komponentengruppen Main und Files hinzu:

<!– Step 3: Features to install –>
<Feature Id=”ProductFeature” Title=”Application”
<ComponentGroupRef Id=”ProductComponents” />
<ComponentGroupRef Id=”Files”/>
</Feature>

Nun haben wir einen funktionsfähigen Installer für unser Projekt Application erstellt. Auf dieselbe Weise verfahren wir mit den beiden anderen Projekten PrerequisiteApplication und ExtraApplication. Als Resultat haben wir drei unabhängige MSI-Pakete.

Vor- und Nachbedingung mit Burn installieren

Wenn nun aber gleichzeitig mehrere Installationspakete installiert werden müssen, reicht die MSI-Kernfunktionalität nicht mehr aus. Burn schließt diese Lücke mit der Bootstrapping- und Chaining-Funktionalität. In Visual Studio erstellen wir dazu wieder ein neues Projekt, diesmal vom Typ Bootstrapper Project, denn wir wollen die Installationspakete zusammenbündeln. Folgendes Code-Snippet zeigt den Aufbau eines Burn Bundles:

<Bundle Name=”Windows Developer”
Version=”2.1.0.0″
Manufacturer=”bbv Software Services AG”
UpgradeCode=”{GUID}”>
<BootstrapperApplicationRef Id=<yourBootstrapperApplication> />
<Chain>
<!– Your installation sequence –>
</Chain>
</Bundle>

Das Bundle-Element übernimmt die Bündelung der beiden Funktionalitäten und definiert die Applikationseinstellungen, also den Namen, die Version und den UpgradeCode zur Identifikation des Produkts. Im Element BootstrapperApplicationRef wird die gewünschte Bootstrapper-Applikation hinterlegt. Im Element Chain wird die Installationsreihenfolge festgelegt. Schauen wir uns die einzelnen Elemente genauer an.

Bootstrapping

Das Bootstrapping-Element beinhaltet die Referenz zur verwendeten Bootstrapper-Applikation. Mit Version 3.6 des WiX Toolset unterstützt Burn zwei verschiedene Bootstrapper-Applikationen von Haus aus, nämlich HyperlinkLicense und RtfLicense. Der Hyperlink-Bootstrapper wird folgendermaßen eingebunden:

<BootstrapperApplicationRef
Id=”WixStandardBootstrapperApplication.HyperlinkLicense”>
<bal:WixStandardBootstrapperApplication
LicenseUrl=”path to License.rtf”
ThemeFile =” path to HyperlinkTheme.xml”
LogoFile=” path to bbvLogo.jpg” />
</BootstrapperApplicationRef>

Über das Id-Attribut wird der gewünschte Bootstrapper referenziert. Zusätzlich können Einstellungen, etwa in Hinblick auf das Logo oder den Link zum Lizenztext, über das Unterelement WixStandardBootstrapperApplication vorgenommen werden. Wenn man das Installationsprojekt kompiliert, erhält man bereits einen angepassten Installer im vordefinierten Layout (Abb. 3).

BootstrapperMitHyperlinkLicense

Abbildung 3: Bootstrapper mit „HyperlinkLicense“

Chaining

Schließlich definieren wir die Chain – wir bestimmen also, welche Pakete in welcher Reihenfolge installiert werden. Zuerst wird die Voraussetzung unserer Applikation, der PrerequisiteApplicationInstaller, ausgeführt. Anschließend folgt die eigentliche Applikation, der ApplicationInstaller.

Als Letztes wird der ExtraApplicationInstaller ausgeführt. Vor diesem gibt es eine Rollback Boundary, die im Falle eines Rollbacks der Installation die vor der Boundary installierten Pakete nicht wieder deinstallieren würde. Wenn die Installation der ExtraApplication fehlschlägt, werden die bereits installierten Produkte Application und PrerequisiteApplicationauf dem System belassen, während das Paket ExtraApplication wieder sauber entfernt wird.

Die einzelnen MSI-Pakete werden im Quiet-Modus installiert, also ohne ihre eigenen Benutzeroberflächen. So bekommt der Benutzer nichts von den einzelnen MSI-Paketen mit; ihm präsentiert sich lediglich eine einzelne, einheitliche Oberfläche. Diese sieht noch relativ rudimentär aus. Im Folgenden werden wir zeigen, wie auf einfache Art und Weise eine individuelle Benutzeroberfläche für den Installationsassistenten bereitgestellt werden kann.

Burn mit eigener Installationsoberfläche

Wir haben bisher gesehen, wie mit Burn ein komplettes Installationsprogramm mit Standard-Bootstrapper und Chaining-Mechanismus kreiert werden kann. Neben dem Standard-Bootstrapper bietet Burn die Möglichkeit, eigene Bootstrapper-Applikationen zu schreiben, um die Oberfläche und Benutzerführung perfekt auf das zu installierende Produkt abzustimmen. Wir werden nun schrittweise eine auf WPF basierende Installationsoberfläche implementieren.

In unserem Beispiel werden wir die Szenarien Installation und Deinstallation abdecken. Zuerst legen wir in Visual Studio ein neues C#-Class-Library-Projekt an und referenzieren die DLL BootstrapperCore.dll aus dem WiX Toolset. Als Einstiegspunkt dient die abstrakte Klasse BootstrapperApplication, die die Brückenfunktion zwischen der Burn Engine und unserem Bootstrapper übernimmt. Von ihr leiten wir eine neue Klasse CustomBootstrapperApplication ab, die die Implementierung des Bootstrappers enthalten wird. Mithilfe eines Eintrags im Assembly-Info wird sie als Einstiegspunkt für die Burn Engine deklariert. Das wird folgendermaßen erreicht:

[assembly: BootstrapperApplication(typeof(CustomBootstrapperApplication))]

Bevor wir mit der eigentlichen Implementierung unseres Bootstrapper beginnen können, muss zusätzlich noch die BootstrapperCore.Config-Datei zum Projekt hinzugefügt werden. Im SDK-Unterorder des installierten WiX Toolsets ist eine Vorlage zu finden, die folgendermaßen angepasst wird:

<wix.bootstrapper>
<host assemblyName=”WiXArtikelBootstrapper”/>
</wix.bootstrapper>

Der Host Assembly-Eintrag dient zur Identifikation der eigenen Bootstrapper-DLL, die von der Burn Engine während der Installation gestartet wird. Das jetzt bestehende Projekt (Abb. 4) beinhaltet alle zwingend notwendigen Elemente für den eigenen Bootstrapper.

custom_bootstrapper_initial_project

Abbildung 4: Projektstruktur für einen eigenen Bootstrapper

abb5

Jetzt kann mit der Implementierung des Custom Bootstrappers begonnen werden. Wie vorher erwähnt, leitet die Klasse CustomBootstrapperApplication von der abstrakten Klasse BootstrapperApplication ab (Listing 2). Die Methode Run muss zwingend implementiert werden, da diese von der Burn Engine aufgerufen wird, um die Installation durchzuführen. Folgende Aktionen müssen ausgeführt werden:

  1. Detektieren, welche Komponenten bereits installiert sind oder installiert werden müssen

2. Planung der Installation für die fehlenden Komponenten

3. Ausführung der nötigen Schritte

Über die Engine-Eigenschaft der BootstrapperApplication-Klasse kann direkt auf die Burn Engine zugegriffen werden. Das Detektieren des aktuellen Systemzustands wird folgendermaßen ausgeführt:

this.Engine.Detect();

Es besteht die Möglichkeit, über das Abonnieren von Events der Basisklasse Einfluss auf einzelne Phasen und Ereignisse der Installation zu nehmen. Der nächste Schritt besteht in der Planung der nötigen Aktionen. Dazu wird wieder die Burn Engine benutzt:

this.engine.Plan(LaunchAction.Install)

Über den Methodenparameter wird die auszuführende Aktion festgelegt. Wenn die Planung beendet ist, wird das PlanComplete-Event ausgelöst. War die Planung erfolgreich, kann die Installation mit folgendem Befehl ausgeführt werden:

this.bootstrapperApplication.Engine.Apply(this.ViewWindowHandle);

Als Methodenparameter muss die Referenz zur angezeigten View übergeben werden, die bei Instanziierung der Installationsoberfläche erstellt wird und zwischengespeichert werden muss. Ohne Referenz zur View kann die Burn Engine nicht mit der Installationsoberfläche interagieren. Die Methode Apply führt alle vorher geplanten Aktionen durch und feuert am Ende das Event ApplyComplete. Beendet wird die Burn Engine mit dem Aufruf der Quit-Methode.

Dieser Ablauf muss nun in eine Oberfläche integriert werden, in unserem Fall eine WPF-Oberfläche. Anhand des Parameters, mit dem unsere Installationsapplikation aufgerufen wurde, wird entschieden, ob eine Installation oder eine Deinstallation vorgenommen wird. Ist die definierte Aktion Install, wird die Installationsoberfläche angezeigt, ansonsten die Deinstallationsoberfläche.

Grundsätzlich sollte die Entscheidung, welche Aktion durchgeführt wird, aber nicht auf den Aufrufparameter abgestützt werden. Stattdessen sollte das Resultat vom Detektieren des Systemzustands abhängig sein, damit im gegebenen Falle etwa ein Upgrade oder ein Repair durchgeführt werden kann.

Die oben beschriebenen Schritte werden in den jeweiligen ViewModels implementiert und ausgeführt. Zusätzlich werden, falls angebracht, Kommandos zur Ausführung von Benutzeraktionen verwendet. Zum Beispiel wird die Planung der Installation über ein Kommando gestartet, während das Abonnieren des PlanComplete-Events vom ViewModel übernommen wird. Das ViewModel führt auch direkt den Apply-Befehl aus. Die Oberfläche entspricht der in Abbildung 5. Es werden ein Logo und zwei Buttons dargestellt, einer zum Starten der Installation und einer zum Aufrufen der Herstellerwebseite. Das Kreieren, Zusammenhängen und Anzeigen der Oberfläche erfolgt direkt in der Methode ShowInstallView im Bootstrapper (Listing 2).

custom_bootstrapper_screenshot

Abbildung 5: Custom Bootstrapper mit Burn

Zwei Punkte sollten besonders beachtet werden: zum einen der Aufruf der Methode Initialize auf dem ViewModel und zum anderen die Bestimmung des View- WindowHandle. Wie bereits erwähnt, ist die View-Referenz notwendig zur Ausführung der Installation. Die Initialize-Methode ist für das Abonnieren von Events der Burn Engine verantwortlich, namentlich der Events PlanComplete und ApplyComplete.

Somit ist die Implementation des eigenen Bootstrappers komplett und er kann in unsere Installationsapplikation eingebunden werden. Dazu wird die Bundle-Definition wie folgt angepasst:

<BootstrapperApplicationRef Id=”ManagedBootstrapperApplicationHost”>
<Payload SourceFile=”path to CustomBootstrapperApplication.dll”/>
<Payload SourceFile=”path to BootstrapperCore.config”/></BootstrapperApplicationRef>

Anstatt den Standard-Bootstrapper über das Id-Attribut zu referenzieren, wird der ManagedBootstrapperApplicationHost neu referenziert und die eigene Bootstrapper-DLL mit dem Payload-Element übergeben. Über das Payload-Element müssen alle nötigen Dateien übergeben werden, die zusammen die Installationsapplikation ergeben.

Zusätzlich zur DLL der Applikation muss die BootstrapperCore.config übergeben werden. Wir haben an einem einfachen Beispiel die Implementierung eines eigenen Bootstrappers durchgespielt. Die Oberfläche des Bootstrappers und die Behandlung der Installationsszenarien kann beliebig ausgebaut werden.

Die Burn Engine bietet die Flexibilität, alle Zwischenschritte des Installationsprozesses abzufangen und mit eigenen Regelwerken anzureichern.

abb6

Fazit

Während das WiX Toolset alle Möglichkeiten des MSI bietet, geht man mit Burn noch einen Schritt weiter: Wir können Abhängigkeiten auf einfache Weise bündeln und gemeinsam wie gewünscht installieren lassen. Bei Bedarf kann die Installationsoberfläche selbst geschrieben und komplett den individuellen Bedürfnissen angepasst werden. Durch die Schlichtheit und Flexibilität dieser Technologie haben wir ein ideales Werkzeug für große und komplexe Produkte, deren Installation auch über längere Zeit gepflegt und gewartet werden muss.

Links & Literatur

[1] http://wix.codeplex.com/

[2] http://wixtoolset.org/

[3] https://github.com/iappert/BurnYourApplication

This entry was posted in Burn, Visual Studio, Windows, WiX Toolset. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *