Compile the Obj-C file, which imports the Swift-generated header file, to produce an object file

Link all the object files together

Today, we’re going to see what that looks like in practice by examining Xcode build logs.

Why look at Xcode build logs?

The last post was about how you’d build and link code without using Xcode to do the work for you. So why does this article go back to leaning on Xcode?

You’ll probably be doing most of your building using Xcode, so growing acquainted with how it does things will be more applicable to your everyday work than the more academic “If we didn’t have Xcode, what would we do?” question. (Yes, the Swift Package Manager exists as an alternative route to building Swift projects, but, no, we will not be talking about it today.)

Reading how Xcode does its work is a good way to reverse-engineer the build process so you can do it without using Xcode. More usefully, if you learn to see past the noise in the Xcode build logs, you learn to pull out the salient details needed to debug most any build-related issue.

A Minimal Obj-C iOS App

We’re going to start with looking at how a pure Obj-C project comes together.
Here’s the on-disk directory layout of a small iOS app named “ImportSwift”:

The rest of the files either describe the project and IDE state (everything under the
.xcodeproj),
the app (Info.plist), or get run through their own compilers to spit out assets used by the app (the storyboard files and the .xcassets Asset Catalog bundle).

Building an App

There’s more to building an app than just compiling and linking Obj-C files.
Let’s look at where those steps fit into the overall build flow before
digging in.

When it comes to understanding how Obj-C code manages to use Swift code,
we only care about the three steps in the middle of the whole shebang that
compile some C-ish stuff, compile some more C-ish stuff, and finally link it
all together to make an app:

/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ImportSwift.build/Debug-iphonesimulator/ImportSwift.build/Objects-normal/x86_64/AppDelegate.o: This is the product of this build step, a .o object file.

Translated from variable names into words, this says to stick the object file under the object file directory, in a folder specific to the build variant (you’ll probably only ever see “normal” here), in a subfolder specific to the target architecture. Neat and tidy!

ImportSwift/AppDelegate.m: Xcode is using the usual “name the parent folder of all the source file for an app named YourAppNameHere YourAppNameHere/” trick, so this is the source file in situ in our project directory.

normal: This is that $BUILD_VARIANT setting that came up as embedded in the build product output path.

x86_64: And here’s the $CURRENT_ARCH current architecture setting that came up in the same path, yet again. This one says to build for the i3/4/5/6/whatever-86 – x86 – architecture, only the 64-bit version.

x86_64 stands in contrast to i386, which is the 32-bit version, which isn’t flagged as 32-bit explicitly because why would you ever need more than 32 bits? (And wasn’t that 32- to 64-bit default integer size change a fun migration!)

If you play around with Linux, you might see the same architecture called amd64 rather than x86_64, because AMD beat Intel to market with a 64-bit variant of the i386 architecture.

Here, seeing x86_64 also tells you that we’re compiling this code for a simulator rather than the actual device, because all the iOS devices use one flavor of ARM architecture or another.

objective-c: This is the language we’re compiling using the C (slash Obj-C) compiler.

com.apple.compilers.llvm.clang.1_0.compiler: This happens to match the $DEFAULT_COMPILER build setting (and also, thanks to historical accident, path dependency, and the desire for backwards compatibility, the value for $GCC_VERSION, as well, even though Clang is very much not GCC!). It’s Ye Olde Reverse DNS Identifier, pointing at a version of clang. My current clang version actually self-identifies as part of clang --version as clang-800.0.42.1, so I’m not quite sure what they’re getting at with the 1_0, but the rest makes sense: “This is one of Apple’s compilers, part of the LLVM project, called clang, version 1.0, and it’s, uh, a compiler, as you might have guessed from that earlier ‘compilers’ bit, but let’s just make sure we’re on the same page, OK?”

Command Breakdown

Phew! Now we can actually start to dig into the tool invocations that implement this build step. Note that a lot of this is actually driven by settings in the Build Settings configuration; this project had no customization done in there, so we’ll be looking at the Xcode template defaults, which is most likely what any project you look at will be based on, as well.

Establish working directory
First, the build step establishes the working directory used for the rest of the invocations:

cd /Users/jeremy/Workpad/BNR/ImportSwift

This happens to be the $SRCROOT directory of the project, also known as the root directory holding all the target’s files.

onfigure locale
Then, it sets the language and encoding, in case some locale-aware helper gets clever:

export LANG=en_US.US-ASCII

Xcode is expecting to parse out warnings and errors to annotate your code by parsing literally “warning: filename:linenumber: some text” or “error: filename:linenumber: some text”. If some process invoked during compilation started dumping out “advertencia:” or whatever, Xcode would be a lot less helpful - though it would still be aware of whether the build succeeded or failed, since that depends on the human language independent process exit code convention, where exiting with 0 is A-OK, and anything else is bad news.

Ensure PATH includes platform-specific binaries
Then, it sets the PATH used to resolve a command name, like clang, to an actual path, like /usr/bin/clang:

Each directory in the colon-separated PATH list is searched in order from left to right for a match for the command name, and the first one wins. If no match is found, you get an “Unknown command” error in your shell. (Other stuff can go wrong, too, but “no such number, no such name” is the most common problem you’ll encounter.)

This allows Xcode to control what executable runs. Here’s the list reformatted to be a smidge more readable:

You can see it giving preference to the binaries inside its Platforms folder and its Developer folder, before following with the standard system binary locations. Notably, it does not include any of the paths you might have added on to your own PATH – no Fink /sw/ stuff, no MacPorts /opt/ stuff, and if you set up Homebrew somewhere other than /usr/local, you’re out of luck there, too.

Invoke the compiler
Next, we come to the moment we’ve been waiting for - the actual compiler invocation. And ain’t this a humdinger:

-fobjc-arc puts the compiler into the modern world by flipping on ARC.
You probably have run into this more frequently (if at all) as the version that turns ARC off, -fno-objc-arc, which some extant library code requires to compile successfully.

The ABI version specified is the modern, non-fragile Obj-C application binary interface. This has to do with what runtime functions the compiler can expect to be available and lets the compiler make a variety of other assumptions about how to generate Obj-C binary code that can interoperate with the runtime and other Obj-C binary code.

Confusion around what flavor of dispatch would be the default for macOS aside,
this is an iOS project, and the dispatch method is explicitly set to “legacy”,
so let’s move on!

-DOBJC_OLD_DISPATCH_PROTOTYPES=0 defines OBJC_OLD_DISPATCH_PROTOTYPES to
rewrite to 0, which is commonly used to represent a Boolean false.
This setting causes the header <objc/message.h> to expose the
core message-sending functions as () -> Void functions,
which forces the caller to cast them to an appropriate type
– as is required for ARC to work its magic –
rather than as varargs (Any, Selector, Any...) -> Any,
which invites trouble under ARC.

Configure some features

-fpascal-strings
-fno-common
-fasm-blocks
-fstrict-aliasing

-fpascal-strings enables support for Pascal strings.
Pascal used length-prefixed strings rather than NUL-terminated strings.
The Pascal-string support lets you write a Pascal-compatible string like
"\pRockin' it Pascal style" and have the compiler replace the \p with
the strlen of the string. It also turns the string literal into an explicit
character array literal, as you’ll find as you hammer on the variable
declaration.

-fmodules-cache-path tells the compiler where to cache module lookup results.
Xcode is building the cache in a folder named ModuleCache in a per-user
derived-data directory.

-fmodules-prune-interval sets a minimum bound on how long the compiler should go before trying to prune the module cache.
Xcode is setting it to 24 hours, which is a lot more aggressive than the compiler default of 7 days.

-fmodules-prune-after says that, if a module cache file hasn’t been accessed in the number of seconds specified, then it can be pruned.
Xcode is setting this to 4 days, rather than the compiler default of 31 days.

-fmodules-validate-once-per-build-session avoids repeatedly validating a
module during a single build session.

-fbuild-session-file names a file whose modification time is treated as the time when the build session started, which matters for some decision-making related to what-to-validate-when for modules.

-Wnon-modular-include-in-framework-module flips on everyone’s favorite warning
when using older frameworks, “warning: include of non-modular header inside
framework module…”. The -Werror flag following turns this into a hard
error, rather than an ignorable one.

Notice that the module cache is not project-specific, but is instead in a
shared location. If you take a peek in your module cache directory,
you’ll see something like:

A modules.timestamp file, which is empty, but seems to line up with the
Session.modulevalidation file in terms of its modification time.

A bunch of directories whose names are an all-caps mishmash of letters and
numbers. All the directory names are 13 uppercase alphanumerics characters.

Inside each directory is a bunch of files named like $(MODULE_NAME)-$(THIRTEEN_ALPHANUMERICS).pcm. Some of the pcm files might have a suffix of a hyphen and eight lowercase alphanumerics. Some of these might have an individual .pcm.timestamp corersponding or a .pcm.lock symbolic link. And some of the folders might have a modules.idx file.

At a high level, we can say that these are pre-compiled module files and related tracking info needed to safely use and invalidate the cached compilation outputs in the face of possible underlying module changes. Digging in deeper would be wandering into compiler-internal details that are part of clang’s module implementation.

This batch of options ensures that clang dumps as much info as it can about anything that goes wrong, and tells it to send the warnings directly to a file, rather than stderr.

Disable optimization, enable debug info

-O0
-g
-DDEBUG=1

This batch of options enables debug information generation (-g),
disables optimization (-O0) so that the compiler output more closely
matches the code input to it, which make stepping by line and setting
breakpoints by line less confusing.

This also defines DEBUG to be true so app code can adapt to being compiled
with debugging enabled using code like:

#if DEBUG
[self ensureInvariantsHaveBeenMaintained];
#endif

You might conditionally compile code based on DEBUG that is used to
perform time-consuming sanity checks on data structures
or other additional work that is useful during debugging
but is inappropriate to ship to end users.

-isysroot tells clang to pretend the provided directory is the root
of the filesystem, and look for system headers, libraries, and tools
relative to that folder rather than /. This is a quick shorthand to
rewrite the standard lookup paths all at once without needing a pile of
-I, -L, -F, etc. flags to do so piecemeal.

Recall that you can build and link against a base SDK of one version
while still producing a build product that works with an earlier version;
this is the distinction made by the base SDK vs the deployment target.
Most of the time, you’ll have only whatever the latest SDK is that your
Xcode shipped with,
and you’ll target back to whatever OS version you’re supporting back to
while building against that.

-isysroot does a lot of work for us in aiming the file lookup at the right
places, but there are still a lot of search paths specific to the app’s build
process, so that’s what this
clump of options rigs up.

There are three flavors of option used here:

-iquote adds a directory to the search path for headers included using
quotes. This means #include "header.h" will look in this directory, but
#include <header.h> won’t.

-I adds a directory to the overall include search path. Both the
quote and angle bracket flavors of include will look in this directory.

-F adds a directory to the framework search path, which means that
-framework Some.framework will now look in this directory for
Some.framework, before searching the rest of the search path.

This -F flag is used to include the built-products directory. This way,
if you’re building a framework alongside your app, you can easily
link against it.

The last couple -I flags add the derived sources directory and the
architecture-specific derived sources directory to the include file search
path. This lets you generate headers during a build step and have them
picked up by the compilation build step. You can also clobber a general
header with an architecture-specific one if necessary, because the
arch-specific directory will be checked before the more general one.

The several -iquote and -I flags that lead this batch of options off
supply the path to a header map
(.hmap) file rather than to a directory.

A header map is a binary file containing a hashtable.
You could replace it with a pile of symlink files,
but the header map is more compact and supports faster header path lookup.

The header maps are used to allow including various flavor of intermediate
build files, and they all live under the $TARGET_TEMP_DIR folder.
The ones that show up here are:

Generated files

Own-target headers

All-target headers

Project headers

ALL the headers (no suffix, just ImportSwift.hmap)

There are actually a few more, which you can find by looking at the
$CPP_HEADERMAP_FILE_FOR family of build settings.

If you poke around
in the header-map data structure, most of these are
actually empty in this case, because this is such a simple project.
The only ones with some content are the project headers and ALL the headers.
In fact, these both contain one and the same entry,
pointing at the sole header file in the project:
/Users/jeremy/Workpad/BNR/ImportSwift/ImportSwift/AppDelegate.h.

The uppercase -M family of arguments relates to writing out dependencies
between files. The original purpose of this was autogenerating the
“if this changes, then rebuild that thing” information for Make, hence the M
mnemonic.

-MF gives the path for the output Makefile.

-MT names the main target, which will be the first one listed in the
output Makefile, and is what a make without any arguments would try to build.

-MMD causes the compiler to dump dependencies alongside its other work,
rather than as its sole work, during the invocation. This flavor omits
emitting any dependencies on or of system headers.

This information can be used to drive incremental recompilation
as well as incremental reindexing for language-aware syntax highlighting
and code completion, though your guess is as good as mine as to what
Xcode actually does with it.