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
make print then it will output
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.
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
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
+-- 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)/$@
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: