rchook
In reponse to a Product->Archive in Xcode, automatically:
- Verify all git working directories are clean
- Bump build number to next even number
- Create .xcarchive file
- Do a
git commit
/git tag
of main project and all sub-projects - Bump build number to next odd number, do another
git commit
- Copy .xcarchive and all source of all projects to external archive directory
Why?
I submitted the wrong binary to Apple for Theory Lessons 1.2. A week later, it was approved. This completely broke landscape orientation on iPad. After pounding my forehead into the table a few times (and resubmitting the correct binary), I decided to look at my submission process and see if I could prevent this disaster from reoccuring.
Major issues with my former submission process:
- Xcode doesn't show the build number (CFBundleVersion) of archives, only the marketing version
- Even if Xcode showed the build number, my correct binary and my faulty binary had the same build number
- I didn't realize that I could re-sign binaries (change from App Store to Ad-hoc). My testing process involved making an Ad-hoc build for TestFlight testing, and another binary for App Store distribution. Talking with fellow developers, this appears to be a common misconception
- I previously used
agvtool
andapple-generic
versioning to manage my build number. At some point from Xcode 4.3 -> 4.5,agvtool
stopped bumping my Info.plist files.
Needed Features
After much coffee and pondering, I came up with the following feature set for my new build system:
- There should be a one-to-one relationship between Xcode archives and build numbers. Each successfully built archive should have its own unique build number.
- The same archive/build number should be submitted to both TestFlight and the App Store.
- Development builds should never have the same build number as release. For my purposes, it's ok if different development builds share the same build number, as those builds only go on my devices.
- git tagging of release builds should be handled automatically
- A copy of all source code used to build the product, as well as the resulting xcarchive, should be stashed in a separate git repository (which is backed up to multiple servers).
- Xcode's Organizer's Archives window needs to show the build number, somehow.
- Everything should magically occur from Product->Archive in Xcode. If anything goes wrong, Xcode stops the archive process and no .xcarchive is produced.
Project Hierarchy
Both Tenuto and Theory Lessons have one main Xcode project, with multiple sub-project dependencies:
Tenuto.xcodeproj (product=Tenuto.app)
IXCore.xcodeproj (product=libIXCore.a)
MTCore.xcodeproj (product=libMTCore.a)
TheoryLessons.xcodeproj (product=TheoryLessons.app)
IXCore.xcodeproj (product=libIXCore.a)
MTCore.xcodeproj (product=libMTCore.a)
SwiffCore.xcodeproj (product=libSwiffCore.a)
Each project is in its own git repository. When I build Tenuto with CFBundleVersion=100, I want to tag all three repositories with "Tenuto-100" (see Feature #4 from above)
Unfortunately, having a project hierarchy like this adds complexity to any build scripts.
Xcode Scripts
Xcode has two ways of executing scripts: "Run Script" Build Phases and Scheme Actions. My first attempt used Build Phases exclusively. Unfortuately, the earliest you can run a "Run Script" Build Phase is after the internal CopyPlist phase, which is too late for modifying the Info.plist file (Feature #1).
My second attempt used Scheme Actions exclusively. Unfortuately, there is no way to stop the build process
during a Scheme Action (exit 1
doesn't work).
My third attempt used pre and post actions on the Archive Scheme of Tenuto, and Run Script Build Phases on each target. Unfortuately, the Run Script Build Phases were running before the Archive pre-script.
It looks like the internal ordering of build phases and scheme actions for one of my projects (Tenuto) looks something like this:
Tenuto - Build Scheme pre-Action
IXCore - Build Scheme pre-Action
IXCore - Run Script Build Phase
IXCore - Build Scheme post-Action
MTCore - Build Scheme pre-Action
MTCore - Run Script Build Phase
MTCore - Build Scheme post-Action
Tenuto - Run Script Build Phase
Tenuto - Build Scheme post-Action
Tenuto - Archive Scheme pre-Action
Tenuto - Archive Scheme post-Action
Hence, the very first opportunity to run a script is the Build Scheme pre-action. The last opportunity to run a script is the Archive Scheme post-action. Each project can be cancelled during a Run Script Build Phase.
The configuration for all of this looks like:
Edit Scheme window for main project (Tenuto):
Build Phases window for both main project and sub-projects:
The Actual Script (rchook)
Upon hitting Product->Archive in Xcode
- Tenuto Build scheme starts,
rchook
is called withxcode-app-build-pre-action
argument - Creates a fresh
/tmp/rchook
directory - Modifies the main project's Info.plist to prepare for an archive release. Archives are given even build numbers.
- Creates a backup copy of the original Info.plist
- Creates an Info.plist with the next development build number (odd).
- Creates
/tmp/rchook/cleanup.sh
, which is called to revert everything if we abort - For each project (Tenuto, IXCore, MTCore),
rchook
is called with thexcode-build-phase
argument - Ensures that the git working directory is clean. If not, exits out and causes Xcode to abort the archiving process. This is needed since we will be commit to the git repository at the end of the archive process.
- Concatenates current
$PROJECT_DIR
into/tmp/project_dirs
- Tenuto Build scheme finishes
- Tenuto Archive scheme starts
- .xcarchive producted
- Tenuto Archive scheme finishes,
rchook
is called with thexcode-app-build-post-action
argument - Iterate over each project directory in
/tmp/project_dirs
. Change the working directory andgit commit
andgit tag
- Performs some trickery to set the comment of Xcode's archive to the build number
- Create a new directory on the file system, copies .xcarchive file, as well as source files for each project to the directory
- Copy the new Info.plist created in Step 1.4 to the main project's directory
- Commit the new Info.plist using
git commit
Options to xcode-app-build-post-action
The xcode-app-build-post-action
command supports these additional options.
-s Resign the .app using the provided identity
-z Create a zip file of the .app
-u Upload the zip file (via -z) using scp
For example, to sign, zip, and upload a Mac OS X app to a web server for beta testing:
rchook xcode-app-build-post-action \
-s "Developer ID Application: John Appleseed (0AAA00AA00A)" \
-z "$HOME/Desktop/TheApp" \
-u "[email protected]:/path/to/www/betas/TheApp"
Resources
These resources were helpful when developing rchook: http://stackoverflow.com/questions/9855955/xcode-increment-build-number-only-during-archive