How to build an iOS app archive via command line

In my previous post, I detailed “How to export an Ad Hoc iOS ipa using Xcode” however there are advantages to exporting an iOS app archive using the command line. Top of mind reasons include:

  1. faster than using Xcode with a mouse
  2. can automate the build process (e.g. with GitHub Actions)

Make sure you followed along in the previous post so all prerequisites are met or have an active iOS app that you’ve successfully built and exported at least once. Also, shout out to Tarik Dahic as much of this blog is based on his excellent 2021 blog “Build iOS apps from the command line using xcodebuild”.

Note: you need to be enrolled in the Apple Developer Program (vs. the free Apple ID account) to create an Ad Hoc distribution directly from Xcode (there is a command line work around but your app won’t be properly signed). You can still use the free Apple ID account with Xcode, the iOS Simulator and to deploy the app to an Apple device physically connected to your computer.

1. Code setup

Make sure you have the iOS app source code on your computer:

Fork and clone the repo

$ mkdir -p ~/spfexpert; cd $_
$ git clone git@github.com:ahoog42/iamgroot.git
$ cd iamgroot

and change the build identifier.

2. Env setup

Next, assuming Xcode is installed, we will then install the Xcode Command Line Tools.

Install Xcode Command Line Tools

While you can install Xcode Command Line Tools from XCode, I find it easier to just kick off the install command line:

$ xcode-select --install

which will then prompt you to install the tools: install-cmd-line-tools.png

If you already have the command line tools installed, you get this error message (which you can ignore):

xcode-select: error: command line tools are already installed, use "Software Update" to install updates

3. Build the iOS app command line

To build an iOS app via the command line, there are a few steps:

  1. List the iOS app build information
  2. Optional - Remove previous build artifacts
  3. Build iOS app command line
  4. Archive iOS app command line
  5. Export iOS app archive command line

List the iOS app build information

For an iOS project, you can run the following command (if the app is more complex and uses workspaces, simple swap out -project for -workspace <project>.xcworkspace):

$ xcodebuild -list -project I\ am\ Groot.xcodeproj

which will list of key information you need to choose the correct app configuration to build:

Command line invocation:
    /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list -project "I am Groot.xcodeproj"

User defaults from command line:
    IDEPackageSupportUseBuiltinSCM = YES

Information about project "I am Groot":
    Targets:
        I am Groot

    Build Configurations:
        Debug
        Release

    If no build configuration is specified and -scheme is not passed then "Release" is used.

    Schemes:
        I am Groot

For a simple “hello world” app, we can pretty much get away with just the defaults but as you work on more complex project, you will have options regarding the targets, configurations and schemes you want to build.

Optional - Remove previous build artifacts

If you want to ensure a clean slate, you can optionally choose to clean up previous build artifacts with this one liner:

$ xcodebuild clean

and you’ll see something along the lines of:

Command line invocation:
    /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild clean

User defaults from command line:
    IDEPackageSupportUseBuiltinSCM = YES


** CLEAN SUCCEEDED **

Build iOS app command line

Next, we’ll build the iOS app with the following command:

$ xcodebuild build -scheme "I am Groot" -sdk iphoneos -destination generic/platform=iOS

which will generate significant output to the terminal (truncated below):

Command line invocation:
    /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild build -scheme "I am Groot" -sdk iphoneos -destination generic/platform=iOS

User defaults from command line:
    IDEPackageSupportUseBuiltinSCM = YES

Build settings from command line:
    SDKROOT = iphoneos16.2

Prepare packages

Computing target dependency graph and provisioning inputs

Create build description
Build description signature: acd074eace0504e5cf9c89f62ea1af42
Build description path: /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Intermediates.noindex/XCBuildData/acd074eace0504e5cf9c89f62ea1af42-desc.xcbuild

note: Building targets in dependency order
CreateBuildDirectory /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Products/Debug-iphoneos
    cd /Users/hiro/spfexpert/iamgroot/I\ am\ Groot.xcodeproj
    builtin-create-build-directory /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Products/Debug-iphoneos

<snip> 

Touch /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Products/Debug-iphoneos/I\ am\ Groot.app (in target 'I am Groot' from project 'I am Groot')
    cd /Users/hiro/spfexpert/iamgroot
    /usr/bin/touch -c /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Products/Debug-iphoneos/I\ am\ Groot.app

** BUILD SUCCEEDED **

As you can see, the iOS app build process will create files in the DerivedData folder managed by Xcode (~/Library/Developer/Xcode/DerivedData/).

Archive iOS app command line

Then we’ll create the iOS app archive which is an intermediary bundle that includes your app binary and symbols for debugging and crash reporting. To do this, issue the following command:

$ xcodebuild archive -scheme "I am Groot" -sdk iphoneos -destination generic/platform=iOS -archivePath ./build/iamgroot.xcarchive

which will create a directory in the current directory called build/iamgroot.xcarchive and like before, will generate verbose output:

Command line invocation:
   /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild archive -scheme "I am Groot" -sdk iphoneos -destination generic/platform=iOS -archivePath ./build/iamgroot.xcarchive

User defaults from command line:
   IDEArchivePathOverride = /Users/hiro/spfexpert/iamgroot/build/iamgroot.xcarchive
   IDEPackageSupportUseBuiltinSCM = YES

Build settings from command line:
   SDKROOT = iphoneos16.2

Prepare packages

Computing target dependency graph and provisioning inputs

Create build description
Build description signature: 2e45f1ed16bb0b981d471b57cb445da6
Build description path: /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Intermediates.noindex/ArchiveIntermediates/I am Groot/IntermediateBuildFilesPath/XCBuildData/2e45f1ed16bb0b981d471b57cb445da6-desc.xcbuild

note: Building targets in dependency order

<snip>

Touch /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Intermediates.noindex/ArchiveIntermediates/I\ am\ Groot/InstallationBuildProductsLocation/Applications/I\ am\ Groot.app (in target 'I am Groot' from project 'I am Groot')
   cd /Users/hiro/spfexpert/iamgroot
   /usr/bin/touch -c /Users/hiro/Library/Developer/Xcode/DerivedData/I_am_Groot-gfxpkzbpazztxzabhewsezcfrozd/Build/Intermediates.noindex/ArchiveIntermediates/I\ am\ Groot/InstallationBuildProductsLocation/Applications/I\ am\ Groot.app

** ARCHIVE SUCCEEDED **

Export iOS app archive command line

Finally, you can create the iOS app .ipa file but first you’ll need an ExportOptions.plist file. In my previous post, this file is actually created by Xcode when you manually export an iOS app archive with “Automatically Manage Signing” enabled.

So instead of creating this file by hand, you could simply locate the plist file in the directory where you saved the iOS app archive. Alternatively, here’s what a basic file looks like and if you update the teamID with your team, you should be able to use this file as is:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>compileBitcode</key>
	<false/>
	<key>method</key>
	<string>development</string>
	<key>signingStyle</key>
	<string>automatic</string>
	<key>stripSwiftSymbols</key>
	<true/>
	<key>teamID</key>
	<string>YOUR-TEAM-ID-HERE</string>
	<key>thinning</key>
	<string>&lt;none&gt;</string>
</dict>
</plist>

You then simply provide the exportOptions.plist file as a parameter to export the iOS app archive:

$ xcodebuild -exportArchive -archivePath ./build/iamgroot.xcarchive -exportOptionsPlist exportOptions.plist -exportPath ./build

This will output status as follows:

2023-02-14 16:45:46.227 xcodebuild[24792:218357] [MT] IDEDistribution: -[IDEDistributionLogging _createLoggingBundleAtPath:]: Created bundle at path "/var/folders/s0/ps95pmts5_93qf5ms7zv9rz00000gn/T/I am Groot_2023-02-14_16-45-46.226.xcdistributionlogs".
Exported I am Groot to: /Users/hiro/spfexpert/iamgroot/build
** EXPORT SUCCEEDED **

The resulting .ipa file will be in the ./build directory:

$ ls -ltrh build
total 8112
drwxr-xr-x  5 hiro  staff   160B Feb 14 16:44 iamgroot.xcarchive
-rw-r--r--  1 hiro  staff   3.9M Feb 14 16:45 I am Groot.ipa
-rw-r--r--  1 hiro  staff    16K Feb 14 16:45 Packaging.log
-rw-r--r--  1 hiro  staff   466B Feb 14 16:45 ExportOptions.plist
-rw-r--r--  1 hiro  staff   1.4K Feb 14 16:45 DistributionSummary.plist

as well as various plist and log files. You can then use the .ipa file as need, for example uploading it to NowSecure Platform for automated mobile app security and privacy testing. :-)