Build Xamarin iOS App in the Cloud

While developing an app using Xamarin, you want it to work on your iPhone but you don’t own a Mac. At work we have a old outdated iMac which is slow and remote-desktop is horrible. So how do you build an iOS Xamarin app in the cloud? Using Azure DevOps.

So how do we tackle this?

Note: In this tutorial I’m making an iOS Disitribution App as an Apple Enterprise Developer. However the steps are similar if you intend on publishing your app to the App-Store, except you have to manually upload it.

Requirements

For this to work, you need the following already setup;

  • An Apple developer enterprise account with a iOS Distribution profile and have registered your app.

1. Configure your Library

We don’t want our secrets inside our git so we will store them in Azure DevOps, as we do need them during the build proces. So first go to Library in Azure Devops under Pipelines. Then select the tab Secure Files, then upload the files below. (I used that old Mac from work to export my certificate to a p12 format, with a password. But you can also do that using command-line tools; see stackoverflow.)

  • Your iOS Distribution profile: a p12 certificate (from the Apple developer portal).
  • Your app’s mobile-provision profile (from the Apple developer).

Then go back to the tab Variable Groups and create a Variable Group with your secrets; (Note: you can rename your file names in DevOps.)

  • DistributionCertificateFileName: File name of your p12 certificate.
  • DistributionCertificatePassword: Password for the p12 certificate.
  • ProvisioningProfileFileName: File name of your mobile-provision profile of your app.

2. Change your Pipeline script

Next we will setup our pipeline. Most important; we want to use a Mac to do this so set the vmImage to macos-latest. Next we define a parameter to either build Release or Debug, by default we build a Release. Of course you could also add triggers, for that see the manual. I do this to be able to manually create a Debug build (which has some different settings from my Release build).

pool:
  vmImage: macos-latest

parameters:
- name: buildConfiguration
  displayName: Build Configuration
  type: string
  default: Release
  values:
  - Release
  - Debug

As variables we want to add our created VariableGroup from step 1. And we also add a ProjectName variable. This is the name of your application, we use it to find the correct paths to search. We can do this because most Xamarin projects have the same project structure. In the example we below we thus use ExampleProject.

  • ExampleProject
    • ExampleProject
      • ExampleProject.csproj
    • ExampleProject.iOS
      • ExampleProject.iOS.csproj
  • Solution.sln

Replace the <<VariableGroupName>> with the name you chose in step 1.

variables:
- group: <<VariableGroupName>>
- name: ProjectName
  value: <<ProjectName>>

Install the iOS Distribution certificate. This uses the variables from the variable group and downloads the certificate from the secure files.

- task: InstallAppleCertificate@2
  displayName: Install iOS Distribution Certificate
  inputs:
   certSecureFile: '$(DistributionCertificateFileName)'
   certPwd: '$(DistributionCertificatePassword)'
   keychain: 'temp'
   deleteCert: true

Install the app’s provisioning profile, this works similar.

- task: InstallAppleProvisioningProfile@1
  displayName: Install $(ProjectName) Provisioning Profile
  inputs:
   provisioningProfileLocation: 'secureFiles'
   provProfileSecureFile: '$(ProvisioningProfileFileName)'
   removeProfile: true

Build your iOS app using the Xamarin tasks. This uses our build-configuration parameter. And uses the generated Environment Variables (in blue) by the previous tasks.

- task: XamariniOS@2
  displayName: Build iOS
  inputs:
    solutionFile: '**/*.sln'
    configuration: '${{ parameters.buildConfiguration }}''
    clean: true
    buildForSimulator: false
    packageApp: true
    signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)'
    signingProvisioningProfileID: '$(APPLE_PROV_PROFILE_UUID)'

Now we have build our iOS app and generated an IPA file.

3. Upload your App

We could upload the app as a Build Artifact, but then we would have to download it and manually distribute it. There is however an online-service for enterprise app distribution called Diawi. To use their REST-api to upload an account is required.

Add extra variables to your variable group (from step 1);

  • DiawiAccessToken: generate your own access-token.
  • DiawiCallbackEmail: the receiver of an email with the Diawi-link.

First we copy the artifacts to the staging directory; (This is not required, but makes somes paths easier in the following step and can enable you to still store your artifacts in DevOps.)

- task: CopyFiles@2
  displayName: Copy IPA
  inputs:
    SourceFolder: '$(ProjectName)/$(ProjectName).iOS'
    Contents: |
      bin/iPhone/${{ parameters.buildConfiguration }}/*.ipa
      bin/iPhone/${{ parameters.buildConfiguration }}/*.plist
    TargetFolder: '$(build.ArtifactStagingDirectory)/${{ parameters.buildConfiguration }}'
    flattenFolders: true

Then we run a script (bash on macos) using curl to upload our IPA file, using the REST-api of Diawi using our variables from the variable group from step 1.

- script: >-
    curl https://upload.diawi.com/
     -F token='$(DiawiAccessToken)'
     -F file=@$(build.ArtifactStagingDirectory)/${{ parameters.buildConfiguration }}/$(ProjectName).iOS.ipa
     -F wall_of_apps=1
     -F callback_emails='$(DiawiCallbackEmail)'
  displayName: Upload to Diawi

You can add more parameters, see the API description (you need an account to access it).

Conclusion

As you can see the proces is quite easy, with existing tasks from the Azure DevOps library.

Jenkins ‘Build back to normal’

Yesterday, I finished porting our Jenkinsfile to use the new Declarative syntax. It makes the flow of processing a lot more straightforward and it’s great for handling errors and post actions. However getting everything to work again was tricky!

I was looking to send an e-mail and Office365 notification when a build returns to normal. Others updated the status of the current build during their steps, as seen on stackoverflow and here. I managed to do it slightly different without having to manage the current state;

pipeline {
  agent any
  post {
    success {
      script {
        if (currentBuild.getPreviousBuild().getResult().toString() != "SUCCESS") {
          echo 'Build is back to normal!'
        }
      }
    }
  }
}

For more details on the syntax of declarative pipelines, I’d recommend this site.