Build an iPhone app from the command line
If you, like me, are an old-time Unix command-line fanatic now doing iOS development, you've probably wondered if you can build an iPhone app from scratch, entirely outside of XCode. After all, Mac OS/X is a *nix, and all the familiar tools — Make, cc, ld — are all there. So, can you build and compile completely outside of XCode? As it turns out, yes, you can, and there are actually some speed advantages to doing so.
Now, before I continue — if you're looking to incorporate an automated
build system like Cruise Control or Hudson into your development process in
support of a development effort that normally works with XCode, you're
probably far better off looking into the command-line tool xcodebuild
that ships as part of XCode. However, if you just want to throw together
a POC really quick, and you know your Objective-C syntax well enough that
you don't really need the overhead of the XCode IDE (which can be quite a bit,
in spite of Apple's impressive efforts to keep it manageable), you can compile
and simulate an iPhone app using only command-line tools like we used to back
in the day when men were men, women were women, and a compiler and a text
editor was all we needed.
XCode is pretty good about telling you what it's doing for you behind the scenes, as long as you know where to look. For example, you can see all of the build parameters that were run as part of a build by opening up the Log Navigator (Command+7) and clicking one of the "build" logs as shown in figure 1.
As you can see, there's a lot of stuff going on in there, and as it turns out, it's not all stuff you necessarily need.
The smallest iPhone app I can think of that (arguably) does something "useful" is shown in listing 1 below.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface MyDelegate : UIResponder< UIApplicationDelegate >
@end
@implementation MyDelegate
- ( BOOL ) application: ( UIApplication * ) application
didFinishLaunchingWithOptions: ( NSDictionary * ) launchOptions {
UIWindow *window = [ [ [ UIWindow alloc ] initWithFrame:
[ [ UIScreen mainScreen ] bounds ] ] autorelease ];
window.backgroundColor = [ UIColor whiteColor ];
UILabel *label = [ [ UILabel alloc ] init ];
label.text = @"Hello, World!";
label.center = CGPointMake( 100, 100 );
[ label sizeToFit ];
[ window addSubview: label ];
[ window makeKeyAndVisible ];
[ label release ];
return YES;
}
@end
int main( int argc, char *argv[ ] )
{
UIApplicationMain( argc, argv, nil, NSStringFromClass( [ MyDelegate class ] ) );
}
This "app" opens a window and displays "Hello, World" as illustrated in
Figure 2 (it also leaks its UIWindow
instance because I didn't
create an autorelease pool. I hope your computer has enough memory to
compensate for this).
If you save this as, say window.m
, you can compile and launch
it (in the simulator) without ever starting up XCode.
You must have XCode installed — the simulator isn't distributed independently of XCode and even the cross-compile libraries (which I'll detail further below) are part of the XCode bundle. However, you don't have to have it running to compile and test an app.
LLVM
, or Low Level Virtual Machine, is
Apple's preferred Objective-C compiler these days. If
you've been doing iOS programming for a while, you probably know that it used
to all be done in gcc
(and can still be done that way, if you're so inclined), but the
default is LLVM (clang
on the command line), so that's what I'll use here. clang
is invoked from the command line just like gcc
— you give
it an input file and an output file. I'll go ahead and create a Makefile
for this process; the first iteration is shown in listing 2.
window: window.m
clang -o window window.m
This fails (and honestly, you probably expected that) with:
clang -o window window.m
window.m:1:9: fatal error: 'UIKit/UIKit.h' file not found
#import
If you have any experience with C programming, you can immediately see
the problem here; I didn't give the compiler a -I
flag pointing
to this header file. Interestingly, though, this minimal makefile
didn't error on my import
of <Foundation/Foundation.h>
on line 1 — yet I didn't tell
it where to find this include file. In fact, it would have
compiled without complaining if I had given it an input that didn't require UIKit. For instance,
listing 3 will compile just fine.
#import <Foundation/NSString.h>
int main( int argc, char *argv[ ] )
{
NSString *s1 = @"abc";
NSString *s2 = @"123";
NSLog( @"%@", [ s1 stringByAppendingString: s2 ] );
}
minimal.m
won't link, however, because although the header
<Foundation/NSString.h>
is found, the Foundation
library itself isn't. To include it, you have to specify the framework:
clang -o minimal minimal.m -framework Foundation
From a coding perspective,
-framework
works a lot like the -l
flag in gcc
.
But — where did clang
find
NSString.h
? It's not in /usr/include
, or
/usr/local/include
— in fact, you'll search in vain for
a file anywhere on your system named "NSString.h" that's contained in a
directory named "Foundation". However, if you do look for "NSString.h",
you'll find a copy in /System/Library/Frameworks/Foundation.framework/Headers
.
But, by the strict "rules" of C compilation, that can't be where clang
resolved it, because it's not in a directory named "Foundation".
This is another Objective-C extension to clang
. In addition to
regular header include searches (like /usr/include
), there's the
notion of Framework searches. By default, clang
will
look under /System/Library/Framework for frameworks to include.
Any such framework, if it has a Headers
directory, will be
resolved as Framework Name/header file. An ordinary iOS
developer would not need to know or care about such low-level details, but
when you're working so close to the system core, it does matter.
So what are these "frameworks"? Since NeXT's NeXTSTEP operating system and
it's derivatives (Mac OS/X and GNUStep) are the only users of Objective C,
it's sort of difficult to tease apart which parts of
clang
are "pure" Objective C and which parts are Mac OS/X
extensions. I believe, however, that this concept of a "framework" is a native
part of objective C. Essentially, a framework is a bundling of libraries
and their headers and associated resources with built-in support for versioning.
Anybody who's spent much time compiling and maintaining C-based packages on
a Unix system can see how useful this is; the Linux community has been trying
to standardize on something like this with pkginfo and RPM's for quite a while.
clang
has a -l
flag that will link ordinary
C libraries, but you'll likely never use this for iOS development.
So, what about UIKit
? Well, it's not there under
/System/Library/Frameworks
. That's because it's not part of
Mac OS/X; it's part of iOS. So how can you link to it? This is where the
simulator starts to come into play. XCode installs the iPhone simulator and
a set of cross-compile libraries for you to build against. XCode has a
tendency to move things around from one version to the next, so you may have
to hunt around to find the "core" directory, but as of XCode 4.6.1, the
cross-compile libraries are found under:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk/
Underneath this directory is another /System/Library/Framework
,
this time with a set of the Frameworks that iOS includes. (In fact, this
directory serves as the root directory of an entire iOS "installation"). So,
to get clang to compile an app that depends on UIKit
, you have
to add this directory to the framework search directory. You do this via
the OS/X-specific "-F" flag to clang. So, the updated makefile looks like
listing 4:
XCODE_BASE=/Applications/Xcode.app/Contents
SIMULATOR_BASE=$(XCODE_BASE)/Developer/Platforms/iPhoneSimulator.platform
FRAMEWORKS=$(SIMULATOR_BASE)/Developer/SDKs/iPhoneSimulator6.1.sdk/System/Library/Frameworks/
window: window.m
clang -F$(FRAMEWORKS) -o window window.m -framework Foundation -framework UIKit
The results are a little better, but this fails with:
UIKit has a dependency on a framework called
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/
iPhoneSimulator6.1.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CFBase.h:146:14: fatal error:
'MacTypes.h' file not found
#include
Core
. This framework is
part of Mac OS/X, so clang
finds it and tries to use it, but
UIKit
relies on a different version of Core
than
the one that Mac OS/X uses. The problem here is that
clang
is trying to resolve its frameworks from the simulator
directory, but it's still finding headers under /usr/include. The Unix
C programmer's solution, of course, is to add a "-I" as shown in listing 5.
XCODE_BASE=/Applications/Xcode.app/Contents
SIMULATOR_BASE=$(XCODE_BASE)/Developer/Platforms/iPhoneSimulator.platform
FRAMEWORKS=$(SIMULATOR_BASE)/Developer/SDKs/iPhoneSimulator6.1.sdk/System/Library/Frameworks/
INCLUDES=$(SIMULATOR_BASE)/Developer/SDKs/iPhoneSimulator6.1.sdk/usr/include
window: window.m
clang
-I$(INCLUDES) \
-F$(FRAMEWORKS) \
-o window window.m -framework Foundation -framework UIKit
This gets us further, but the compiler now emits a lot of errors along the
lines of:
Again,
'UIScrollView' is unavailable: not available on Mac OS X
clang
is trying to generate a Mac OS/X executable when
what we want is an iOS executable. The solution is to add:
In fact, with
-mios-simulator-version-min=6.1 \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk
isysroot
, you can get rid of the -F
and -I
flags; this replaces the "/" directory as it pertains
to the compiler. Now, the compile completes, but the link step fails due to:
This is because, although I've requested some cross-compile options, I still
haven't actually asked for a cross-compile. I have to do that with:
Undefined symbols for architecture x86_64:
Almost there. This fails for me with a handful of "Undefined symbols for
architecture i386". The reason is because I'm running on a 64-bit machine;
to work around this, I have to add
-arch i386
And voila! I get the executable file
-fobjc-abi-version=2
window
. The completed
make file is shown in listing 6.
XCODE_BASE=/Applications/Xcode.app/Contents
SIMULATOR_BASE=$(XCODE_BASE)/Developer/Platforms/iPhoneSimulator.platform
FRAMEWORKS=$(SIMULATOR_BASE)/Developer/SDKs/iPhoneSimulator6.1.sdk/System/Library/Frameworks/
INCLUDES=$(SIMULATOR_BASE)/Developer/SDKs/iPhoneSimulator6.1.sdk/usr/include
window: window.m
clang
-arch i386 \
-mios-simulator-version-min=6.1 \
-fobjc-abi-version=2 \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk \
-o window window.m -framework Foundation -framework UIKit
These are the fewest command-line parameters you can pass into clang
and still get a working iOS executable.
Now, how about running it? If I try to invoke it directly from the command
line, it predictably fails with:
Which makes sense - I compiled something for iOS, but now I'm trying to
run it on a OS/X. As you undoubtedly know, XCode includes an iPhone emulator,
and you can invoke it from the command line and pass in an app to simulate
with the
dyld: Library not loaded: /System/Library/Frameworks/UIKit.framework/UIKit
Referenced from: /Users/joshuadavies/devl/test/objc/./window
Reason: image not found
Trace/BPT trap: 5
-SimulateApplication
parameter. You can invoke it
from the command line like this:
(Notice the "./" in front of the application name; if you omit this, the simulator
won't find your executable).
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone\ \
Simulator.app/Contents/MacOS/iPhone\ Simulator \ -SimulateApplication ./window
So there you have it — without XCode running or open, I've compiled
and tested a complete, working iOS app.
Note that this whole process does not build an actual iPhone bundle.
iPhone runs under an arm
architecture, whereas this build, although
it was a cross-compile, still runs on an intel
processor. To
build for an actual deployment to an iphone, you'd have to change the
architecture to arm
, and to get it to install on a real device,
you'd also have to deal with code signing. Still, it's interesting and fun
to see how far you can go without actually running XCode.
Add a comment:
Ah, I'm so sorry Anantha, I omitted a backslash from listing 6. It should read:
window: window.m clang \ -arch i386 \ -mios-simulator-version-min=6.1 \ -fobjc-abi-version=2 \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk \ -o window window.m -framework Foundation -framework UIKit
In fact, the backslashes in the code listing were there strictly for formatting reasons. So, you can jam all of this onto a single very long line; to simplify (but slightly uglify):
window: window.m
clang -arch i386 -mios-simulator-version-min=6.1 -fobjc-abi-version=2 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk -o window window.m -framework Foundation -framework UIKit
The line break after "window.m" (as well as a hard-tab character) are required by the Make utility.
Great article.
As I'm sure you're aware, some things have changed since 4 - however I'm sure most readers of the page would be able to fill in the holes.
I personally love to build projects this way. However, I can't seem to find a way to easily debug an iOS app (or any other executable) on OS X without an Xcode project.
Would be nice to be able to keep the build external from Xcode, but still leverage Xcode's debugging, and launching to external devices.
Thoughts?
- Zach
xcrun simctl spawn booted ./window
Great blog! Did you know you share the same name as a teen murderer?