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, credentials for tool packages etc.) } parameters { //Provide mutable configuration for scripts (anka VM name, anka tag, 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 label 'anka-create' } } steps { create_task() } post { always { // Create a vm that stores the state of the VM on exiting. When fixing failures, this can speed up the process a lot sh "" "#!/bin/bash if [\$(anka list | grep current_state_vm | wc - l) == 1] then anka delete--yes current_state_vm fi anka stop\ $ANKA_VM_NAME anka clone - c\ $ANKA_VM_NAME current_state_vm if [\$(anka list | grep\ $ANKA_VM_NAME | wc - l) == 1] then echo "Deleting Anka VM: \$ANKA_VM_NAME" anka delete--yes\ $ANKA_VM_NAME fi "" " cleanWs() } } } } } 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() { cleanWs() credentials.withAllCredentials { checkout_provisioning_scripts() // Once we cloned the repository, print the environment variables to see if job parameters and variables are correctly passed to this task // provision.sh comes from the repo sh "" "#!/bin/bash printenv sh. / provision.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)
#!/bin/bash # 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 node # 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 ] then #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_VM_NAME anka modify $ANKA_VM_NAME add port-forwarding --host-port 0 --guest-port 22 --protocol tcp ssh else #Clone VM anka clone current_state_vm $ANKA_VM_NAME fi # Iterate on the setup/install scripts for file in $files_to_execute do # 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 done # 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