Reading and Understanding the CMake in apple/swift

In my last post, I invoked cmake in order to build apple/swift. When I invoked cmake, I gave it the path to a source code directory. cmake then executes the CMakeLists.txt file in that directory.

In the case of apple/swift, that means the swift/CMakeLists.txt CMake script file is executed.

swift/CMakeLists.txt, in turn, calls the CMake function add_subdirectory() to include its child directories. The CMakeLists.txt files in those subdirectories then include their children, and so on:

~/local/Source/apple/swift/CMakeLists.txt
    lib/CMakeLists.txt
        Driver/CMakeLists.txt
            Action.cpp, Compilation.cpp, ...
        Frontend/CMakeLists.txt
            Frontend.cpp, FrontendOptions.cpp, ...
        ...
     tools/CMakeLists.txt
        ...
     test/CMakeLists.txt
        ...

Here's an animated "time-lapse" of this process.

This recursive execution of build system code is referred to as the recursive make pattern. The LLVM, Clang, and apple/swift projects all structure their CMake code following this pattern.

By convention, a CMakeLists.txt file in a directory describes how the source code in that directory is built. For example, to understand how the source code in swift/lib/Driver is compiled, I would first read the swift/lib/Driver/CMakeLists.txt file.

In addition, apple/swift defines helper CMake functions in the swift/cmake/modules directory. The top-level swift/CMakeLists.txt file includes these CMake functions by first appending the directory to the CMake module path…

swift/CMakeLists.txt

4  list(APPEND CMAKE_MODULE_PATH
5      "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules")

…and then including them by name by using the CMake include() function:

swift/CMakeLists.txt

15  include(SwiftUtils)        # Loads swift/cmake/modules/SwiftUtils.cmake.
16  include(CheckSymbolExists) # Loads swift/cmake/modules/CheckSymbolExists.cmake.

Including the two files above makes functions defined therein available to all CMake scripts in the project. For example, swift/cmake/modules/SwiftUtils.cmake defines a function named precondition(), which halts CMake with an error at configuration time if a variable is not defined.

I find reading the CMake in apple/swift to be straightforward, because the order in which the CMake is executed is clear. All I have to do is read from the top of swift/CMakeLists.txt to the bottom, following any calls to include() or add_subdirectory().

Reading apple/swift CMake by example: the swift executable

The above description should be enough to determine how, for example, the swift compiler executable is configured and built. Here's how I found out:

First, I know that swift is, at its core, just a command-line C++ program, so it must have a main() function I can search for:

git \
    -C ~/local/Source/apple/swift \
    grep "int main("

The apple/swift project contains many C++ programs, and so this search returns several results. Looking at the returned file names and their contents, however, it soon becomes clear that swift/tools/driver/driver.cpp is the file that defines the entry point to the swift executable.

As I mentioned above, I can usually find out how a source code file is compiled by looking for a CMakeLists.txt file in the same directory. Here, swift/tools/driver/CMakeLists.txt is in the same directory as driver.cpp. The first few lines of code in that CMake file are exactly what I'm looking for:

swift/tools/driver/CMakeLists.txt

 1  add_swift_host_tool(swift
 2    api_notes.cpp
 3    driver.cpp
 4    autolink_extract_main.cpp
 5    modulewrap_main.cpp
 6    swift_format_main.cpp
 7    LINK_LIBRARIES
 8      swiftDriver
 9      swiftFrontendTool
10    LLVM_COMPONENT_DEPENDS
11      DebugInfoCodeView
12    SWIFT_COMPONENT compiler
13  )

It appears this add_swift_host_tool() function takes the name of the executable to be built (swift), a list of source files (api_notes.cpp driver.cpp ...), a list of libraries to link (LINK_LIBRARIES swiftDriver ...), a list of LLVM dependencies, and something called a SWIFT_COMPONENT.

Based on its name, I can guess that the add_swift_host_tool() function is not built into CMake, so to understand exactly what it does, I can't consult the CMake documentation. Instead, I can git grep for its definition:

git \
    -C ~/local/Source/apple/swift \
    grep --line-number \
    "function(add_swift_host_tool"

The above command outputs:

cmake/modules/AddSwift.cmake:2104:function(add_swift_host_tool executable)

So it looks like the add_swift_host_tool() function is defined in swift/cmake/modules/AddSwift.cmake.

For add_swift_host_tool() to be called from within swift/tools/driver/CMakeLists.txt, the AddSwift.cmake module must have been included at some point. Sure enough, a git grep for "include(AddSwift)" shows that it is included from within swift/CMakeLists.txt. No magic here!

Here's the definition of add_swift_host_tool():

swift/cmake/modules/AddSwift.cmake

2104  function(add_swift_host_tool executable)
2105    cmake_parse_arguments(
2106       ADDSWIFTHOSTTOOL # prefix
2107       "" # options
2108       "" # single-value args
2109       "SWIFT_COMPONENT" # multi-value args
2110       ${ARGN})
2111  
2112    # Create the executable rule.
2113    add_swift_executable(${executable} ${ADDSWIFTHOSTTOOL_UNPARSED_ARGUMENTS})
....  
2130  endfunction()

First, the add_swift_host_tool() function parses any arguments it has been invoked with, by using the CMake function cmake_parse_arguments(). Then, it calls add_swift_executable() with a subset of those arguments.

Like add_swift_host_tool(), the add_swift_executable() function is also defined within the cmake/modules/AddSwift.cmake module. It's documented as "add an executable for the host machine."

The "host machine" part of the documentation above is important. apple/swift CMake is responsible for describing how to build not only executables like swift, but also the Swift runtime and standard library. The Swift runtime could be used on the macOS machine I'm using to build apple/swift, or it could be run on an iOS device, or a tvOS device, or even an Android device.

apple/swift CMake refers to the machine I'm using to configure and build apple/swift as the host machine. It refers to the platform I'm building for as the target. macOS, iOS, tvOS, and Android are all valid targets.

add_swift_executable() describes how to build an executable for the host machine. In my case, that's the macOS machine I'm using to configure and build apple/swift:

swift/cmake/modules/AddSwift.cmake

2063   function(add_swift_executable name)
....   
2083     _add_swift_executable_single(
2084         ${name}
2085         ${SWIFTEXE_SOURCES}
2086         DEPENDS ${SWIFTEXE_DEPENDS}
2087         LLVM_COMPONENT_DEPENDS ${SWIFTEXE_LLVM_COMPONENT_DEPENDS}
2088         LINK_LIBRARIES ${SWIFTEXE_LINK_LIBRARIES}
2089 SDK ${SWIFT_HOST_VARIANT_SDK}
2090 ARCHITECTURE ${SWIFT_HOST_VARIANT_ARCH} 2091 ${SWIFTEXE_EXCLUDE_FROM_ALL_FLAG} 2092 ${SWIFTEXE_DONT_STRIP_NON_MAIN_SYMBOLS_FLAG} 2093 ${SWIFTEXE_DISABLE_ASLR_FLAG}) 2094 endfunction()

All add_swift_executable() does is parse arguments and call another function, _add_swift_executable_single(). When it does so, it sends the argument SDK ${CMAKE_HOST_VARIANT_SDK}.

The term "SDK" here is misleading; it means the target for which to build the executable. In this case, its being set as the value of the variable CMAKE_HOST_VARIANT_SDK, which is set to "OSX" at a higher level of the recursive CMake.

How can I be sure the value of CMAKE_HOST_VARIANT_SDK is being set to "OSX"?

There are two ways to confirm the CMAKE_HOST_VARIANT_SDK that's being passed as an argument to _add_swift_executable_single(). The first is by using the CMake function message():

cmake/modules/AddSwift.cmake

++++ message(
++++ STATUS
++++ "Adding Swift executable '${name}' for "
++++ "SDK '${SWIFT_HOST_VARIANT_SDK}'")
2083 _add_swift_executable_single( 2084 ${name} 2085 ${SWIFTEXE_SOURCES} ....

Adding the above results in the following output when I run cmake to configure the project:

-- Adding Swift executable 'swift' for SDK 'OSX'

Another way is to run cmake using the --trace-expand option. This prints to stderr every single line of CMake that is executed, along with the values of the variables on those lines. That's a ton of output, but I can use grep to filter it for the specific line that swift is added:

cmake --trace-expand \
    ... 2>&1 | grep "_add_swift_executable_single(swift "

That outputs the following:

/Users/bgesiak/local/Source/apple/standalone/swift/cmake/modules/AddSwift.cmake(2083):
  _add_swift_executable_single(
      swift 
      api_notes.cpp;driver.cpp;autolink_extract_main.cpp;modulewrap_main.cpp;swift_format_main.cpp
      DEPENDS
      LLVM_COMPONENT_DEPENDS DebugInfoCodeView
      LINK_LIBRARIES swiftDriver;swiftFrontendTool
      SDK OSX
      ARCHITECTURE x86_64
  )

I can see in both of the outputs above that SDK OSX is set.

The _add_swift_executable_single() function is the one that finally calls into the built-in CMake function add_executable() in order to define the swift executable target:

swift/cmake/modules/AddSwift.cmake

1825   function(_add_swift_executable_single name)
....   
1907     add_executable(${name}
1907         ${SWIFTEXE_SINGLE_EXCLUDE_FROM_ALL_FLAG}
1907         ${SWIFTEXE_SINGLE_SOURCES}
1907         ${SWIFTEXE_SINGLE_EXTERNAL_SOURCES})
....   
1937     target_link_libraries("${name}" ${SWIFTEXE_SINGLE_LINK_LIBRARIES} ${SWIFTEXE_SINGLE_LINK_FAT_LIBRARIES})
....   
1942   endfunction()

You may recall that add_executable() is the same function I used to describe the hello executable, in my toy CMake example from my last post. The function is how to describe, in CMake, that an executable is to be built.

In addition, _add_swift_executable_single() then calls target_link_libraries(). This is another built-in CMake function. It specifies that the swift executable depends upon the swiftDriver and swiftFrontendTool libraries.

A recap of how apple/swift CMake describes the swift executable

In summary, the story of how apple/swift CMake describes the swift executable is as follows:

  1. When cmake is run to configure the project, swift/CMakeLists.txt is executed. It adds swift/cmake/modules to the CMake module path, and it includes swift/cmake/modules/AddSwift.cmake, which defines the add_swift_host_tool() function.
  2. swift/CMakeLists.txt then calls add_subdirectory() on each of its subdirectories, including swift/tools. Each of these subdirectories contains a CMakeLists.txt file that includes their subdirectories in turn. swift/tools/CMakeLists.txt is no exception, of course, and it calls add_subdirectory() on swift/tools/driver.
  3. swift/tools/driver/CMakeLists.txt contains the CMake code that describes how to build the swift executable. It invokes the add_swift_host_tool() function. This eventually calls CMake's built-in functions, add_executable() and target_link_libraries(), in order to describe a swift executable that is linked to swiftDriver and swiftFrontend.

The challenge for a contributor who hopes to improve Swift's CMake isn't that the CMake behaves in a magical way, or does unintuitive things. Rather, the challenge is simply that there is a lot of CMake code: _add_swift_executable_single() alone is nearly 100 lines long.

The entire apple/swift project has 189 CMake files, with over 8,000 significant lines of code. It also makes use of CMake functions defined in LLVM, which itself has 347 CMake files that contain over 10,000 significant lines!

As a result, it's not practical for me to describe how every single component in apple/swift is configured. However, if you're interested in how, for example, the sil-opt executable is configured, this post has hopefully shown how you can find out for yourself.

In an upcoming post, I'll use the methods above to learn and write about how some of the main components of the apple/swift project are configured:

Afterwards, I'll also write about specific CMake configurations that allow me to build just what I'm working on, in order to iterate quicker on Swift compiler development.