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 withinswift/tools/driver/CMakeLists.txt
, theAddSwift.cmake
module must have been included at some point. Sure enough, agit grep
for"include(AddSwift)"
shows that it is included from withinswift/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 functionmessage()
: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 usegrep
to filter it for the specific line thatswift
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:
- When
cmake
is run to configure the project,swift/CMakeLists.txt
is executed. It addsswift/cmake/modules
to the CMake module path, and it includesswift/cmake/modules/AddSwift.cmake
, which defines theadd_swift_host_tool()
function. swift/CMakeLists.txt
then callsadd_subdirectory()
on each of its subdirectories, includingswift/tools
. Each of these subdirectories contains aCMakeLists.txt
file that includes their subdirectories in turn.swift/tools/CMakeLists.txt
is no exception, of course, and it callsadd_subdirectory()
onswift/tools/driver
.swift/tools/driver/CMakeLists.txt
contains the CMake code that describes how to build theswift
executable. It invokes theadd_swift_host_tool()
function. This eventually calls CMake's built-in functions,add_executable()
andtarget_link_libraries()
, in order to describe aswift
executable that is linked toswiftDriver
andswiftFrontend
.
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:
- The Swift compiler libraries that are used by the
swift
executable. I'll explore how LLVM tools such astablegen
are used when, for example, definingswift
command-line options. - The Swift runtime and standard library. I'll write about the
handle_swift_sources()
function, which describes how Swift source files should be compiled and included in the finished library. - The Swift test suite. I'll explore how a Python script executes the unique set of tests used by the project.
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.
Tweet