Xamarin.iOS App mit Azure DevOps bauen

Ich verwende Azure DevOps zum Organisieren meiner Projekte. Ich habe also den gesamten benötigten Code im Repository und nutze die Boards, um die notwendigen Aufgaben im Überblick zu haben. In diesem Beitrag möchte ich euch jetzt zeigen, wie ihr mit wenig Aufwand eine Pipeline in Azure DevOps erstellen könnt, welche es euch ermöglicht eure iOS-Version zu bauen. Einen Blog-Beitrag zum Erstellen einer Pipeline für die Android-Version habe ich bereits vor einiger Zeit auf meinem Blog veröffentlicht.

Der Beitrag ist auch schon in englischer Sprache auf Medium veröffentlicht worden.

Vorbereitungen

Um später den iOS-Build signieren zu können, benötigt man ein kostenpflichtiges Entwicklerkonto von Apple. Man kann dieses auf der Webseite developer.apple.com für 99 Euro pro Jahr beantragen. Außerdem wird ein Mac benötigt, um ein Entwickler-Zertifikat zu erstellen. Es werden nämlich ein P12-Zertifikat und ein Provisioning Profile benötigt.

In diesem Beitrag gehe ich jetzt nicht im Detail darauf ein, wie man diese beiden notwendigen Dateien erzeugen kann. Sollte jedoch Interesse bestehen, so kann ich gerne einen separaten Blog-Beitrag zu diesem Thema schreiben.

Im weiteren Verlauf dieses Posts gehe ich davon aus, dass das P12-Zertifikat mit dem entsprechenden Passwort und das Provisioning Profil vorliegen.

Hochladen der Dateien zu Azure DevOps

Die zwei angesprochenen Dateien benötigen wir in unserer Pipeline. Zunächst öffnen wir Azure DevOps, navigieren zu unserem Projekt und wählen dann den Eintrag Library, welcher ein Teil des Pipeline-Tabs ist. Anschließend wechseln wir auf den Reiter Secure files. Der folgende Screenshot zeigt bereits die Einstellungen, welche für die Android-Pipeline vorgenommen worden sind.

Nun klicken wir auf + Secure file und wählen zunächst das Provisiong Profil aus und akzeptieren den Upload mit einem Klick auf OK.

Nun wiederholen wir den Vorgang mit dem P12-Zertifikat.

Wir wechseln nun auf den Variable group Tab. Im Blog-Post über die Android-Pipeline haben wir bereits einen Gruppe mit dem Namen variables angelegt. Solltest du diese Gruppe noch nicht habe, so kannst du diese ganz einfach erstellen und nennst sie dann variables. Wir müssen nun drei neue Variablen erzeugen: AppleCertificate-FileName, AppleCertificate-Password und ProvisioningProfile-FileName. Die Variable AppleCertificate-Password sollte als sichere Variable angelegt werden, denn niemand sollte das Password auslesen können. Die FileName-Variablen sollten den passenden Namen der hochgeladenen Dateien beinhalten.

Anlegen der Pipeline

Es ist zwar möglich innerhalb von einer Pipeline sowohl die Android- als auch die iOS-Version zu bauen, aber in diesem Beitrag werde ich eine neue Pipeline für die iOS-App anlegen.

Wir wählen zunächst den Pipelines-Tab im linken Menü. Um eine neue Pipeline anlegen zu können, klicken wir auf den Button New pipeline. Im ersten Schritt müssen wir den Speicherort für unseren Code-Angeben. Für mein Beispiel liegt der Code in Azure DevOps und daher kann ich Azure Repos Git auswählen. Ihr habt aber die Möglichkeit auch andere Speicherorte, wie GitHub oder BitBucket, anzugeben.

Ich wähle nun also Azure Repos Git aus. Nun sollten alle möglichen Repositories aufgelistet werden. In meinem Beispiel ist dies nur ein Repository (Xamarin Playground), welches ich auswählen werden.

Im nächsten Schritt gibt uns Azure DevOps nun ein paar vorgefertigte Templates. Ich möchte die Pipeline aber selbst von Hand anlegen und daher wähle ich Starter pipeline, um eine YAML-Datei als Grundgerüst zu erhalten.

Es öffnet sich nun ein Editor mit der aktuellen Version der Pipeline.

Nun müssen wir die Datei so anpassen, dass am Ende unsere IPA-Datei erzeigt wird. IPA ist eine Abkürzung und steht für iOS App Store Package und ist technisch ein Zip-Archiv, welches die iOS-App beinhaltet und zur Installation genutzt werden kann.

Der folgende Code-Ausschnitt zeigt nun die vollständige Pipeline, welche ich im Anschluss Schritt für Schritt erklären möchte.

# Xamarin.iOS

trigger:
- main

variables:
- group: 'variables'

jobs:
  - job: 'BuildiOS'
    pool:
      vmImage: 'macOS-latest'
    
    variables:
      buildConfiguration: 'Release'
    
    steps:
      - task: NuGetToolInstaller@1

      - task: NuGetCommand@2
        inputs:
          restoreSolution: '**/*.sln'
      
      - task: DownloadSecureFile@1
        inputs:
          secureFile: '$(ProvisioningProfile-FileName)'
      
      - task: DownloadSecureFile@1
        inputs:
          secureFile: '$(AppleCertificate-FileName)'
      
      - task: InstallAppleCertificate@2
        inputs:
          certSecureFile: '$(AppleCertificate-FileName)'
          certPwd: '$(AppleCertificate-Password)'
      
      - task: InstallAppleProvisioningProfile@1
        inputs:
          provisioningProfileLocation: secureFiles
          provProfileSecureFile: '$(ProvisioningProfile-FileName)'
          removeProfile: true

      - task: XamariniOS@2
        inputs:
          solutionFile: '**/*.sln'
          configuration: '$(buildConfiguration)'
          buildForSimulator: false
          packageApp: true
          signingIdentity: $(APPLE_CERTIFICATE_SIGNING_IDENTITY)
          signingProvisioningProfileID: $(APPLE_PROV_PROFILE_UUID)
      
      - task: CopyFiles@2
        inputs:
          Contents: '**/*.ipa'
          TargetFolder: '$(Build.ArtifactStagingDirectory)'
          OverWrite: true
          flattenFolders: true
      
      - task: PublishBuildArtifacts@1
        inputs:
          PathtoPublish: '$(Build.ArtifactStagingDirectory)'
          ArtifactName: 'MyAwesomeApp-$(Build.BuildNumber)'
          publishLocation: Container

Zunächst werden die Metadaten definiert. In meinem Fall möchte ich nur auf Änderungen im main-Branch achten, aber hier können sämtliche Branches definiert werden, wie zum Beispiel dev oder auch staging.

Das Keyword variables verbindet unsere erstellte Variablengruppe mit der Pipeline. Hier muss sichergestellt werden, dass der Name entsprechend übereinstimmt.

In dieser Pipeline verwende ich Jobs, da wir später auch noch die Schritte zum Erstellen der Android-Version in der Pipeline integrieren wollen. Aber für den Moment haben wir nur einen Job, welcher die iOS-Version erstellt.

Da wir eine iOS-App erstellen möchten, müssen wir zum Erstellen des Pakets einen Mac verwenden. Glücklicherweise stellt Microsoft einen gehosteten Agenten bereit, der auf MacOS läuft. Aus diesem Grund verwenden wir macOS-latest.

Der nächste Abschnitt enthält einige Variablen. In unserem Fall ist es nur die aktuelle Build-Konfiguration.

Jetzt passiert die Magie, denn wir definieren die Schritte für unsere Pipeline. Zunächst müssen wir sicherstellen, dass NuGet installiert ist. Anschließend führen wir ein NuGet Restore für unsere Solution durch, um alle benötigten NuGet-Pakete zu installieren.

Im nächsten Schritt müssen wir die Dateien, welche wir im Rahmen der Vorbereitung hochgeladen haben, wieder herunterladen. Aufgrund der Tatsache, dass wir zwei Dateien haben, müssen wir auch den Task DownloadSecureFile@1 zwei Mal ausführen. Wir verwenden die Werte aus unserer Variablengruppe, um die entsprechenden Dateinamen zu erhalten.

Der nächste Schritt ist die Installation der Dateien. Zuerst installieren wir die P12-Zertifikatsdatei mithilfe eines InstallAppleCertificate@2-Tasks. Wir müssen noch einmal den Dateinamen und das entsprechende Passwort aus unseren Variablen bereitstellen. Das Provisioning Profile müssen wir jedoch mithilfe des Tasks InstallAppleProvisioningProfile@1 installieren. Hier müssen wir angeben, wo das Profil gespeichert werden soll und auch den Namen der Datei. Wir setzen auch removeFiles auf true, um sicherzustellen, dass das Profil am Ende wieder entfernt wird.

Jetzt ist es an der Zeit, die App zu erstellen. Dafür verwenden wir den XamariniOS@2-Task. Wenn wir eine signierte IPA-Datei erstellen möchten, ist es wichtig, dass signingIdentity und signingProvisioningProfileID angegeben werden.

Die nächsten beiden Schritte sind das Kopieren und Veröffentlichen der entsprechenden IPA-Datei.

Ausführen der Pipeline

Nun ist es an der Zeit, dass wir unsere Pipeline einmal ausprobieren. Dafür drücken wir einfach Save und wir geben noch an, ob wir die Pipeline in einem separaten Branch anlegen wollen oder direkt auf dem main-Branch. Im Anschluss können wir die Pipeline nun ausführen.

Wie ihr sehen könnt, war der Durchlauf erfolgreich und wir klicken nun in der Sektion Related auf den Link 1 published um anschließend den Zugriff auf die IPA-Datei zu erhalten.

Android- und iOS-Pipeline kombinieren

Nachdem wir nun zwei separate Pipelines für unsere Anwendung eingerichtet haben, ist es an der Zeit, diese Pipelines zu einer Pipeline zu kombinieren, die die APK- und AAB-Dateien für Android und die IPA-Datei für iOS erstellt. Wir müssen nur die Jobs der beiden Pipelines in einer Pipeline kombinieren.

# Xamarin.Android and Xamarin.iOS

trigger:
- main

variables:
- group: 'variables'

jobs:
  - job: 'BuildAndroid'
    pool:
      vmImage: 'windows-2022'
      
    variables:
      buildConfiguration: 'Release'
      outputDirectory: '$(build.binariesDirectory)/$(buildConfiguration)'

    steps:
      - task: NuGetToolInstaller@1

      - task: NuGetCommand@2
        inputs:
          restoreSolution: '**/*.sln'
      
      - task: XamarinAndroid@1
        inputs:
          projectFile: '**/*droid*.csproj'
          outputDirectory: '$(build.binariesDirectory)'
          configuration: '$(buildConfiguration)'
          msbuildVersionOption: latest
      
      - task: DownloadSecureFile@1
        name: keystore
        inputs:
          secureFile: '$(KeyStore-FileName)'
          
      - task: AndroidSigning@3
        inputs:
          apkFiles: '**/*.apk'
          apksign: true
          zipalign: true
          apksignerKeystoreFile: '$(KeyStore-FileName)'
          apksignerKeyPassword: '$(KeyStore-Password)'
          apksignerKeystoreAlias: '$(KeyStore-Alias)'
          apksignerKeystorePassword: '$(KeyStore-Password)'
      
      - task: CopyFiles@2
        inputs:
          Contents: '**/*.apk'
          TargetFolder: '$(Build.ArtifactStagingDirectory)'
          OverWrite: true
          flattenFolders: true
          
      - task: PublishBuildArtifacts@1
        inputs:
          PathtoPublish: '$(Build.ArtifactStagingDirectory)'
          ArtifactName: 'MyAwesomeApp-$(Build.BuildNumber)'
          publishLocation: Container

      - task: XamarinAndroid@1
        inputs:
          projectFile: '**/*droid*.csproj'
          outputDirectory: '$(Build.BinariesDirectory)'
          configuration: '$(BuildConfiguration)'
          clean: true
          msbuildVersionOption: latest
          msbuildArguments: '/p:JavaSdkDirectory="$(JAVA_HOME_11_X64)/" 
                             /p:AndroidPackageFormat=aab 
                             /t:SignAndroidPackage 
                             /p:AndroidNdkDirectory="$(androidNdkPath)" 
                             /p:AndroidKeyStore="True" 
                             /p:AndroidSigningKeyStore="$(keystore.secureFilePath)" 
                             /p:AndroidSigningKeyPass="$(KeyStore-Password)" 
                             /p:AndroidSigningKeyAlias="$(KeyStore-Alias)" 
                             /p:AndroidSigningStorePass="$(KeyStore-Password)"'

      - task: CopyFiles@2
        inputs:
          Contents: '**/*.aab'
          TargetFolder: '$(Build.ArtifactStagingDirectory)'
          OverWrite: true
          flattenFolders: true

      - task: PublishBuildArtifacts@1
        inputs:
          PathtoPublish: '$(Build.ArtifactStagingDirectory)'
          ArtifactName: 'MyAwesomeApp-$(Build.BuildNumber)'
          publishLocation: Container

  - job: 'BuildiOS'
    pool:
      vmImage: 'macOS-latest'
    
    variables:
      buildConfiguration: 'Release'
    
    steps:
      - task: NuGetToolInstaller@1

      - task: NuGetCommand@2
        inputs:
          restoreSolution: '**/*.sln'
      
      - task: DownloadSecureFile@1
        inputs:
          secureFile: '$(ProvisioningProfile-FileName)'
      
      - task: DownloadSecureFile@1
        inputs:
          secureFile: '$(AppleCertificate-FileName)'
      
      - task: InstallAppleCertificate@2
        inputs:
          certSecureFile: '$(AppleCertificate-FileName)'
          certPwd: '$(AppleCertificate-Password)'
      
      - task: InstallAppleProvisioningProfile@1
        inputs:
          provisioningProfileLocation: secureFiles
          provProfileSecureFile: '$(ProvisioningProfile-FileName)'
          removeProfile: true

      - task: XamariniOS@2
        inputs:
          solutionFile: '**/*.sln'
          configuration: '$(buildConfiguration)'
          buildForSimulator: false
          packageApp: true
          signingIdentity: $(APPLE_CERTIFICATE_SIGNING_IDENTITY)
          signingProvisioningProfileID: $(APPLE_PROV_PROFILE_UUID)
      
      - task: CopyFiles@2
        inputs:
          Contents: '**/*.ipa'
          TargetFolder: '$(Build.ArtifactStagingDirectory)'
          OverWrite: true
          flattenFolders: true
      
      - task: PublishBuildArtifacts@1
        inputs:
          PathtoPublish: '$(Build.ArtifactStagingDirectory)'
          ArtifactName: 'MyAwesomeApp-$(Build.BuildNumber)'
          publishLocation: Container

Wenn wir diese Pipeline jetzt ausführen, sehen Sie, dass Android und iOS während der Ausführung der Pipeline erstellt werden.

Nachdem die Pipeline nun erfolgreich durchgelaufen ist, klicken wir wieder auf den Link 1 published in der Sektion Realted und erhalten so Zugriff auf die APK-, die AAB- und die IPA-Datei.

Zusammenfassung

In diesem Beitrag habe ich gezeigt, wie ihr eine Pipeline zum Erstellen und Veröffentlichen einer Xamarin.iOS-Anwendung mit Azure DevOps einrichten könnt. Diese Pipeline erstellt die benötigte IPA-Datei. Ich habe auch eine kombinierte Pipeline erstellt, die die erforderlichen Dateien für Android und iOS erstellt.

Notfall-Rufnummern: App für Android und iOS Foto-Galerie App mit Xamarin.Forms SCRCPY: Bildschirm von Android-Device auf Desktop spiegeln