Previous on Setup playground, we follow instructions from swift open source project to build an swift compiler as an executable. Particularly, we use
/wtsc/swift/utils/build-script
which is a python script, and invoke it on terminal with some arguments.
/wtsc/swift/utils/build-script \
--skip-build-benchmarks \
--cmake-c-launcher="$(which sccache)" \
--cmake-cxx-launcher="$(which sccache)" \
--release \
--debug-swift \
--llvm-targets-to-build="X86"
This whole command majorly means to build swift in debug mode whereas other components(clang, lldb, etc) in release mode. Then we can use lldb to debug swift compiler after changing its source code for experiments.
Before diving into source code of swift compiler, we need to understand the inside mechanism of the swift build system, that is how the build-script works. That will help us a lot during this journey.
First of all, build system is a helper to build what we need from the same souce code and related materials. For instance, the command above will invoke build system to build an debuggable swift compiler with other tools and libraries in release mode on X86 plaform which we don't debug other components except swift compiler. People can also build swift for other target platform like iOS with ARM. That means we can use the same source code but with different configures to satisfy our needs for different targets.
Now the following overviwe diagram shows us the build-script will call a bash script named build-script-impl which will then invoke cmake, a build generator, to generate build.ninja to indicate Ninja how to build the whole project.
Because Ninja just faithfully executes exactly what the cmake say, and the main logic of build system is written in cmake scripts, CMakeLists.txt on each level of directory started from root source directory (our example is "/wtsc/swift"). And build-script and build-script-impl is meant to provide user-friendliness and preparation for cmake. This means we can build swift project by using cmake directly if we want to get our hand dirty. This insight also facilitate incremental build after modifying some change on source code.
Thus let begin with the core of build system: CMake.
In CMake's world, what cmake want to do is building targets in order. And target can be executable, library, alias to other target, or custom tasks. Therefore target is an abtract object that describe what cmake do and its relations to other target. Moreover, targets have their propeties as well to describe how cmake do with other global propeties. And the relations among targets will become a large dependency graph to transit some propeties (public) from upstream target to downstream target. In addition that dependency graph also shows which target should be built before which one.
Based on the above view of CMake's world, the cmake script we write would majorly describe what targets we need, like swift compiler executable, and setup their properties.
Minimal Example of CMake.
/wtsc
|__cmake_example
|__helloworld.cpp
|__CMakeLists.txt
|__cmake_example_build
/* /wtsc/cmake_example/helloworld.cpp */
#include <iostream>
int main() { std::cout << "helloworld!\n"; }
# /wtsc/cmake_example/CMakeLists.txt
# This should be first cmake command
# indicating what are the default behaviors
# according to different version.
cmake_minimum_required(VERSION 3.12)
# Name of this c++ project
project(Hello cxx)
# Setup helloworld executable target
# with helloworld.cpp as its source code file
# and default properties
add_executable(helloworld helloworld.cpp)
$:cmake -H/wtsc/cmake_example -B/wtsc/cmake_example_build -G Ninja
$:cmake -C /wtsc/cmake_exmaple_build
$:/wtsc/cmake_example_build/helloworld
helloworld!
By this small example, we can feel how cmake to build an executable. And the whole swift project is a more complex one without magic. So let's try to build swift directly with cmake.
We can take advantage of build-script to see what bash command are executed to build swift project. By doing this, we add --dry-run argument to build-script as following.
/wtsc/swift/utils/build-script \
--skip-build-benchmarks \
--cmake-c-launcher="$(which sccache)" \
--cmake-cxx-launcher="$(which sccache)" \
--release \
--debug-swift \
--llvm-targets-to-build="X86" \
--dry-run
Then we can see a list of commands which are prefixed with +. Thus we can copy and paste those commands manually on terminal, build swift directly with cmake.
Those commands organize how the builds place and do some preparation, then invoke cmake to build the project. Furthermore, they also show build-script and build-script-impl is to make up those commands in order to call cmake to build the whole project.
Seperating the build job further, build-script is responsible to interact with user, then call build-script-impl with specific arguments according to user's input, which as hint of its name, implements concrete logic of how to emit cmake commands to build the whole project.
For more details of build system, readers can read the source code of build-script, build-script-impl and CMakeLists.txt on each source code directories.