How To Make A Mac App Without Xcode

Home | Articles | Talks
—————————————————————————————————————————————

15 Oct 2018

Fonts, images, and other assets are also exported. In the upper right hand corner, click “More” 6. It might be achieved the way you talking about but think on this bit that if your app need any update or minor bug or changes then? Let’s see how to do it: Here we are! You send this link (and/or QR code) to the device owner (the UDID of the iOS device should be in the provisioning. If you want to get a project created as you did in XCode 7.3, without storyboard, but with a main menu as entry point. In XCode 8.3 (or 9 probably) - Create New MacOS project: it will be create with storyboard - In project navigator, delete Main.stroryBoard - in infos.plist, delete the mainStoryboard entry. This article is intended for educational purposes only, to demonstrate that it’s possible to test out iOS applications with Xcode without having to buy a Mac. However, you should consider testing on a real MacOS device before publishing your application on the App Sto.

A build system, despite its scary-sounding name, is just a regular program, which knows how to build other programs. As an iOS developer, you’re certainly familiar with how to build a project using Xcode. You go to the Product menu and select Build, or you use the ⌘B keyboard shortcut.

You may have also heard about Xcode Command Line Tools. It’s a set of tools which allows you to build Xcode projects directly from the terminal using the xcodebuild command. A very convenient thing for automating your processes, for example on your CI.

No matter how you’ve initiated it, the building itself is orchestrated by Xcode’s build system.

Can we replicate the building process and build the app “manually”, without Xcode’s build system?

Is it possible to sign the resulting app? Or even deploy it to an actual iOS device?

⚠️ Disclaimer 1 ⚠️

What is this post about:

Writing a non-reusable script that builds one concrete iOS project the simplest way possible.

What is this post NOT about:

Writing a complex and universal build system.

Content

The App We’re About to Build

I let Xcode 10.0 generate a new project using the Single View App template, and named it “ExampleApp”. This is going to be the reference app we will try to build “manually”. The only adjustment to the project I made was adding a UILabel with 🎉 to the main (and only) ViewController.

I also created the build.bash file in the root folder of the project. We’re going use this file for the actual build script.

Don’t forget to make the file executable by running the following in the terminal:

Step 1: Prepare Working Folders

⚠️ Disclaimer 2 ⚠️

The complete “recipe” of how the app should be built is contained in its xcodeproj file. This article is not about how to parse and retrieve this information from it.

We will ignore the project file for the sake of this article. To make our life easier, we will hard-code all the details like the project name, source files, or build settings, directly into the build script.

Let’s start with some housekeeping. We need to define and create a set of folders we will be using during the building process.

When you run the script, your terminal should like this:

How To Make A Mac App Without Xcode

Notice, that the script created two folders:

Step 2: Compile Swift Files

Did you know you can call the Swift compiler directly from the terminal using swiftc?

With the correct flags, compiling all .swift source files is quite a straightforward operation.

I’d like to talk a little bit about the -emit-executable flag. According to the documentation, when this flag is used, the compiler “emits a linked executable”. When you run swiftc in the verbose mode (-v), you can actually see, what’s happening under the hood. The compiler creates .o object for every .swift file, and then calls ld to link them into the final executable file.

Notice, that we set output (-o) to ExampleApp.app/ExampleApp. That’s the resulting executable file from the compilation process.

Run the script and you should see the following:

You can also check, that the resulting executable was created inside the ExampleApp.app bundle:

Step 3: Compile Storyboards

Source code files are not the only files which need to be compiled. We also need to compile all .storyboard files. The compiler for storyboards (and all the other interface builder files) is called ibtool. The resulting files from the process have the extension .storyboardc.

Fun fact

storyboardc files are file bundles. When you inspect the content of the bundle, you will see, that a compiled storyboard is actually just a bunch of nib files.

ibtool doesn’t accept more than one file at a time. We need to use a for loop to iterate over all storyboards.

Let’s run the script and verify the content of ExampleApp.app:

Step 4: Process and Copy Info.plist

A valid app bundle has to contain Info.plist. Unfortunately, we can’t simply copy the one from the project directory. This raw file contains several variables which we need to replace with the actual values.

To deal easily with plist files, Apple provides us with a tool called PlistBuddy. Because we don’t want to modify the original plist we first create a temporary copy in the _BuildTemp folder, and modify it there. Finally, we copy the processed plist to the app bundle.

Let’s run the script:

We should also verify that the processed plist file is there, and its content is correct:

Running ExampleApp in the Simulator

At this point, we should have a valid .app bundle. At least for running it in the iOS simulator. You can open the simulator directly from the terminal window:

To interact with iOS simulators, Apple created a tool named simctl. Here’s how you can install the ExampleApp app bundle to the currently running simulator. The second command starts the installed app.

How To Make A Mac App Without Xcode
I must admit I was extremely surprised when I saw the app was working!

The truth is, I thought one more step would be needed. Swift doesn’t have a stable binary interface yet and Swift runtime is not included in iOS. Thus every app has to have its own copy of the Swift runtime libraries included in the bundle.

We haven’t copied the runtime libraries into the bundle but the app works!

When compiling Swift files using swiftc, you can specify a dylib runtime search folder using -Xlinker -rpath flags. By doing so, you’re letting the linker know where to search for dynamic libraries, like the Swift-runtime ones. It turns out if you don’t specify them as I did, the search path defaults to:

This is exactly the place where the Swift runtime libraries are stored on your computer!

The iOS simulator is not sandboxed and has access to all your files. It means it can easily load the runtime libraries from any arbitrary place. I had no idea about this.

As long as you target the iOS simulator, you can create a valid, fully functional, Swift iOS app without including the Swift runtime libs in the bundle.

Building for Device

As you can see, building for Simulator is quite forgiving. We don’t need to include the runtime libraries in the bundle. We also don’t need to sign it and deal with provisioning.

Changing the build script so it can produce a valid app bundle, which can be executed on an iOS device, is another level of fun.

Firstly, we need to let the script know when to build for device.

Let’s use the --device flag to signal the script the desired target architecture. We update the code from step 1:

Secondly, we need to update the code from step 2:

Changes we made:

  • We check the value of BUILDING_FOR_DEVICE and set TARGET and SDK_PATH variables accordingly.
  • We introduced the OTHER_FLAGS variable. This variable is empty for simulator builds. For device builds, we pass to the compiler additional information about where to find dynamic libraries in the app bundle. For device builds, we will HAVE TO include Swift runtime in the app bundle.

Finally:

Because all the next steps will be needed only for device builds, if we build for simulator, we can exit the build script right after step 4.

Let’s add this to the end of the script:

How To Make A Mac App Without Xcode Password

Step 5: Copy Swift Runtime Libraries

I already mentioned, when building for device, we need to include the Swift runtime libraries in the app bundle. Fortunately, no magic is required here. We can just simply copy the libraries to the bundle.

Let’s run the script and verify the result. Don’t forget to use --device.

Step 6: Code Signing

I spent over 20 hours trying to figure out all the steps needed to successfully build the app for device. A half of this time I spent on this one step.

Before we start, here’s a nice picture of kittens for you.

6.1 Provisioning Profile

All your installed provisioning profiles are stored in ~/Library/MobileDevice/Provisioning Profiles/. The first challenge is to find the correct one to use for this app.

I’d strongly recommend to figure everything out in Xcode, first. If I’m not mistaken, with the free Apple dev account, you can’t even create a new provisioning profile using the web portal. You have to use Xcode for that.

Once you have the code signing working there, make a note of which provisioning profile and signing identity were used.

For the app bundle to be valid, it has to contain a file named embedded.mobileprovision in its root. Let’s copy the correct provisioning profile to the app bundle, and rename it:

6.2 Signing Entitlements

Another file needed to successfully sign the bundle is the .xcent file. This file is just another plist, created by merging project’s Entitlements file with additional signing information.

There’s no Entitlements file in our project, so this part of merging is done 👍.

The additional signing information, I was referring to, is the Apple dev team ID. This ID has to be the one from the signing identity you are about to use in the next step.

Let’s create the .xcent file and set the required values:

6.3 Signing

Now, when we have all the required pieces in place, we perform the actual signing. The tool which will help us with this is called codesign.

You need to specify which identity should be used for signing. You can list all available identities in the terminal using:

For the app bundle to be valid, we need to sign all dynamic libraries in the Frameworks folder, and then the bundle itself. We use the xcent file only for the final signing. Let’s finish this!

Celebrate!

The last part of the script we need to write is the final message:

You can now run the script, sit back, and enjoy your victory:

Installing to an iOS Device

Unfortunately, installing the app to a connected iOS device is not at all that straightforward as it was for the simulator. Luckily, there are 3rd party tools which make the task much more pleasant.

I’m using ios-deploy.

You can list all connected devices using -c and then copy the identifier of the device.

Finally, you install the app bundle to the device using:

How To Make A Mac App With Xcode

Next Steps

Here’s the GitHub repo with the final version of the script.

I learned a lot when writing this article. I don’t personally plan to work on this script anymore. However, if someone is interested in improving it, here are some ideas:

  • I didn’t need to touch *.xcassets files because there was no content inside. However, these files also need to be compiled.
  • It would be nice to get incremental builds working for Swift.
  • The script currently builds only Swift files. What about projects with Objective-C, C++, Objective-C++, C sources?
  • I don’t think it would make sense to parse xcodeproj and get the build parameters from it. What about getting the build settings from an XcodeGen file?
  • What about target dependencies (like custom embedded frameworks) that also need to be built?

Special thanks to Harlan Haskins for helping me with the article by answering my questions.

How To Make A Mac App Without Xcode Install

Do you see a typo or have a comment? PRs and issues appreciated!

Recent Posts

How To Make A Mac App Without Xcode Install