Tuist Your Circle — Building A Project With CircleCI — Part 1

Ronan O Ciosoig
eDreams ODIGEO
Published in
6 min readMay 19, 2022

--

Machines of the mill. A small section of a giant beast found at Leeds Industrial Museum, by Mike Hindle

If you have worked on a large software project with several other developers, all of you on different features, you will know all about the pain of merging them all together. It can often be a tedious and error-prone process. If only there was a better way to build an app and run all the tests every time code changes are pushed to the online repository then you could have an early warning of those conflicting changes and take corrective actions.

Continuous Integration (CI) is termed as the process of merging code changes multiple times a day, and a hosted CI service is one that can be configured to trigger a series of steps (workflows) to build and test that project. This is essential for any mid-to-large software project.

In this series of articles I will go very deep into how to configure iOS projects based on tuist, and show how to optimise build times by taking advantage of various forms of caching to get the best outcome for a development team. In this article I will cover the basics of how to get a simple iOS project up and building on CircleCI, explaining the YML syntax, which sets the stage for the series.

Although there are many CI systems and services available (Jenkins, TeamCity, Travis, etc.), and although Tuist integration has already been described for Bitrise and GitHub Actions, I am investigating CircleCI not only because my current company, eDreams, is using this service, but also because it is a truly powerful service and one of the best available today.

The project referenced in this article is here.

CircleCI YML Basics

The CI/CD process is orchestrated through a single file called config.yml, which must be located inside a .circleci folder in the root of the project. The most important syntax are the following:

Steps: Run commands and shell scripts to do the work required for your project.

Jobs: A collection of steps.

Workflows: A list of jobs and their run order.

Orbs: Orbs are reusable snippets of code that help automate repeated processes, accelerate project setup, and make it easy to integrate with third-party tools.

Workspace: A storage mechanism unique to a job, which may be needed in downstream jobs. Each workflow has a temporary workspace associated with it. The workspace can be used to pass along unique data built during a job to other jobs in the same workflow.

Cache: A cache stores a file or directory of files such as dependencies or source code in object storage. This can be used to speed-up build times by retrieving data from previous builds.

Each job must declare an executor, in this casemacos, and this executor requires an Xcode version to be specified. For example:

jobs: 
build:
macos:
xcode: "13.3.1"

A step can run the common commands that you would expect to find in a terminal. Some examples:

run: ls -la
run: echo "Hello"
run:
name: Another Example
command: echo "Good bye"

I have found the ls command particularly useful to debug the config YML.

Configuration For iOS Projects

The general steps for a project that uses CocoaPods for dependency management are the following:

  • checkout
  • pod install
  • xcodebuild build
  • xcodebuild test

The XcodeBuild command also needs the workspace and scheme to execute correctly.

The recommended orb for iOS apps is macos: circleci/macos@2.2.0 It provides some commands for UI testing like adding permissions.

For iOS projects the first few lines should be:

# .circleci/config.yml version: 2.1orbs: 
macos: circleci/macos@2.2.0

If you are not familiar with YML files, indentation of 2 spaces is key to it working. An extra or missing space and it won’t work, and is the most common reason it will fail to run.

The MacOS environment on CircleCI comes with pre-installed with HomeBrew, Ruby and a whole host of command line tools typical of unix-based systems.

There is an issue with the xcodebuild command if a team is not defined and the signing certificate is not specified — it fails to build. This can be resolved by explicitly specifying the simulator device by defining the destination like so: -destination “platform=iOS Simulator,name=iPhone 11”.The same applies for running the test target. Thus the working configuration for build and test is now this:

jobs: 
build_and_test:
macos:
xcode: "13.3.1"
steps:
- checkout
- run: pod install
- run: xcodebuild build-for-testing -workspace "Pokedex.xcworkspace" -scheme "Pokedex" -destination “platform=iOS Simulator,name=iPhone 11”
- run: xcodebuild test-without-building -workspace "Pokedex.xcworkspace" -scheme "Pokedex" -destination “platform=iOS Simulator,name=iPhone 11”

The workflow needs to call this job:

workflows:
build_test_workflow:
jobs:
build_and_test

On saving the change in the YML in the editor on CircleCI, that will trigger a run of the workflow and it succeeds

CircleCI passing workflow for build and test

On clicking the workflow this opens the detailed window showing the timings for each step.

The timing tab presents a bar chart comparison that makes it very visually clear which step consumes the most of the CI time.

Lint The Code

One thing that is missing here is running SwiftLint. A script is defined in the Xcode project build phase, but it merely prints a warning if it isn’t found. Since HomeBrew comes pre-installed the command to install swift lint is simply: brew install swiftlint. The only drawback with this is that it will take almost 1m 30s to complete because HomeBrew automatically updates all the libraries. Fortunately the fix is trivial:

HOMEBREW_NO_AUTO_UPDATE=1 brew install swiftlint

By blocking updates, the install takes a mere 9 seconds, which is definitely acceptable in my books.

Optimising Pod Install

Although the pod install command is being run in the config.yml above for every run of the workflow, and the recommended approach to removing this time is to commit them into the repo. it can also be optimised by using a cache.

- restore_cache:
name: Restore CocoaPods
keys:
- pokedex-{{ checksum "Podfile.lock" }}
- pokedex-
- run: pod install
- save_cache:
name: Save CocoaPods
key: pokedex-{{ checksum "Podfile.lock" }}
paths:
- ./Pods

With a restore cache before the install, and save after, the time for the install was reduced from 13s to 9s. Although this isn’t significant, if there were a lot of pods to install that impact would be far more meaningful.

Note that caches cannot be updated or deleted so the above command to save a cache depends on the hash of the lock file, and only when that file changes will a new cache be saved.

Workflows And Jobs

In this example building and testing are completed in the same job, but they could be broken up into separate jobs that can run in parallel or in a specific order with conditions, but this will be explained in a future article.

Be Less Trigger Happy

With build and test jobs now defined and working, the last point to note is that by default every push will trigger the workflow which is not the most efficient use of the CI system, but it is useful when starting on the configuration. In the project settings (click on the top right) there is a flag to set the trigger to only fire on pull requests: Select advanced and scroll down to this and enable it:

Now it’s ready for prime time! Well, almost.

Recap

In this article I provided an introduction to the basics of the syntax and commands to get it all running. In the next part of this series I will describe all the steps to get a tuist-based project configured, taking advantage of both the CircleCI cache and tuist caching to optimise running all the test targets.

The config.yml in the repo for this article is here, and the CircleCI project is here.

If you find this useful, please follow to get the next updates.

--

--

Ronan O Ciosoig
eDreams ODIGEO

iOS software architect eDreams ODIGEO and electronic music fan