Provisioning on-demand macOS virtual machines with Jenkins and Anka Build for iOS CI

Guest Blog Post By Peter Wiesner, Senior Software Engineer @Skyscanner

Every year Apple releases a new version of Xcode. CI systems for iOS application development need to adopt it, so developers can take advantage of the new iOS features. CI systems using Anka Build have a head-start here. For folks not familiar with Anka Build, read more details here.

You only need to create a new tag for the current Anka macOS virtual machine with the new Xcode installed. Anka ships with features helping to automate this process.

In this blog post, I will show you how to do this. We will use the following solutions:

  • anka create to create the macOS virtual machine from a script
  • Jenkins pipeline to run the jobs on demand
  • anka run to execute batch commands on the virtual machines

About the prerequisites:

  • Jenkins with pipeline plugin installed
  • a mac with anka-create label connected to Jenkins. We will use this native node to create and provision the virtual machine
  • Anka Build package version 1.4 current release installed on the mac

Jenkins pipeline

Let’s create a Jenkins pipeline project with following script. The key tricks are:

  • use Jenkins parameters to provide configuration (Xcode version to install)
  • use a separate repo for the actual provisioning files
  • always save the state of the VM on pipeline failure so next iteration is faster
pipeline {

    agent none

    options {

        timeout(time: 240, unit: 'MINUTES')


    environment {

        // Provide credentials here (anka username/pwd, keychain passwords,
        for tool packages etc.)


parameters {

    //Provide mutable configuration for scripts (anka VM name, anka
    Xcode version)

string(name: 'ANKA_VM_NAME', defaultValue: 'anka_VM_node',
    description: 'Name of the Anka VM')


stages {

    stage("anka-create") {

        agent {

            node {

                // There is a native mac connected to Jenkins with this
                label 'anka-create'


        steps {



        post {

            always {

                // Create a vm that stores the state of the VM on exiting. When fixing
                this can speed up the process a lot

                sh ""

                if [\$(anka list | grep current_state_vm | wc - l) == 1]


                anka delete--yes current_state_vm


                anka stop\ $ANKA_VM_NAME

                anka clone - c\ $ANKA_VM_NAME current_state_vm

                if [\$(anka list | grep\ $ANKA_VM_NAME | wc - l) == 1]


                echo "Deleting Anka VM: \$ANKA_VM_NAME"

                anka delete--yes\ $ANKA_VM_NAME






def checkout_provisioning_scripts() {

    // It is useful to decouple the actual provisioning script from this
    pipeline script

    // Easy to do updates and helps reading the pipeline script

    // Clone the provisioning script from a GitHub repo

    timeout(15) {

        checkout([$class: 'GitSCM', branches: [
                [name: 'master']
            ], ,

            extensions: [
                [$class: 'CloneOption', depth: 1, noTags: false,
                    shallow: true, timeout: 30
            userRemoteConfigs: [
                [name: 'origin', refspec:
                    '+refs/heads/*:refs/remotes/origin/*', url: 'SSH_GIT_URL'


def create_task() {


    credentials.withAllCredentials {


        // Once we cloned the repository, print the environment variables
        to see
        if job parameters and variables are correctly passed to this task

        // comes from the repo

        sh ""


        sh. /



Creating the VM

The Jenkins pipeline calls out to a provision script. This is responsible to utilize anka for provisioning. The key tricks are:

  • collecting all necessary packages beforehand
  • allowing the script to use previously saved virtual machine as starting point (`anka create` can take up to 20 mins)

# Let's fail on error

set -e

# Retrieve macOS installer and necessary packages

# It's beneficial to store them on a NAS that can be connected to the mac

# We can start the process with previous state of vm if we would like to

# This saves a lot of time

if [ "$START_FROM_BEGINNING" -eq 0 ]


#Create VM from the Installer on NAS

ANKA_DEFAULT_USER=$USER_NAME anka --debug create --ram-size $VM_RAM
--cpu-count $VM_CPU --disk-size $VM_DISK --app "$PATH_TO_OS_INSTALLER"

anka modify $ANKA_VM_NAME add port-forwarding --host-port 0 --guest-port
22 --protocol tcp ssh


#Clone VM

anka clone current_state_vm $ANKA_VM_NAME


# Iterate on the setup/install scripts

for file in $files_to_execute


# Run script as root

anka run --env $ANKA_VM_NAME sudo -E bash $file

# Run script as created anka user

#anka run --env $ANKA_VM_NAME sudo -E -H -u $USER_NAME bash $file


# Stop the VM after we are done, start it up and suspend it, so CI can use
the super fast start-up feature of Anka VMs.

anka stop -f $ANKA_VM_NAME

anka run $ANKA_VM_NAME ls

anka suspend $ANKA_VM_NAME

# Save the new baseline virtual machine

anka registry -a $REGISTRY_IP_ADDRESS push $ANKA_VM_NAME -t $ANKA_TAG

Installing necessary tools

The actual provisioning happens in the `for loop`, where we use `anka run`. These scripts install the tools like Xcode, Xcode CLI, Ruby, git or any packages. The content and order of the scripts are company/case specific, but let me list some general tricks. Keep in mind, when running the `anka run` command, the current working directory will be mounted to the VM and the files can be accessed with a relative path (sweet!).

Installing certificates

$P12_PATH contains the path to the exported p12 files location.

echo $USER_PWD | sudo -S security import $P12_PATH -k
"/Library/Keychains/System.keychain" -P "$P12_PASSWORD" -A

Installing Xcode CLI

$XCODE_CLI_PATH contains the path to the Xcode Command Line Tools package location.

MOUNTDIR=$(echo `hdiutil mount $XCODE_CLI_PATH | tail -1 | awk '{$1=$2="";
print $0}'` | xargs -0 echo)

echo $USER_PWD | sudo -S installer -pkg "${MOUNTDIR}/"*.pkg -target /
hdiutil unmount "${MOUNTDIR}"

Installing Xcode

You need to install the xcode-install gem for this. $XCODE_APP_VERSION contains the version number of the Xcode you want to install (like 10.0) $XCODE_REMOTE_URL is an URL from where the Xcode xip can be downloaded (it is worth to download it and upload to a private remote server to have bigger download speed).

xcversion install $XCODE_APP_VERSION --url="$XCODE_REMOTE_URL" --verbose

Share this post