How to setup Xcode Cloud with Kotlin Multiplatform (KMM/KMP)

When starting with Kotlin Multiplatform, it can get a little bit overwhelming with all the new changes you have to make to your project. You also might be tempted to move all the CI/CD logic into Github actions (centralized for iOS and Android) but it's a pity to leave the generous 25 hours of allowance that Xcode Cloud offers.

Here's a straightforward guide to enabling Xcode Cloud to build your Kotlin Multiplatform project:

Step 1: Project or Workspace Configuration

Set the project path for your workflow to include the iOSApp folder. This can be done either from the Xcode Cloud website or within Xcode:

  1. Go to Xcode -> Integrate -> Manage Workflow -> Select the workflow -> General.
  2. Set the path to include the iOSApp folder (e.g., /iosApp/Example.xcodeproj).

Replace iosApp with the name of your iOS folder in the Kotlin Multiplatform project.

Step 2: CI Script Setup

Create a folder named ci_scripts in the root directory of your iOS project. This folder will contain scripts to run before or after the build process. Inside ci_scripts, create a file named ci_post_clone.sh with the following script (credit):

#!/bin/sh

root_dir=$CI_WORKSPACE_PATH
repo_dir=$CI_PRIMARY_REPOSITORY_PATH
jdk_dir="${CI_DERIVED_DATA_PATH}/JDK"

gradle_dir="${repo_dir}/Common"
cache_dir="${CI_DERIVED_DATA_PATH}/.gradle"

jdk_version="20.0.1"

# Check if we stored gradle caches in DerivedData.
recover_cache_files() {
    
    echo "\nRecover cache files"

    if [ ! -d $cache_dir ]; then
        echo " - No valid caches found, skipping"
        return 0
    fi

    echo " - Copying gradle cache to ${gradle_dir}"
    rm -rf "${gradle_dir}/.gradle"
    cp -r $cache_dir $gradle_dir

    return 0
}

# Install the JDK
install_jdk_if_needed() {

    echo "\nInstall JDK if needed"

    if [[ $(uname -m) == "arm64" ]]; then
        echo " - Detected M1"
        arch_type="macos-aarch64"
    else
        echo " - Detected Intel"
        arch_type="macos-x64"
    fi

    # Location of version / arch detection file.
    detect_loc="${jdk_dir}/.${jdk_version}.${arch_type}"

    if [ -f $detect_loc ]; then
        echo " - Found a valid JDK installation, skipping install"
        return 0
    fi

    echo " - No valid JDK installation found, installing..."

    tar_name="jdk-${jdk_version}_${arch_type}_bin.tar.gz"

    # Download and un-tar JDK to our defined location.
    curl -OL "https://download.oracle.com/java/20/archive/${tar_name}"
    tar xzf $tar_name -C $root_dir

    # Move the JDK to our desired location.
    rm -rf $jdk_dir
    mkdir -p $jdk_dir
    mv "${root_dir}/jdk-${jdk_version}.jdk/Contents/Home" $jdk_dir

    # Some cleanup.
    rm -r "${root_dir}/jdk-${jdk_version}.jdk"
    rm $tar_name

    # Add the detection file for subsequent builds.
    touch $detect_loc

    echo " - Set JAVA_HOME in Xcode Cloud to ${jdk_dir}/Home"

    return 0
}

recover_cache_files
install_jdk_if_needed

This script installs JDK in the environment to compile the shared logic part of KMM. Run the workflow and look for the message in the logs:

Set JAVA_HOME in Xcode Cloud to ${jdk_dir}/Home

Copy the JAVA_HOME value (e.g., /Volumes/workspace/DerivedData/JDK/Home) for the next step.

Step 3: Environment Variable Configuration

In Xcode Cloud, select the workflow and add a new environment variable:

JAVA_HOME=/Volumes/workspace/DerivedData/JDK/Home

Run the workflow, and your app should build successfully.

That's it! Happy coding!