The Swift Compiler's Build System

My last post explained that invoking swift/utils/build-script compiles the C++ source code in the apple/swift project and produces a swift compiler executable.

swift/utils/build-script is a Python script that invokes another script, swift/utils/build-script-impl. That, in turn, invokes cmake, in order to configure and then compile the source code in apple/swift.

I'll refer to this process as the Swift build system, and I'll explain it in detail below.

swift/utils/build-script invokes swift/utils/build-script-impl, which invokes cmake, which configures and builds apple/swift.

There are many benefits to having a deep understanding of the build system:

I'll begin with the build system's core: CMake.

Using CMake: An example

The cmake executable reads script files that describe how a software project builds. For example, here's how I'd use it to build a simple C++ program, composed of a single source file, /tmp/src/hello.cpp:

/tmp/class/hello.cpp

#include <iostream>

int main() {
  std::cout << "Hello!" << std::endl;
  return 0;
}

I can describe how to build this program using the CMake language, in a file named src/CMakeLists.txt:

/tmp/src/CMakeLists.txt

# CMake version 3.2 or greater must
# be used to configure this project.
cmake_minimum_required(VERSION 3.2)

# The name of the project is 'Hello'.
project(Hello)

# When built, create an executable
# named 'hello', by compiling the
# source file 'hello.cpp'.
add_executable(hello
               hello.cpp)

Running cmake on the command line generates build files that are used to actually build the project:

# Read the CMakeLists.txt in the '/tmp/src' directory,
# and generate build files at '/tmp/build'.
cmake -H/tmp/src -B/tmp/build

I can then compile the hello executable by executing the build files in /tmp/build. By default, CMake generates a Makefile, which I can execute like so:

make -C /tmp/build

Running the above command compiles and links the executable /tmp/build/hello. Running that program outputs the expected text, "Hello!".

It's important to note that CMake itself doesn't compile and link the hello executable. CMake generates build files, and those build files compile and link hello. I can instruct CMake to generate different kinds of build files, including an Xcode project:

cmake -H/tmp/src -B/tmp/build-with-xcode -G Xcode

The above command generates /tmp/build-with-xcode/Hello.xcodeproj. I can open Hello.xcodeproj in Xcode and build hello by clicking the "Run" button:

Building the CMake project via the Xcode GUI.

I can also build hello via the command line, by invoking xcodebuild:

xcodebuild -project /tmp/build-with-xcode/Hello.xcodeproj

Notice how, when I didn't specify a build file generator using the cmake -G argument, cmake generated a Makefile, that I could build by invoking make. The Xcode project, on the other hand, was built by invoking xcodebuild. Remembering which invocation to use is tiresome, so cmake provides a convenient way to build using whichever files were generated:

# Build using the Makefile in the
# /tmp/build directory.
cmake --build /tmp/build

# Build using the .xcodeproj in the
# /tmp/build-with-xcode directory.
cmake --build /tmp/build-with-xcode

CMake has many more features than just the three lines from my example above. I'll cover additional concepts below, as we encounter them in apple/swift's CMake code. To learn about CMake itself, try LLVM's CMake primer, or the latest official CMake documentation.

Why does apple/swift use CMake?

If you're an iOS developer, you've likely used Xcode project files to specify how your iOS application is built. So why couldn't the apple/swift project just use Xcode, too? The simple hello example above demonstrates several reasons why:

Building apple/swift with CMake

The apple/swift project is essentially the same as my "Hello" project above. Yes, it is a larger project with more CMake code, but it can be configured and built in the same way "Hello" can. Doing so is a useful exercise in understanding the apple/swift build infrastructure.

First, I'll clone the source code from the apple/swift project, as well as the three projects it depends upon:

  1. apple/swift-cmark: A Markdown parsing library, used by apple/swift when parsing Markdown documentation and comment blocks. This is Apple's fork of commonmark/cmark.
  2. apple/swift-llvm: apple/swift uses LLVM in a variety of ways. The most prominent is as a "backend": a library that generates the 0's and 1's that are capable of running on the target machine, whether that be an iOS armv7 device, a macOS x86_64 device, or even Android armv7. But apple/swift also uses LLVM utilities such as lit and FileCheck, two tools used to run the apple/swift test suite.
  3. apple/swift-clang: apple/swift makes use of Clang, a C and C++ compiler, as part of its C and Objective-C interoperability.

I'll clone these projects into a directory named ~/local/Source/apple/standalone:

git clone https://github.com/apple/swift-cmark.git \
    ~/local/Source/apple/standalone/swift-cmark

git clone https://github.com/apple/swift-llvm.git \
    ~/local/Source/apple/standalone/swift-llvm

# A bug in Swift's CMake requires this repository
# to be named 'clang', not 'swift-clang'.
# https://bugs.swift.org/browse/SR-5778
git clone https://github.com/apple/swift-clang.git \
    ~/local/Source/apple/standalone/clang

git clone https://github.com/apple/swift.git \
    ~/local/Source/apple/standalone/swift

Before I can configure and build apple/swift, I need to build its dependencies. First is apple/swift-cmark. I'll generate Ninja build files because they're faster:

# First configure.
cmake \
    -H~/local/Source/apple/standalone/swift-cmark \
    -B~/local/Source/apple/standalone/swift-cmark-build \
    -G Ninja

# Then build.
cmake --build \
    ~/local/Source/apple/standalone/swift-cmark-build

Next up is apple/swift-llvm and apple/swift-clang. LLVM"s CMake defines cache entry settings that allow users to build both LLVM and Clang at once. As an example of a cache entry setting, consider the following, defined in swift-llvm/CMakeLists.txt:

swift-llvm/CMakeLists.txt

124  set(LLVM_ENABLE_PROJECTS "" CACHE STRING
125          "Semicolon-separated list of projects to build (${LLVM_ALL_PROJECTS}), or \"all\".")

Users are able to set this value by invoking cmake -DLLVM_ENABLE_PROJECTS="foo" when configuring apple/swift-llvm. The empty string "" is the default value used when the user does not specify -DLLVM_ENABLE_PROJECTS= on the command line.

A full explanation of LLVM and Clang's CMake build system is outside of the scope of this article – perhaps I'll write about it more next year – but the following invocations configure and build apple/swift-llvm and apple/swift-clang:

# First configure.
cmake \
  -H~/local/Source/apple/standalone/swift-llvm \
  -B~/local/Source/apple/standalone/swift-llvm-build \
  -G Ninja \
  -DLLVM_ENABLE_PROJECTS=clang

# Then build.
cmake --build \
    ~/local/Source/apple/standalone/swift-llvm-build

# Finally, create a symlink from the build
# directory to the C++ headers included with Xcode.
# If you're following along, make sure the
# path to your Xcode beta is the same as below.
ln -s \
    /Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++ \
    ~/local/Source/apple/standalone/swift-llvm-build/include

Now, I can finally configure and build apple/swift itself. Like LLVM, apple/swift CMake defines many settings; for example, CMAKE_BUILD_TYPE is used to determine whether the swift executable is built with debugging symbols or not. I'll go into more of these options below, but for now the following invocations configure and build apple/swift:

# First configure.
cmake \
  -H~/local/Source/apple/standalone/swift \
  -B~/local/Source/apple/standalone/swift-build \
  -G Ninja \
  -DCMAKE_BUILD_TYPE="Debug" \
  -DSWIFT_PATH_TO_CMARK_SOURCE=$HOME/local/Source/apple/standalone/swift-cmark \
  -DSWIFT_PATH_TO_CMARK_BUILD=$HOME/local/Source/apple/standalone/swift-cmark-build \
  -DSWIFT_PATH_TO_LLVM_SOURCE=$HOME/local/Source/apple/standalone/swift-llvm \
  -DSWIFT_PATH_TO_LLVM_BUILD=$HOME/local/Source/apple/standalone/swift-llvm-build \
  -DSWIFT_PATH_TO_CLANG_SOURCE=$HOME/local/Source/apple/standalone/clang \
  -DSWIFT_PATH_TO_CLANG_BUILD=$HOME/local/Source/apple/standalone/swift-llvm-build

# Then build.
cmake --build \
    ~/local/Source/apple/standalone/swift-build

Once the build finishes, the built swift compiler executable is located at ~/local/Source/apple/standalone/swift-build/bin/swift. It works exactly as well as the executable I built using swift/utils/build-script in my last post.

What do swift/utils/build-script and swift/utils/build-script-impl do?

The apple/swift utils/build-script is a Python script that performs the exact same actions I did above:

  1. Use CMake to configure and build apple/swift-cmark.
  2. Use CMake to configure and build apple/swift-llvm and apple/swift-clang, as well as perform post-build configuration like symlinking C++ headers.
  3. Use CMake to configure and build apple/swift.

swift/utils/build-script accepts command-line arguments to specify how the project is built. The logic in the Python script translates these arguments into arguments to swift/utils/build-script-impl, a shellscript that in turn invokes cmake. For example, when a user invokes:

~/local/Source/apple/swift/utils/build-script --release

This in turn calls swift/utils/build-script-impl:

~/local/Source/apple/swift/utils/build-script-impl \
    --cmark-build-type=Release \
    --llvm-build-type=Release \
    --swift-build-type=Release

When invoked with these arguments, swift/utils/build-script-impl invokes cmake -DCMAKE_BUILD_TYPE=Release to configure and build apple/swift-cmark, apple/swift-llvm (and thus simultaneously apple/swift-clang), and apple/swift.

The shellscript swift/utils/build-script-impl is an unnecessary complication. Ideally, the Python script swift/utils/build-script would invoke cmake directly. A Swift bug report exists to get rid of swift/utils/build-script-impl, but that's easier said than done: the script contains over 3,000 lines of code.

swift/utils/build-script places the build products for each project in a separate directory. The full path of the directory is based upon the options used; for the invocation swift/utils/build-script --release --debug-swift, the products are placed at:

~/local/Source/apple/build/Ninja-ReleaseAssert+swift-DebugAssert/
    cmark-macosx-x86_64/ # swift-cmark build products.
    llvm-macosx-x86_64/  # swift-llvm and swift-clang build products.
    swift-macosx-x86_64/ # swift build products.

It's clear that, compared to invoking cmake multiple times to configure and build three separate projects, one invocation of swift/utils/build-script is much simpler for new apple/swift contributors:

In addition, some projects related to apple/swift, such as apple/swift-corelibs-foundation, do not include CMake files that describe how to build the project. swift/utils/build-script and swift/utils/build-script-impl take care of building these as well.

However, the simplicity comes at a cost:

This is a frequent stumbling block for new contributors to apple/swift.

Many experienced contributors recommend newcomers invoke ninja -C ~/local/Source/apple/build/swift-macosx-x86_64 directly in order to perform faster incremental builds of apple/swift. But a new contributor who doesn't know what swift/utils/build-script does will have a lot of questions:

  • "What is ninja?"
  • "What is the swift-macosx-x86_64 directory?"
  • "I used swift/utils/build-script --xcode in order to generate an Xcode project; attempting to invoke ninja results in an error. Why?"

Contributors familiar with the interaction between swift/utils/build-script and CMake know that swift/utils/build-script --xcode eventually calls cmake -G Xcode when configuring the apple/swift project, and so it stands to reason that invoking ninja on the configured project would not work. A slight improvement would be to recommend newcomers use cmake --build instead.

One last trick: "In-tree" builds of apple/swift

Configuring and building apple/swift using direct invocations of cmake gave me a new appreciation of the work done by swift/utils/build-script and swift/utils/build-script-impl. But it also made me notice that configuring and building Clang did not require a cmake invocation of its own.

The recommended way to build the Clang project is slightly different than the way I built apple/swift with CMake. Whereas building apple/swift required me to build LLVM first, Clang is built as part of the LLVM project. LLVM's CMake automatically detects when Clang is present at llvm/tools/clang. If it is, then LLVM's CMake includes Clang in the build.

It turns out that apple/swift can be built in this way as well:

# Clone apple/swift-llvm.
git clone https://github.com/apple/swift-llvm.git \
    ~/local/Source/apple/intree/swift-llvm

# Clone cmark, clang, and swift
# into swift-llvm/tools.
git clone https://github.com/apple/swift-cmark.git \
    ~/local/Source/apple/intree/swift-llvm/tools/cmark
git clone https://github.com/apple/swift-clang.git \
    ~/local/Source/apple/intree/swift-llvm/tools/clang
git clone https://github.com/apple/swift.git \
    ~/local/Source/apple/intree/swift-llvm/tools/swift

Configuring and building apple/swift "in-tree" requires fewer options to be specified on the command-line, because the paths to the apple/swift-llvm, apple/swift-cmark, and apple/swift-clang source and build directories are inferred by Swift's CMake code:

# First configure.
cmake \
    -H~/local/Source/apple/intree/swift-llvm \
    -B~/local/Source/apple/intree/build \
    -G Ninja

# Then build: first Clang, and then Swift.
cmake --build \
    ~/local/Source/apple/intree/build \
    -- clang swift

Compared to the "standalone" build I first tried, in-tree builds of apple/swift don't require me to remember to recompile apple/swift-llvm. If I make any changes to the apple/swift-llvm source code, they will be recompiled the next time I invoke cmake --build.

However, in-tree builds are not the official documented way to build apple/swift; if I encounter an error related to the build system, it's on me to file a bug and maybe even fix it myself.

For newcomers to apple/swift, I would recommend either using swift/utils/build-script, or the standalone setup.

Summary

Next Sunday morning, I'll post a "deep dive" into apple/swift's CMake code: how it uses a "recursive make" pattern, examples of how I read the code to see what it does, and some summaries of how things like the compiler itself, the standard library, and the test suite are built.