Using Custom Build Scripts and Environment Variables in Xcode Cloud

This article introduces Custom Build Scripts and Environment Variables for automated workflows in Xcode Cloud.

Using Custom Build Scripts and Environment Variables in Xcode Cloud

During WWDC 2021 Apple announced Xcode Cloud, a new cloud-based toolchain to automatically build, test, and distribute apps from within Xcode. It is available in beta currently, and you can join the waiting list here. To get started, the toolchain can be set up in a few minutes within any development project. For a quick guide on how to get started, consider our article "Setting up Xcode Cloud for Automated Builds, Tests and Distribution".

"Xcode Cloud lets you adopt continuous integration and delivery (CI/CD), a standard software development practice that helps you develop and maintain your code and deliver apps to testers and users. Xcode Cloud is a CI/CD system that combines the tools you use to create apps and frameworks for Apple platforms: Xcode, TestFlight, and App Store Connect." Apple Developer Documentation

This article will cover more advanced workflows and the use of custom build scripts and environment variables in Xcode Cloud workflows. They allow customizing the build pipeline in any way needed to integrate with complex projects, third-party tools, and more.

Custom Build Scripts

If your projects require additional tools to be installed in the environment in which the build is happening, if you want to copy your artifacts to an external storage or if you want to change assets in the project folder depending on the workflow, custom build scripts come to the rescue.

The Xcode Cloud pipeline supports three types of build scripts which are executed at different moments in the workflow.

  1. Post-clone: It runs after the project source git repository is cloned to the temporary environment and before all other dependencies are resolved. It can be used to install additional tools, edit files before building the project, etc and has to use ci_post_clone.sh as a file name.
  2. Pre-Xcodebuild: It runs before the xcodebuild command is run in the Xcode Cloud environment and can be used to compile additional dependencies for example. It has to use ci_pre_xcodebuild.sh as a filename.
  3. Post-Xcodebuild: It runs after the xcodebuild command is run in the Xcode Cloud environment and is executed also if the build fails. As an example, it can be used to upload artifacts to other services and has to use ci_post_xcodebuild.sh as a file name.

These build scripts have to be stored in a specific folder inside the Xcode project, namely a ci_scripts directory. When Xcode Cloud executes a new build, the custom build scripts are automatically detected and run at their designated time in the timeline.

To create the ci_scripts directory, navigate to the Project Navigator in Xcode and Control-click your project,  choose New Group to create the group and its corresponding directory, and name it ci_scripts.

To create a custom build script, the precise naming convention mentioned above needs to be followed. Inside the project navigator in Xcode, Control-click the ci_scripts group select New File and choose the Shell Script template. Adopt the ci_post_clone.sh for a Post close script, ci_pre_xcodebuild.sh for a Pre-Xcodebuild script, or ci_post_xcodebuild.sh for a Post-Xcodebuild script and ensure that the file is not added to any target.

Then, to make the script executable within the Xcode Cloud workflow timeline, use the chmod +x ci_post_clone.sh,  hmod +x ci_pre_xcodebuild.sh or hmod +x ci_post_xcodebuild.sh command in Terminal.  

Now you can add your custom code to the script file and push it to the Git repository to be run the next time an Xcode Cloud workflow is executed.

Editing Custom Build Scripts

When creating your scripts, an important thing to note is that the script cannot obtain administrator privileges by using sudo. Also, the script is run in a temporary environment where access to the project's source code is not available. All files needed to run the script, therefore, have to be available inside the ci_scripts directory. Subfolders can be used to organize the content, however, the build scripts have to be placed in the top level of the ci_scripts folder.

When creating complex scripts, it is also recommendable to handle potential errors and cases in which a command fails or returns a nonzero exit code. Using a nonzero exit code in your custom build script, for example, will let Xcode know something was wrong and result in a build fail. Using the -e option, you can stop a script if a command exits with a nonzero exit code.

#!/bin/sh

# Set the -e flag to stop running the script in case a command returns
# a nonzero exit code.
set -e

# A command or script succeeded.
echo "A command or script was successful."
exit 0

...

# Something went wrong.
echo "Something went wrong. Include helpful information here."
exit 1

For more information, explore Writing custom Build Scripts in the official Apple Developer Documentation.

It is also likely, that you will have multiple Xcode Cloud workflows and some of them might require different build scripts than others. Since only one script per type is supported in the ci_scripts directory, you can use helper scripts to customize and split up tasks of the default script. A good practice is to avoid the ci_ prefix for helper scripts, to make them more recognizable, especially if you are using a larger number of scripts.

For example, you could detect the intended platform the project is built for and use separate scripts to perform tasks depending on the platform.

#!/bin/sh

if [ CI_PRODUCT_PLATFORM = 'macOS' ]
then
    ./macos_perform_example_task.sh
else
    ./iOS_perform_example_task.sh
fi

As you may notice, the script uses aCI_PRODUCT_PLATFORM variable to detect the platform. This is one of many default environment variables, which are powerful components for your build script workflow. Let's explore this in more detail.

Environment Variables

To provide extensive flexibility when writing custom build scripts with advanced control flows, Xcode Cloud comes with a predefined set of environment variables. These can be used to determine the next steps within your build scripts and allow in-depth customization based on current conditions. By default, three types of environment variables can be considered

  1. Variables that Are Always Available: These variables are available every time Xcode Cloud workflows are run, independent of any start conditions or actions that may be defined in the workflow. Some example are CI_BUILD_ID a unique identifier for the current build, CI_BUNDLE_ID the bundle ID of the product, CI_PRODUCT_PLATFORM the platform (iOS, macOS, tvOS or watchOS,  CI_XCODE_SCHEME the scheme that the current action uses, and many others.
  2. Variables for Specific Start Conditions: These variables are available based on specific start conditions defined in the workflow and might not be available every time Xcode Cloud workflows are run. Some examples are CI_BRANCH the name of the source branch, CI_PULL_REQUEST_NUMBER the pull request’s number,  CI_TAG the name of the tag that Xcode Cloud checked out for the current build and many others.
  3. Variables for Specific Actions: The variables are available depending on the action that Xcode Cloud performs, such as Archiving or Testing. Some example are CI_RESULT_BUNDLE_PATH the path to the test action’s result bundle, CI_TEST_DESTINATION_DEVICE_TYPE the device type of the test action,  CI_TEST_DESTINATION_RUNTIME the OS version of the simulated device used for testing, CI_ARCHIVE_PATH the path to the exported app archive and many others.

For a more detailed overview of the available environment variables available in Xcode Cloud workflows, check the Environment Variable Reference in the official Apple Developer Documentation.

These environment variables can be key to advanced and powerful scripts. As an example, the following code checks for the CI_PULL_REQUEST_NUMBER to only execute a command when Xcode Cloud runs the script as part of a build from a pull request.

#!/bin/sh

if [[ -n $CI_PULL_REQUEST_NUMBER ]];
then
    echo "This build started from a pull request."

    # Perform an action only if the build starts from a pull request.
fi

Another example that Apple also shared in their session Customize your advanced Xcode Cloud workflows at WWDC 2021, is to replace the app icon with a custom beta app icon whenever a new beta is released on Testflight. This for example could happen in a workflow that has an archive action and is based on a pull request to the Git repository.

#!/bin/sh

if [[ -n $CI_PULL_REQUEST_NUMBER && $CI_XCODEBUILD_ACTION = 'archive' ]];
then
    echo "Replacing app icon with beta app icon."
    APP_ICON_PATH=$CI_WORKSHPACE/Shared/Assets.xcassets/AppIcon.appiconset

    # Remove existing app icon
    rm -rf $APP_ICON_PATH
    
    # Add beta app icon
    mv "$CI_WORKSPACE/ci_scripts/AppIcon-Beta.appiconset" $APP_ICON_PATH
fi

For this to work, you need to place the alternative .appiconset has to be placed inside the ci_scripts directory to be accessible then the script is run. This script but be run as a Pre-Xcodebuild script and be stored as ci_pre_xcodebuild.sh in the ci_scripts folder.

Custom Environment Variables

Beyond that, you can also define additional environment variables as needed. Such environment variables can be configured for the Xcode Cloud workflow. For this navigate to the Report Navigator in Xcode and edit your workflow. Inside the Xcode Cloud workflows settings, the Environment can be configured by specifying the Xcode and macOS version as well as any additional Environment Variable.

A typical use case for this could be a secret environment variable that contains an API key to be used in a custom build script, for example, to access certain resources or upload data to servers, etc.

For example, when using the Mapbox SDK for iOS with Xcode Cloud, the environment can be used to set the MAPBOX_TOKEN and add the Mapbox API key. To securely store an environment variable and obscure it from any appearance in logs, you can select the Secret checkbox when adding the value to the variable. To learn more about configuration Xcode Cloud workflows, explore the Xcode Cloud Workflow Reference in the official Apple Developer Documentation.

If you want to know more about how to set up the Mapbox SDK with Xcode Cloud workflows, refer to our article  "Using the Mapbox SDK for iOS with Xcode Cloud" which covers how to get started with Mapbox on Xcode Cloud.


To learn more about Xcode Cloud and how to automate building, testing, and deploying your app projects, stay tuned for more articles on Xcode Cloud and join our free mailing list to not miss when new content is out.