Building iOS App without Xcode

Xcode isn’t just an IDE; it actually has an entire ecosystem with tools for iOS App development like SDKs, compilers, debuggers, instrumentation tools, test runners, and one of the most important, the Xcode Build System.

A build system executes and manages a series of processes that results in a ready-to-use binary like apps, libraries, frameworks, etc.

In this article, we will build an iOS App without using the Xcode Build System. Instead, we will “create” our own build system using one of the most primitive build automation tools: Make.

Make is a bash script execution tool installed natively in some Unix-based operating systems, including macOS. The make syntax is divided into two types: macros and rules.

You can go deeper on Make. There are some excellent tutorials available, but for our proposal, we’re going to focus on just com macros and rules; We can do a parallel with programming languages; we can see macros like variables and rules like functions.

Working with Make can be pretty straightforward, just create a text file called makefile inside your project root folder and start writing scripts without installing any other tool.

hello_world = "Hello World"
print:
    echo $(hello_world)

We created a macro called hello_world that holds a text value and the print rule that executes the echo command in this sample. Just save this file on an empty folder and run make print then it will output Hello World.

Now that you have the basics about Make let’s create our iOS app.

First of all:

  • Download the starter project. It contains a basic iOS app with swift code and an interface builder file (xib).
  • Install Xcode and Command Line Tools. Remember, the Xcode is not just an IDE. It also provides every tool for iOS app development, like SDKs, compilers, linkers, debuggers, etc.

Let’s get started.

iOS Application Bundle

In a nutshell, an iOS app is a folder with .app an extension with the following contents:

– A Mach-O executable binary

– An Info.plist file with some information about the application like exhibition name, bundle identifier, app icon location, and many others.

– Resources: images, string files, and interface files.

– Support files like frameworks, plugins, and dynamic libraries.

Creating app bundle

From now on, we’ll be going editing the makefile on the start project root folder, to test every change, just run make VeryCoolApp on terminal.

First, we’ll create a macro called APP_BUNDLE that holds the bundle name.

Inside VeryCoolApp rule, let’s add a mkdir to create the app bundle folder, but before that line, add a rm -rf command to clean the last build before every running

APP_BUNDLE = VeryCoolApp.app

VeryCoolApp:
	rm -rf $(APP_BUNDLE)
	mkdir $(APP_BUNDLE)

As we saw before, the application bundle needs a Info.plist file, at the start project, there’s a simple list file that doesn’t need any additional processing, so it’s ready to be copied to the app bundle. To do it, use cp to create a copy of Info.plist inside VeryCoolApp.app

APP_BUNDLE = VeryCoolApp.app

VeryCoolApp:
	rm -rf $(APP_BUNDLE)
	mkdir $(APP_BUNDLE)
	
	cp Info.plist $(APP_BUNDLE)/Info.plist

Until now, our build system creates a folder called VeryCoolApp.app with an Info.plist file

+-- VeryCoolApp.app/ 
|   +-- Info.plist

Compiling an executable

Following the Bundle Structures Apple article, an iOS Application bundle needs to have an executable binary, a MACH-O executable. This binary has the main entry point and (optionally) also includes the statically linked libraries (we’ll go deeper on this later).

We can build a MACH-O executable by calling the swift compiler with at least a .swift file as the main entry point.

If you input more than 1 swift file, the main entry point can be the file called main.swift or any other that contains a @main notation. See more about it at Top-Level Code.

First of all, let’s create a macro called swift_compiler that holds the path to the swift compiler.

At the VeryCoolApp function, right after the Info.plist copy, let’s call the compiler passing the app’s swift files followed by the -o flag that indicates the output file (our app’s executable). Inside the Info.plist file, a property CFBundleExecutable tells the operating system the .app executable name. So, to simplify, let’s call our executable by the value of that property, which is the same name as the make rule, use the $@ automatic variable to get the function name and pass it as swift compiler output.

The makefile will look like this:

APP_BUNDLE = VeryCoolApp.app
SWIFT_COMPILER = $(shell xcrun --find swiftc)

VeryCoolApp:
	rm -rf $(APP_BUNDLE)
	mkdir $(APP_BUNDLE)
	
	cp Info.plist $(APP_BUNDLE)/Info.plist

	$(SWIFT_COMPILER) *.swift \
	-o $(APP_BUNDLE)/$@

By running make VeryCoolApp we’ll get the following error:

.../usr/bin/swiftc *.swift
AppDelegate.swift:1:8: error: no such module 'UIKit'
import UIKit
       ^
make: *** [VeryCoolApp] Error 1

(If running on M1 this error may be different)

Our app contains an import of UIKit, a framework from iOS SDK. But we reference that SDK at any moment, so the compiler cannot find it. So let’s pass the -sdk flag to the swift compiler, giving the path to the iOS Simulator SDK path (if building for iOS Device, you need to pass the right SDK path).

It’s pretty simple to get the SDK path; just run xcrun -sdk iphonesimulator --show-sdk-path. So we can add this to the makefile like this:

APP_BUNDLE = VeryCoolApp.app
SWIFT_COMPILER = $(shell xcrun --find swiftc)
SDK = $(shell xcrun --sdk iphonesimulator --show-sdk-path)

VeryCoolApp:
	rm -rf $(APP_BUNDLE)
	mkdir $(APP_BUNDLE)
	
	cp Info.plist $(APP_BUNDLE)/Info.plist

	$(SWIFT_COMPILER) *.swift \
	-sdk $(SDK) \
	-o $(APP_BUNDLE)/$@

Running make and another error:

<unknown>:0: warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX'
<unknown>:0: error: unable to load standard library for target 'arm64-apple-macosx12.0'
make: *** [VeryCoolApp] Error 1

This is pretty straightforward to solve, we’re building an iOS Simulator SDK-based app, but as the build target isn’t set, the compiler takes the development platform (macOS) as the target. So, we just need to specify the target; the Swift compiler has a -target property that receives an LLVM triple value which is made of architecture-vendor-operating_system-environment (environment is optional).

APP_BUNDLE = VeryCoolApp.app
SWIFT_COMPILER = $(shell xcrun --find swiftc)
SDK = $(shell xcrun --sdk iphonesimulator --show-sdk-path)
TARGET = "arm64-apple-ios15.0-simulator"

VeryCoolApp:
	rm -rf $(APP_BUNDLE)
	mkdir $(APP_BUNDLE)
	
	cp Info.plist $(APP_BUNDLE)/Info.plist

	$(SWIFT_COMPILER) *.swift \
	-sdk $(SDK) \
	-target $(TARGET) \
	-o $(APP_BUNDLE)/$@

So, let’s run make VeryCoolApp , and voilà: We have an iOS App. Just drag and drop it to an iOS Simulator:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: