CAKE Build - I

Xamarin Builds mit CAKE Build automatisieren - Teil 1

Versionsgeschichte
Version Aktualisierungsdatum Änderungen
0.1 27.11.2017 (Erste Fassung)
Nach einiger Zeit kommt bei der Entwicklung der Wunsch auf, das Build und die Tests zu automatisieren. Ich nutze dazu CAKE (C# Make), das mir die Arbeit deutlich erleichtert. Es gibt noch weitere Build Tools (wie FAKE oder PSake). CAKE hat für einen C# Entwickler den Vorteil, dass die Syntax bereits bekannt ist und man in Zweifel auch eine Erweiterung schreiben kann, ohne die geliebte Sprache wechseln zu müssen. Es ist ja einer der Gründe, warum Ihr mit Xamarin und nicht Nativ oder mit JavaScript basierten Frameworks Eure Apps schreibt.
In mehreren tutorials beschreibe ich Schritt für Schritt, wie Sie ein Xamarin-Projekt auf einen (oder mehreren) CI Server automatisiert erstellen könnt:
  • CAKE Einrichtung
  • Definition der notwendigen Schritte
  • Versionsnummern anpassen
  • Apps fürs Testen und App Stores erstellen
  • usw.

CAKE Einrichten

Am einfachsten funktioniert die Einrichtung mit dem Visual Studio Code Editor, der bei vielen C# Entwicklern bereits installiert ist. Wenn nicht installieren Sie zuerst den Editor (egal ob unter Windows oder macOS).

Im Editor, installieren Sie folgende zwei Erweiterungen, die die Arbeit mit CAKE erleichtern:

  • Cake
  • Cake Runner
Erweiterungen für CAKE
Erweiterungen unter VS Code für CAKE

Öffnen Sie im Editor nun den obersten Ordner Ihres Projektes (unter Windows über Datei | Ordner öffnen, unter macOS über Datei | Öffnen oder Command + O). In der Dateiansicht sollte nun die Ordnerstruktur des Projektes sichtbar sein. Für die Initialisierung installieren wir zuerst den Bootstrapper für CAKE, der NuGet und CAKE herunterlädt.

Aktivieren Sie dazu die Befehlspalette ... (über Anzeige | Befehlspalette ... oder unter Windows mit Strg + Umschalt + P oder unter macOS mit Command + Umschalt + P). Tippen Sie Cake ein. Es erscheint eine Auswahl an Cake-Befehlen.

Verfügbare CAKE-Befehle
Verfügbare CAKE-Befehle in VS Code

Nehmen Sie den Befehl Cake: install a bootstrap. Dieser bietet die Möglichkeit, eine Bootstrap-Datei für Windows (Powershell-Script build.ps1) oder für macOS/Linux (Bash-Script build.sh) zu erstellen. Wählen Sie die Option, die Sie benötigen.

Vergessen Sie nicht bei build.sh die Ausführungsrechte zu setzen, wenn Sie diese unter Windows erstellen, da sonst der Script nicht ausgeführt werden kann.

Für den ersten Beispelscript können Sie wieder über die Befehlspalette den Befehle Cake: install simple build file ausführen. Dieser legt die Datei build.cake an, in der wir alle notwendigen Schritte umsetzen werden.

Wir starten aber komplett neu und gehen Schritt für Schritt die einzelnen Stationen durch. Bei dem Script gelten folgende Konventionen (wie bei den meisten C# Projekten):

  • Scriptweite Variablen werden in pascalCase und mit einen vorangestellten _ benannt (z.B.: var _appName = "Meine App";)
  • Interne Schritte (die nicht vom CI direkt aufgerufen werden sollten), werden auch in pascalCase mit vorangestellten _ benannt (z.B.: Task("_cleanup"))
  • Externe Schritte (die vom CI aufgerufen werden), werden in CamelCase benannt (z.B.: Task("BuildAndroidApp"))

Build Script erstellen

Legen Sie eine neue leere Datei build.cake im selben Ordner an, in dem Sie bereits die Bootstraper installiert haben.

Schreiben Sie Folgendes in die neue Datei:

// Standard-Target
var _target = Argument("target", "Default");

Task("Default")
    .Does(() => {
        Information("Standard Task");
    });

// Ausführen des Targets
RunTarget(_target);

Die 2. Zeile liest aus den übergebenen Parametern das auszuführende Target aus. Ist dieses nicht gesetzt, wird Default als Target festgelegt.

In der letzten Zeile wird das Target ausgeführt.

Der Task dazwischen gibt in unserem Fall nur eine Logausgabe (Level: Info) aus. Sie können noch Warning und Error für Logs nutzen. Die Methode Does akzeptiert ein Lambda-Ausdruck, der den Code enthält, um den Schritt auszuführen.

Führen Sie das Script über die Tastenkombination unter Windows Strg + Umschalt + T oder Command + Umschalt + T unter macOS aus. In der oben erscheinenden Liste sehen Sie die aktuell verfügbaren Tasks. Die Ausgabe sollte in etwa wie unten aussehen.

Auflistung der verfügbaren Schritte
Auflistung der verfügbaren Schritte
PS C:\Users\Tutorials\XF.App> powershell -ExecutionPolicy ByPass -File build.ps1 -target "Default"
Preparing to run build script...
Running build script...

========================================
Default
========================================
Standard Task

Task                          Duration
--------------------------------------------------
Default                       00:00:00.0086368
--------------------------------------------------
Total:                        00:00:00.0086368

Aufräumen vor dem Build

Als erstes wollen wir alle nicht notwendige Artifakte entfernen, die das Build stören könnten. Dazu gehören vor allem die bin und obj Ordner der Projekte.

Um alle notwenigen Ordner zu erwischen (egal wie die Projektordner organisiert sind), nutzen wir das Globbing-Feature von CAKE.

// Standard-Target
var _target = Argument("target", "Default");

// Private Tasks
Task("_cleanup")
    .Description("Löschen der ordner mit alten Build-Artefakten")
    .Does(() => {
        CleanDirectories("./**/obj");
        CleanDirectories("./**/bin");
    });

Task("Default")
    .Description("Standard Task")
    .IsDependentOn("_cleanup");

// Ausführen des Targets
RunTarget(_target);

Mit CleanDirectories können wir mehrere Ordner direkt löschen. Diese Methode akzeptiert entweder eine Liste an Ordnern oder ein Globbing-String, wie in unserem Fall. Über das Globbing ** geben wir an, dass hier jeder Unterordner gemeint ist, in dem ein Unterordner obj (oder bin) liegt. Diese sollen dann geleert werden (nicht gelöscht).

Den Task Default haben wir umgeschrieben. Dieser hängt nun vom Task _cleanup ab. Wenn wird Default nun ausführen, erhalten Sie folgende Ausgaben (und alle bin und obj Ordner sind nun leer).

PS C:\Users\Tutorials\XF.App> powershell -ExecutionPolicy ByPass -File build.ps1 -target "Default"
Preparing to run build script...
Running build script...

========================================
_cleanup
========================================

========================================
Default
========================================

Task                          Duration
--------------------------------------------------
_cleanup                      00:00:00.0322681
--------------------------------------------------
Total:                        00:00:00.0337501

Wiederherstellen der NuGet Abhängigkeiten

NuGet Packages können Sie auf zwei unterschiedliche Arten wiederherstellen:

  1. Mit dem NuGet.exe-Tool
  2. Als MSBuild Task

Nutzen wir die erste Möglichkeit. Um NuGet Packages für alle Projekte wiederherzustellen, übergeben wir den Namen der Projektmappe. Den Namen hinterlegen wir als Scriptvariable.

// Standard-Target
var _target = Argument("target", "Default");

// Script Variablen
var _solutionName = "XamarinWithEntityFramework.sln";

// Private Tasks
Task("_cleanup")
    .Description("Löschen der ordner mit alten Build-Artefakten")
    .Does(() => {
        CleanDirectories("./**/obj");
        CleanDirectories("./**/bin");
    });

Task("_restoreNuGetPackages")
    .Description("Wiederherstellen aller NuGet Packages in Projektmappe")
    .Does(() => {
        NuGetRestore(_solutionName);
    });

Task("Default")
    .Description("Standard Task")
    .IsDependentOn("_cleanup")
    .IsDependentOn("_restoreNuGetPackages");

// Ausführen des Targets
RunTarget(_target);

In der Zeile 5 hinterlegen wir den Dateinamen für die Projektmappe. Mit dem Methode NuGetRestore, die als Parameter eine Datei (Projekt- oder Projektmappe) erwartet, werden alle NuGet Abhängigkeiten aufgelöst, und wenn notwendig heruntergeladen. Es kommt dazu die NuGte.exe-Datei, die bereits durch den Bootstraper heruntergeladen wurde.

CAKE nutzt immer die aktuelle stabile Version von NuGet. Das ist bei VisualStudio 2017 nicht immer der Fall (VS nutzt manchmal bereits Beta / RC Versionen). Aus diesem Grund kann es ab und zu zu unerklärlichen Verhalten beim Wiederherstellen kommen (in VS funktioniert es, bei CAKE nicht). Das kam in der letzten Zeit oft in Verbindung mit .Net Stadard und UWP / Xamarin vor.

Der Default Schritt wird um den Wiederherstellungsschritt erweitert.

PS C:\src\XamarinWithEntityFramework> powershell -ExecutionPolicy ByPass -File build.ps1 -target "Default"
Preparing to run build script...
Running build script...

========================================
_cleanup
========================================

========================================
_restoreNuGetPackages
========================================
MSBuild auto-detection: using msbuild version '15.4.8.50001' from 'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin'.
Alle in "packages.config" aufgeführten Pakete sind bereits installiert.
Restoring packages for C:\src\XamarinWithEntityFramework\src\XamarinWithEntityFramework.Repos\XamarinWithEntityFramework.Repos.csproj...
...

NuGet Config files used:
    C:\Users\Eugen\AppData\Roaming\NuGet\NuGet.Config
    C:\Program Files (x86)\NuGet\Config\Microsoft.VisualStudio.Offline.config

Feeds used:
    C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\
    https://api.nuget.org/v3/index.json

========================================
Default
========================================

Task                          Duration
--------------------------------------------------
_cleanup                      00:00:01.6675430
_restoreNuGetPackages         00:00:03.7868412
--------------------------------------------------
Total:                        00:00:05.4553638

Versionierung der Apps / Assemblies mit GitVersion

In den meisten Fällen sollte eine App (aber auch Bibliothek) versioniert werden. Das kann man manuell machen, oder einer der verfügbaren Tools nutzen. Ich nutze für die Entwicklung sehr gern GitFlow. Hier ist klar, welche der Versionen aus einem bestimmten Branch zum Testen, und welche für den Store vorgesehen ist. Für die Versionierung nutze ich die Erweiterung GitVersion (die es auch als reines Command Line Tool gibt). Diese bestimmt die Version des aktuellen Builds anhand der Git-Historie.

Dazu müssen wir eine Erweiterung für CAKE installieren. Diese werden bei CAKE in zwei Ausprägungen zur Verfügung gestellt:

  1. Tool: Sind CLI-Tools, die über NuGet installiert werden und durch einen Wrapper aufgerufen werden können. Somit können diese Tools auch ohne CAKE genutzt werden.
  2. AddIns: Spezielle Erweiterungen für CAKE, die nur mit CAKE funktionieren. Hier fangen die IDs der NuGet Packages oft mit Cake. an.

Bei beiden können neben NuGet auch weitere Quellen zum Herunterladen dienen. Bei NuGet besteht die Möglichkeit, die Erweiterung an eine bestimmte Version zu pinnen (standardmäßig wird immer die neueste Version genommen). Das ist bei Projekten, die oft eine ältere Version neu erstellen müssen, von Vorteil, da dann der exakte Zustand des Build-Systems wiederhergestellt werden kann.

// Tools
#tool "nuget:?package=GitVersion.CommandLine&version=3.6.5"

// Standard-Target
var target = Argument("target", "Default");

// Script Variablen
var _solutionName = "XamarinWithEntityFramework.sln";

// Private Tasks
Task("_cleanup")
    .Description("Löschen der ordner mit alten Build-Artefakten")
    .Does(() => {
        CleanDirectories("./**/obj");
        CleanDirectories("./**/bin");
    });

Task("_restoreNuGetPackages")
    .Description("Wiederherstellen aller NuGet Packages in Projektmappe")
    .Does(() => {
        NuGetRestore(_solutionName);
    });

Task("_fixVersion")
    .Description("Korrektur der Version in Assembly durch GitVersion")
    .Does(() => {
        var versionSettings = new GitVersionSettings {
            UpdateAssemblyInfo = true,
            WorkingDirectory = "./src"
        };
        GitVersion(versionSettings);
    });

Task("Default")
    .Description("Standard Task")
    .IsDependentOn("_cleanup")
    .IsDependentOn("_restoreNuGetPackages")
    .IsDependentOn("_fixVersion");

// Ausführen des Targets
RunTarget(target);

In der Zeile 2 definieren wird, dass das Tool GitVersion über NuGet mit der Version 3.6.5 installiert werden soll. Nun steht das Tool für uns im Script zur Verfügung.

In der Konfiguration (Zeilen 27-30) geben wir an, dass die Versionen in den Dateien AssemblyInfo im Unterordner src angepasst werden sollen. Dabei werden folgende Informationen zu AssemblyInfo angepasst / hinzugefügt:

  • AssemblyVersion: Standardmäßig auf drei Stellen, also 1.2.3.0. Die letzte Nummer ist immer 0. Das kann konfiguriert werden.
  • AssemblyFileVersion: Bekommt dieselbe Versionsnummer, wie AssemblyVersion.
  • AssemblyInformationalVersion: Ein String, der neben der SemVer-Versionsnummer (Semantische Version) auch Brach und Git-Hash enthält (z.B.:0.1.0-main.13+Branch.main.Sha.ca5ee73a51fbd24b5aaeb0ff4a0f8c54945951a1)

In der Zeile 31 wird dann nur noch die Versionierung nach Konfigurationsvorgaben durchgeführt.

Wenn Sie das GitVesion direkt aufruft, erhaltet Ihr eine Auflistung, von allen möglichen Meta-Informationen, die aus Git ausgelesen werden und wir für einige Schritte später nutzen werden.

PS C:\src\XamarinWithEntityFramework> .\tools\gitversion.commandline.3.6.5\GitVersion.CommandLine\tools\GitVersion.exe
{
  "Major":0,
  "Minor":1,
  "Patch":0,
  "PreReleaseTag":"unstable.16",
  "PreReleaseTagWithDash":"-unstable.16",
  "PreReleaseLabel":"unstable",
  "PreReleaseNumber":16,
  "BuildMetaData":"",
  "BuildMetaDataPadded":"",
  "FullBuildMetaData":"Branch.develop.Sha.bcb4452dcd008eed5b70245247b03c72552baf88",
  "MajorMinorPatch":"0.1.0",
  "SemVer":"0.1.0-unstable.16",
  "LegacySemVer":"0.1.0-unstable16",
  "LegacySemVerPadded":"0.1.0-unstable0016",
  "AssemblySemVer":"0.1.0.0",
  "FullSemVer":"0.1.0-unstable.16",
  "InformationalVersion":"0.1.0-unstable.16+Branch.develop.Sha.bcb4452dcd008eed5b70245247b03c72552baf88",
  "BranchName":"develop",
  "Sha":"bcb4452dcd008eed5b70245247b03c72552baf88",
  "NuGetVersionV2":"0.1.0-unstable0016",
  "NuGetVersion":"0.1.0-unstable0016",
  "CommitsSinceVersionSource":16,
  "CommitsSinceVersionSourcePadded":"0016",
  "CommitDate":"2017-11-10"
}

Fazit

In diesem ersten Teil haben wir ein kleines Script erstellt, der unsere Build-Umgebung bereinigt, Abhängigkeiten installiert und die Versionsnummer automatisch anhand der Informationen auf dem Git-Versionsystem bestimmt.

Im zweiten Teil werden wir das Script erweitern, um Apps für Android und iOS erstellen zu können (sowohl zum Testen als auch für die App Stores).