My own personal tools to perform various release-related tasks for iOS and macOS applications.
Think Fastlane, but much simpler (and consequently much less capable).
The main activities supported are:
- calculating a build number and injecting it whilst building or archiving
- exporting and uploading an archive to the App Store portal
- (or) notarising and building a Sparkle feed for an externally distributed macOS app.
Note: The notarisation/sparkle path is one I used it extensively for a while, but I haven't needed it recently. It may need a little bit of fixing up, so let me know if you encounter problems. It isn't likely to be hard to fix.
When publishing via the App Store Connect portal, the main command is:
rt submit
This builds an archive, exports it, and uploads it to the app store, using credentials you've previously configured.
The submit
command is the equivalent of performing this sequence of smaller commands:
rt archive
rt export
rt upload
The effect of these commands is:
- archive: build an archive
- export: export from output of
archive
- upload: upload the output of
export
to the app store connect portal
Normally you can just use the submit
command, but the individual steps
can also executed in order if you want more control over the process, or
if you need to restart the halfway through (eg run the export again
without rebuilding or exporting the archive).
Some other tasks are supported by rt (see below), but haven't been used for a while and may need some debugging. These are mostly aimed at external distribution of a macOS application via a website, and include support for notarizing and updating an appcast feed suitable for Sparkle.
ReleaseTools can automatically calculate and inject a build number into your application.
You can either do this only when making an archive for distribution (running rt archive
or rt submit
), or every time you make a build.
The former is more efficient, but means that the build number is not available when running from Xcode. I generally use this approach.
The latter requires running rt update-build
in a script phase, and so slightly increases the build times for normal builds.
When archiving, the injection is done by:
- generating a
VersionInfo.h
header file - overriding some build settings on the command line when invoking
xcodebuild
, so that the header file is included by the Info.plist preprocessor.
The constants defined by the VersionInfo.h
file are:
- CURRENT_PROJECT_VERSION: the calculated build number
- CURRENT_PROJECT_COMMIT: the full git commit hash
The build settings supplied on the command line are:
- INFOPLIST_PREFIX_HEADER: set to the location of the generated
VersionInfo.h
file - INFOPLIST_PREPROCESS: set to YES
- CURRENT_PROJECT_VERSION: set to the calculated build number
- CURRENT_PROJECT_COMMIT: the full git commit hash
This causes any occurences of the text CURRENT_PROJECT_VERSION
and CURRENT_PROJECT_COMMIT
to be replaced in the Info.plist file.
It should also allow any occurences of ${CURRENT_PROJECT_VERSION}
and ${CURRENT_PROJECT_COMMIT}
to be replaced in build settings or build scripts.
When using the update-build
command, you can generate the header file as described above. You can then configure your project to use it.
You can also generate an .xcconfig
file which defines the build settings CURRENT_PROJECT_VERSION
and CURRENT_PROJECT_COMMIT
, and have your project include that.
Finally, you can also specify the path of source and destination .plist
files. A new copy of the source file will be written to the destination, with the latest build and commit values written into the CFBundleVersion
and Commit
keys.
ReleaseTools supports several strategies for generating the build number, which can be controlled via command-line flags or the .rt.json
settings file:
-
Explicit Build Number
- Use
--explicit-build <number>
to specify the exact build number to use. This takes precedence over all other options. Cannot be combined with--existing-tag
,--increment-tag
, or--offset
. - Generally this is useful if you want to completely reset the build number sequence.
- Example:
rt archive --explicit-build 1234
- Use
-
Increment Highest Existing Tag
- Use
--increment-tag
(or set"incrementTag": true
in.rt.json
) to find the highest build number for the current platform in tags of the formvX.Y.Z-build-platform
, and increment it by one. If no tags are found, falls back to commit count. - Example:
rt archive --increment-tag
- Use
-
Adopt Build Number from Existing Tag
- Use
--existing-tag
(or set"useExistingTag": true
in.rt.json
) to scan all version tags for all platforms. The tool finds the highest build number for any platform and for the platform being built. If another platform has a higher build number, it uses that. If the highest for any platform matches the current platform, it increments it. - This is useful when cutting simultaneous releases across platforms and you want them to share the same build number or keep them in sync. Typically you will use
--increment-tag
when building the first platform, then--existing-tag
for the subsequent platforms. This should ensure that they all share the same number. - Example:
rt submit --platform macOS --existing-tag
- Use
-
Commit Count
- If none of the above options are specified, the build number is set to the number of commits in the repository (
git rev-list --count HEAD
). - If you are generally not releasing from multiple branches, this method is often sufficient, and for many years, it was all I did. However, the commit count is based on the current branch, and so it is possible for it to end up going down if you switch to a branch with fewer commits, and generally being inconsistent between branches. For most situations, using tags is probably more reliable.
- If none of the above options are specified, the build number is set to the number of commits in the repository (
--explicit-build
cannot be used with--existing-tag
,--increment-tag
, or--offset
. If conflicting options are provided, the tool will report an error.- If
--existing-tag
is specified, it implies--increment-tag
for fallback behavior. - If no options are specified, commit count is used by default.
- Use explicit build number:
rt update-build --explicit-build 5000
- Adopt build number from another platform's tag at HEAD:
rt archive --existing-tag
- Increment highest existing tag for platform:
rt archive --increment-tag
- Use commit count (default):
rt archive
The process of producing a release consists of a number of steps: archiving, exporting, notarizing, stapling, zipping, updating, regenerating an appcast, and publishing.
ReleaseTools is designed to be easy to debug when something goes wrong, and so each step is broken down into separate command. This makes it possible to re-run some steps without having to start at the beginning each time.
There are also --show-commands
and --show-output
options which will echo the commands that the tool is running, and the output of those commands.
When everything is working smoothly, however, the steps are expected to be run one after the other, without the need for interaction. A shell script to run them all together in the correct order, for a macOS release being distributed via an external website, might look something like:
set -e
rt archive --show-commands
rt export
rt notarize
rt wait
rt compress
rt appcast --show-commands
rt publish
More details of each command are given below:
Run xcodebuild archive
to archive the application for distribution.
The scheme to build is either specified explicitly, or set previously by the defaultScheme
setting in the .rt.json
settings file for the project.
The archive is placed into: .build/<platform>/archive.xcarchive
.
Exports the application from the archive created with the archive
command, and puts it into /build/export
.
Takes the app exported with the export
command, zips it up, and uploads it to Apple for notarization.
If the upload succeeds, the Apple servers return an xml receipt containing a RequestUUID that we can use to check on the status later. This is stored in .build/<platform>/export/receipt.xml
.
Takes the ipa exported with the export
command, and uploads it to Apple Connect for review.
If the upload succeeds, the Apple servers return an xml receipt containing status message. This isn't of much use, but is stored in .build/<platform>/export/receipt.xml
.
Requests the notarization status for the app from the Apple servers.
If the status is success
, we copy the exported app from .build/<platform>/exported
into .build/<platform>/stapled
, and staple it with the notarization ticket.
If the status is failed
, we abort with an error.
If the status is not yet known (notarization hasn't completed), we wait 10 seconds and check again.
This command will therefore not return until notarization has completed (or failed).
Compresses the app in .build/<platform>/stapled
into a zip archive suitable for inclusion in the appcast
.
This will have the name <app>-v<version>-<build>.zip
, and will be copied into the location specified with the --to=<path>
option.
A copy of the archive, with the name <app>-latest.zip
is also placed in the location specified with the --latest=<path>
option.
If these two locations aren't specified, we use the default layout, which is the equivalent of --to=Dependencies/Website
and --latest=Dependencies/Website/updates
.
Rebuilds the appcast file, using Sparkle's generate_appcast
command, which it builds first if necessary.
The file is named appcast.xml
and its location is specified with the --to=<path>
option.
If this option is not specified, we use the default layout, which is the equivalent of --to=Dependencies/Website
.
The appcast is signed using a private DSA key which is expected to be in the keychain under the name <scheme> Sparkle Key
.
If this key isn't found, it is generated, using Sparkle's generate_keys
script, and imported into the keychain. Currently I can't find a way to give the imported key the right label, so this has to be done manually using the Keychain Access
app.
The public key is expected to be called dsa_public.pem
, and be included in the Resources/
folder of the app bundle.
In order to be able to build/run the various Sparkle tools, the Sparkle project is expected to be present in the Xcode workspace.
Note: it is necessary to pass the --show-output
flag to this command, because it needs to access the keychain. If you don't do this, the command will sometimes hang (I believe because it's waiting for you to enter a password to allow keychain access).
Commits and publishes the latest changes to the website repo.
Assumes that the submodule defining the website which hosts the appcast is located at Dependencies/Website
.
This command can be used to inject the current build number and commit hash into a C-style header file, an xcconfig file, or an Info.plist file.
If you specify the --header
option with a path to a .h
file, it
will be replaced with generated content:
#define CURRENT_PROJECT_BUILD <build>
#define CURRENT_PROJECT_COMMIT <hash>
containing the current build and hash values.
If you specify the --config
option with a path to an .xcconfig
file, it will be generated with the same values as above.
If you specify the --plist
option with a path to a .plist
file,
it will be generated, or updates, with two keys CURRENT_PROJECT_BUILD
and CURRENT_PROJECT_HASH
.
This performs the archive
, export
and upload
commands in order.
It accepts the same options as those commands.
The tool is currently built using swift package manager: swift build
.
You can build and run in a single line with swift run rt <command> <args>
.
Alternatively you can build & install it somewhere, eg using Mint.
To cut down on the amount of configuration that you have to do, rt
relies on naming conventions and defaults for a lot of things.
We expect you to run rt from the root of a project folder, and we look for a workspace in that folder with the same name as the folder.
We also look for a scheme in that workspace with the same name, by default.
So if your project is in a folder called Foo
, we will use Foo.xcworkspace
as the -workspace
argument to xcodebuild, and Foo
as the -scheme
argument.
You can override the default scheme with the --scheme=<name>
option, or with a setting in the optional .rt.json
configuration file.
We support building for multiple platforms, but only one at a time. To tell rt which platform to build for, use an optional argument --platform=macOS|iOS|tvOS|watchOS
. If you have only one supported platform, you can set it in the .rt.json
file. If you don't supply the platform, it defaults to macOS
.