mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
309 lines
15 KiB
Markdown
309 lines
15 KiB
Markdown
# The Swift Driver, Compilation Model, and Command-Line Experience
|
|
|
|
_or, "why can't I only compile the files that changed?"_
|
|
|
|
The Swift compiler's command-line interface resembles that of other compilers,
|
|
particularly GCC and Clang. However, Swift's compilation model and some of its
|
|
language features make it a bit tricky to plug into a larger build system. In
|
|
particular, there's no correct way to specify a "one command per file" build
|
|
rule for a normal Swift module.
|
|
|
|
("What?" I know! Read on.)
|
|
|
|
The target audience for this document is people who want to integrate the Swift
|
|
compiler into their build system, rather than using Xcode or the package
|
|
manager (`swift build`). If you're looking to work on the driver itself...well,
|
|
this is probably still useful to you, but you should also check out
|
|
[DriverInternals.md](DriverInternals.md) and maybe
|
|
[DependencyAnalysis.md](DependencyAnalysis.md) as well. If you're just using
|
|
Xcode or SwiftPM and want to find out what mysterious command-line options you
|
|
could be passing, `swiftc --help` is a better choice.
|
|
|
|
If you're invoking `swift -frontend` directly, and you aren't working on the
|
|
compiler itself...well, this document should convince you to not do that.
|
|
|
|
Some terms:
|
|
|
|
- For the purposes of this document, a _module_ is a single distributable unit
|
|
of API. (We use this term for a lot of other things too, though; check out
|
|
[Lexicon.md](Lexicon.md) for the full list.) "Foundation" is a single
|
|
module, as is the Swift standard library ("Swift"). An app is a module too.
|
|
|
|
- A _compilation unit_ is a set of source files that are compiled together. In
|
|
Swift, everything intended to be in the same module must be part of the same
|
|
compilation unit, and every file in the same compilation unit are assumed to
|
|
be part of the same module. Doing anything else is unsupported.
|
|
|
|
- The _driver_ is the program that's run when you invoke `swift` or `swiftc`.
|
|
This doesn't actually compile anything itself; instead it invokes other tools
|
|
to produce the desired output.
|
|
|
|
- The _frontend_ is the program that actually compiles code (and in interpreter
|
|
mode, executes it via a JIT). It's hidden behind `swift -frontend`.
|
|
|
|
The frontend is an implementation detail. No aspects of its command-line
|
|
interface are considered stable. Even its existence isn't guaranteed.
|
|
|
|
- The _REPL_ is a mode of the debugger (LLDB) that is launched by the driver
|
|
when you invoke `swift` with no inputs. It doesn't actually come up again in
|
|
this document, but it's probably worth mentioning for completeness.
|
|
|
|
|
|
## What gets run? ##
|
|
|
|
Part of Swift's model is that a file can implicitly see entities declared
|
|
elsewhere in the same module as long as they have not been marked private.
|
|
Consequently, compiling any one file needs some knowledge of all the others.
|
|
However, we don't want a build model that rules out incremental builds, nor one
|
|
that forces non-parallel compilation. Consequently, the Swift driver has three
|
|
stages of subprocesses:
|
|
|
|
1. "Frontend jobs"
|
|
2. "Module merging"
|
|
3. Linking
|
|
|
|
Dependencies between the subprocesses are managed by the driver. Outputs are
|
|
controlled by `-o` and other various compiler flags; see `swiftc --help` for
|
|
more information.
|
|
|
|
|
|
### Frontend jobs ###
|
|
|
|
A normal Swift compilation starts off with as many _frontend jobs_ as input
|
|
files. Each invocation of the Swift frontend parses every file in the module,
|
|
but also has a particular file marked as the _primary file._ A job is only
|
|
responsible for compiling its primary file, and only does as much work as it
|
|
needs to compile that file, lazily type-checking declarations in other files
|
|
in the module.
|
|
|
|
A frontend job emits diagnostics, an object file, dependency information (see
|
|
"Incremental Builds" below), and a _partial module file._
|
|
|
|
|
|
### Module merging ###
|
|
|
|
Swift doesn't have header files; instead it uses a generated binary format
|
|
called a _Swift module file_ (.swiftmodule). With N frontend jobs, it becomes
|
|
necessary to stitch all the partial module files together. This job is also
|
|
performed by the frontend in a step called "module merging". Module merging
|
|
only produces a single merged module file (and accompanying documentation file
|
|
with the extension .swiftdoc), which can then be imported in later builds.
|
|
|
|
The Swift module file format is not stable; using it across compiler versions
|
|
is not supported. It also currently includes a ton of private information about
|
|
both your module and your compilation environment, so make sure you don't
|
|
distribute them with your final build products.
|
|
|
|
|
|
### Linking ###
|
|
|
|
The last stage of compilation is linking. In addition to actually invoking the
|
|
linker, the compiler needs to accomplish two other tasks:
|
|
|
|
- **Autolinking:** Swift object files encode information about what libraries
|
|
they depend on. On Apple platforms the linker can read this information
|
|
directly; on other platforms it's extracted using the
|
|
`swift-autolink-extract` helper tool. Of course the build system can also
|
|
provide manual link commands too.
|
|
|
|
- **Debugging:** In addition to the usual DWARF format, interactive debugging
|
|
of a Swift program requires access to its module. This is supported via a
|
|
custom linker flag on Apple platforms and by the `swift-modulewrap` tool
|
|
elsewhere. Apple platforms also invoke `dsymutil` after linking to produce a
|
|
dSYM debugging archive, so that the program is still debuggable even once the
|
|
object files have been cleaned.
|
|
|
|
|
|
## Output File Maps ##
|
|
|
|
> There are three numbers in computer science: 0, 1, and N.
|
|
|
|
The problem with running a separate frontend job for each input file is that it
|
|
suddenly becomes much more complicated to determine where outputs go. If you're
|
|
just going to link everything together at the end this isn't really a problem,
|
|
but a lot of build systems just want the compiler to produce object files. If
|
|
you want to use Swift's cross-file dependency tracking, or get Clang-style
|
|
serialized diagnostics (.dia files) instead of just text output, those are also
|
|
generated per-input-file. And some build systems (\*cough\*Xcode\*cough\*) want
|
|
to individually track persisted output files, instead of dumping them into a
|
|
temporary directory. Trying to specify the outputs for every input file on the
|
|
command line would be awkward and ridiculous, so instead we use an _output file
|
|
map._
|
|
|
|
An output file map contains a JSON dictionary that looks like this:
|
|
|
|
```json
|
|
{
|
|
"/path/to/src/foo.swift": {
|
|
"object": "/path/to/build/foo.o",
|
|
"dependencies": "/path/to/build/foo.d",
|
|
"swift-dependencies": "/path/to/build/foo.swiftdeps",
|
|
"diagnostics": "/path/to/build/foo.dia"
|
|
},
|
|
"/path/to/src/bar.swift": {
|
|
"object": "/path/to/build/bar.o",
|
|
"dependencies": "/path/to/build/bar.d",
|
|
"swift-dependencies": "/path/to/build/bar.swiftdeps",
|
|
"diagnostics": "/path/to/build/bar.dia"
|
|
},
|
|
"": {
|
|
"swift-dependencies": "/path/to/build/main-build-record.swiftdeps"
|
|
}
|
|
}
|
|
```
|
|
|
|
The build system is responsible for generating this file. The input file names
|
|
don't have to be absolute paths, but they do have to match the form used in the
|
|
invocation of the compiler. No canonicalization is performed. The special `""`
|
|
entry is used for outputs that apply to the entire build.
|
|
|
|
The "dependencies" entries refer to Make-style dependency files (similar to GCC
|
|
and Clang's `-MD` option), which can be used to track cross-module and header
|
|
file dependencies. "swift-dependencies" entries are required to perform
|
|
incremental builds (see below). The "diagnostics" entries are only for your own
|
|
use; if you don't need Clang-style serialized diagnostics they can be omitted.
|
|
|
|
The output file map accepts other entries, but they should not be considered
|
|
stable. Please stick to what's shown here.
|
|
|
|
(Note: In the example output file map above, all of the per-file outputs are
|
|
being emitted to the same directory. [#42949](https://github.com/swiftlang/swift/issues/42949)
|
|
covers adding a flag that would infer this behavior given a directory path.)
|
|
|
|
|
|
## Whole-Module Optimization ##
|
|
|
|
When the `-whole-module-optimization` flag is passed to Swift, the compilation
|
|
model changes significantly. In this mode, the driver only invokes the frontend
|
|
once, and there is no primary file. Instead, every file is parsed and
|
|
type-checked, and then all generated code is optimized together.
|
|
|
|
Whole-module builds actually have two modes: threaded and non-threaded. The
|
|
default is non-threaded, which produces a single object file for the entire
|
|
compilation unit (.o). If you're just producing object files, you can use the
|
|
usual `-o` option with `-c` to control where the single output goes.
|
|
|
|
In threaded mode, one object file is produced for every input source file, and
|
|
the "backend" processing of the generated code (basically, everything that's
|
|
not Swift-language-specific) is performed on multiple threads. Like
|
|
non-whole-module compilation, the locations of the object files is controlled
|
|
by the output file map. Only one file is produced for each non-object output,
|
|
however. The location of these additional outputs is controlled by command-line
|
|
options, except for Make-style dependencies and serialized diagnostics, which
|
|
use an entry under `""` in the output file map.
|
|
|
|
Threaded mode is controlled by the `-num-threads` command-line option rather
|
|
than `-j` used to control the number of jobs (simultaneous subprocesses spawned
|
|
by the driver). Why? While changing the number of jobs should never affect the
|
|
final product, using threaded vs. non-threaded compilation does.
|
|
|
|
Specifics of whole-module optimization mode are subject to change, particularly
|
|
in becoming more like non-whole-module builds.
|
|
|
|
|
|
## Incremental Builds ##
|
|
|
|
Incremental builds in Swift work primarily by cross-file dependency
|
|
analysis, described in [DependencyAnalysis.md](DependencyAnalysis.md).
|
|
Compiling a single file might be necessary because that file has changed, but
|
|
it could also be because that file depends on something else that might have
|
|
changed. From a build system perspective, the files in a particular module
|
|
can't be extracted from each other; a top-level invocation of the compiler will
|
|
result in a valid compilation of the entire module, but manually recompiling
|
|
certain files is not guaranteed to do anything sensible.
|
|
|
|
Performing an incremental build is easy; just pass `-incremental` and be sure to
|
|
put "swift-dependencies" entries in your output file map.
|
|
|
|
Incremental builds don't mix with whole-module builds, which get their
|
|
whole-module-ness by rebuilding everything every time *just in case* there's
|
|
some extra optimizations that can be made by repeated inlining, or by *really*
|
|
being sure that a method is never overridden.
|
|
|
|
(Can this sort of information be cached? Of course. But there's a question of
|
|
whether it's going to be easier to make whole-module builds be more incremental
|
|
or to make incremental builds support more cross-file optimization.)
|
|
|
|
A particular nasty bit about incremental builds: because in general the
|
|
compiler only knows that a file *has* changed and not *what* changed, it's
|
|
possible that the driver will start off assuming that some file won't need to
|
|
be recompiled, and then discover later on that it should be. This means that
|
|
it's not possible to use classic build models like Makefiles to make builds
|
|
incremental, because Makefiles don't accommodate dependency graph changes during
|
|
the build.
|
|
|
|
Only the "frontend" tasks are considered skippable; module-merging and linking
|
|
always occurs. (It is assumed that if *none* of the inputs changed, the
|
|
build system wouldn't have invoked the compiler at all.)
|
|
|
|
Currently Swift does not provide any way to report tasks to be compiled out to
|
|
some other build system. This is definitely something we're interested in,
|
|
though---it would enable better integration between the compiler and llbuild
|
|
(the build system used by the package manager), which would lead to faster
|
|
compile times when many dependencies were involved.
|
|
|
|
The format of the "swiftdeps" files used to track cross-file dependencies
|
|
should be considered fragile and opaque; it is not guaranteed to remain stable
|
|
across compiler versions.
|
|
|
|
|
|
## `-embed-bitcode` ##
|
|
|
|
Apple's "Embed Bitcode" feature adds an extra bit of complexity: to be sure
|
|
builds from the embedded bitcode are identical to those built locally, the
|
|
driver actually splits compilation in two. The first half of compilation
|
|
produces LLVM IR, then the driver runs so-called _backend jobs,_ which compile
|
|
the IR the rest of the way into binaries.
|
|
|
|
There's not really any secrets here, since it's all there in the source, but
|
|
the particular mechanism isn't guaranteed by any means---Apple could decide to
|
|
change it whenever they want. It's probably best not to special-case any logic
|
|
here; just using the driver as intended should work fine.
|
|
|
|
|
|
## So, how should I work Swift into my build system? ##
|
|
|
|
Step 0 is to see if you can use the Swift package manager instead. If so, it's
|
|
mostly just `swift build`. But if you're reading this document you're probably
|
|
past that, so:
|
|
|
|
1. Generate an output file map that contains all the per-file outputs you care
|
|
about. Most likely this is just the object files and incremental build
|
|
dependency files; everything else is an intermediate. (There should probably
|
|
be a tool that does this, perhaps built on what the package manager does.)
|
|
|
|
2. Set TMPDIR to somewhere you don't mind uninteresting intermediate files
|
|
going.
|
|
|
|
3. Do one of the following:
|
|
|
|
- Invoke `swiftc -emit-executable` or `swiftc -emit-library`. Pass all the
|
|
Swift files, even if you're building incrementally (`-incremental`). Pass
|
|
`-j <N>` and/or `-num-threads <N>` if you have cores to spare. Pass
|
|
`-emit-module-path <path>` if you're building a library that you need to
|
|
import later.
|
|
|
|
If you want debugging that's more than `-gline-tables-only`, this is the
|
|
only supported way to do it today [swiftlang/llvm-project#4588](https://github.com/swiftlang/llvm-project/issues/4588)
|
|
and [#45265](https://github.com/swiftlang/swift/issues/45265) are aimed at
|
|
improving on this). On the plus side, this mode doesn't strictly need an
|
|
output file map if you give up incremental builds.
|
|
|
|
- Invoke `swiftc -c`, then pass the resulting object files to your linker.
|
|
All the same options from above apply, but you'll have to manually deal
|
|
with the work the compiler would have done automatically for you.
|
|
|
|
Whatever you do, do *not* invoke the frontend directly.
|
|
|
|
|
|
### Questions ###
|
|
|
|
_Can I link all the object files together in the same binary, even if they came
|
|
from multiple modules?_
|
|
|
|
This is not currently supported, and debugging probably won't work (see
|
|
[swiftlang/llvm-project#4588](https://github.com/swiftlang/llvm-project/issues/4588) and
|
|
[#45265](https://github.com/swiftlang/swift/issues/45265) for more details).
|
|
However, if you are using `-gnone` or `-gline-tables-only`, the worst you will
|
|
suffer is more symbols being visible than are strictly necessary.
|