Skip to content

Instantly share code, notes, and snippets.

@jeremy-w
Last active June 3, 2020 19:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremy-w/d1c5b53dddba4bfce10d1adb1c8295c6 to your computer and use it in GitHub Desktop.
Save jeremy-w/d1c5b53dddba4bfce10d1adb1c8295c6 to your computer and use it in GitHub Desktop.
Rigging up versioning for iOS, the fun way
#!/bin/bash
set -euo pipefail
# Generate a CURRENT_PROJECT_VERSION declaration that
# encodes a framework's build time and git revision info,
# and a DYLIB_CURRENT_VERSION that encodes the build time.
## region CONFIG
# The xccconfig file to produce.
GIT_TARGET_FILE="Config/generated_current_version.xcconfig"
TARGET="$SRCROOT/$GIT_TARGET_FILE"
# Arbitrarily chosen. Minutes will be counted up from this time.
#
# Generated with: date +%s
#
# Can be converted back with: date -r $EPOCH
# Add flag -u to get time in UTC rather than local.
EPOCH=1591104427
## endregion CONFIG
echo "Configured to use epoch $EPOCH in generating file $TARGET. To change these values, edit file: ${BASH_SOURCE[0]}, then add and delete a space from the Versioning target's Run Script build phase to invalidate Xcode's cached version of the script."
# Compute the minutes elapsed since EPOCH.
# This will be used as the major version.
NOW=$(date +%s)
MINUTES=$(( (NOW - EPOCH) / 60 ))
echo "MAJOR VERSION: $MINUTES, the minutes elapsed between $(date -r "$EPOCH") and $(date -r "$NOW")"
# Attempt to encode git commit info into the minor version.
if git --git-dir="$SRCROOT/.git" rev-parse --git-dir >/dev/null 2>&1; then
# Massage the short name for the HEAD commit into decimal format.
# This gets appended to 1 (in case it has leading zeroes) as the minor version.
function decimalize() {
sha="$1"
upper_sha=$(tr 'a-f' 'A-F' <<< "$sha")
dc -e "10 o 16 i $upper_sha p"
}
SHA=$(git --git-dir="$SRCROOT/.git" rev-parse --short=7 HEAD)
DECIMAL_SHA=$(decimalize "$SHA")
# Leading 1 signals this is an actual git commit and the info is current.
MINOR="1$DECIMAL_SHA"
MAYBE_CAVEAT=""
echo "MINOR VERSION: $MINOR. HEAD commit is $SHA in hex, which is $DECIMAL_SHA in decimal."
else
# Preserve the existing SHA.
DECIMAL_SHA=$(grep 'CURRENT_PROJECT_VERSION =' "$TARGET" |
cut -d'=' -f2 | # 'VERSION = X.Y' => ' X.1SHA'
cut -d'.' -f2 | # ' X.Y' => '1SHA'
cut -c2-) # '1SHA' => 'SHA'
SHA=$(dc -e "16 o 10 i $DECIMAL_SHA p" | tr 'A-F' 'a-f')
# Leading 2 signals this isn't actually built from a git checkout, so this info might be inaccurate.
MINOR="2$DECIMAL_SHA"
MAYBE_CAVEAT="(possibly stale - project was built without a gitdir)"
fi
# Dylibs have limits that prevent this approach. For their version numbers, we just use the minutes part.
COMPUTED_VERSION="$MINUTES.$MINOR"
sed -e 's,^,// ,' -e 's, $,,' >"$TARGET" <<EOT
DO NOT EDIT. THIS IS A GENERATED FILE.
This file is re-created each build of the framework using a Run Script phase in the Versioning aggregate target.
It is ignored by git.
Its settings are applied to the relevant targets via the Config/version.xcconfig file.
The information encoded in the project version is:
- Build date: "$(date -r "$NOW")"
- Git short ref: $SHA $MAYBE_CAVEAT
The information encoded in the library version is only the build date,
due to restrictions on the range of each version component.
See: https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/DynamicLibraryDesignGuidelines.html#//apple_ref/doc/uid/TP40002013-SW23
EOT
echo "CURRENT_PROJECT_VERSION = $COMPUTED_VERSION" | tee -a "$TARGET"
echo "DYLIB_CURRENT_VERSION = $MINUTES" | tee -a "$TARGET"
# If we don't have a committed version, then #including it from the applied version.xcconfig fails, and the build fails before any build steps run, and this re-generator script never gets to run.
# This tells git to ignore further changes to the committed file, so it's not always showing as changed.
echo "Instructing git to ignore all changes to the generated, versioned file."
git --git-dir="$SRCROOT/.git" update-index --skip-worktree "$GIT_TARGET_FILE" || true
#import <UIKit/UIKit.h>
//! Project version number for Proj.
FOUNDATION_EXPORT double ProjVersionNumber;
//! Project version string for Proj.
FOUNDATION_EXPORT const unsigned char ProjVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Proj/PublicHeader.h>
#!/bin/bash
# Outputs configuration info that's convenient to know about builds.
# Paste this into your release description.
# You can copy it by running and piping into pbcopy, like so:
#
# bin/release-info.sh | pbcopy
#
# Now it's ready to Cmd-v into a form field.
# If we have args, assume we're being asked to print info about a bundle version, like 123.12341234.
if [[ $# -gt 0 ]]; then
# Duped from generate-bundle-version. It shouldn't need changing in either place for a couple hundred years.
EPOCH=1591104427
majorDotMinor="$1"
echo "version=$majorDotMinor"
major=$(echo "$majorDotMinor" | cut -d'.' -f1)
timestamp=$(( EPOCH + 60*major ))
localDate=$(date -r "$timestamp")
utcDate=$(date -ur "$timestamp")
minor=$(echo "$majorDotMinor" | cut -d'.' -f2)
# Also drop the first character, which was prefixed to preserve any leading zeroes.
shaPart=${minor:1}
sha=$(dc -e "16 o 10 i $shaPart p" | tr 'A-F' 'a-f')
printf "built at:\n\tunixTime=%s\n\tlocalDate=%s\n\tutcDate=%s\n" "$timestamp" "$localDate" "$utcDate"
echo "gitSha=$sha"
git describe --all --always --long "$sha" || true
exit 0
fi
set -euo pipefail
echo "GIT"
git rev-parse master
git describe --long master || true # fails until first annotated tag is created
echo
echo "XCODE"
xcodebuild -version
echo
echo "SWIFT"
xcrun swift --version
echo
echo "CLANG"
xcrun clang --version
echo
echo "macOS"
sw_vers

Releasing a New Version

In a Nutshell

Steps:

  1. Check out the branch master
  2. Update the MARKETING_VERSION in Config/version.xcconfig.
  • Note: This must not have a leading v.
  1. Commit the change as "Bump version to vX.Y.Z":
  • Run:
    git add Config/version.xcconfig && git commit -m "Bump version"
  1. Do a build and commit the updated Config/generated_current_version.xcconfig for Carthage' sake:
  • Run:
    SRCROOT=$PWD bin/generate-bundle-version.sh
    git update-index --no-skip-worktree Config/generated_current_version.xcconfig
    git add Config/generated_current_version.xcconfig && git commit -m 'Update generated version'
  1. Tag that commit with the new version number:
  • Run: VERS='vX.Y.Z' git tag -a -m "Release $VERS" "$VERS"
  1. Archive a pre-built framework for the release
  • Run: carthage checkout && carthage build --cache-builds --no-skip-current && carthage archive EnsentaKit
  1. Push master along with the tag:
  • Run: git push --follow-tags
  1. Attach the archive to a corresponding GitHub release
  1. Paste into the description the output of bin/release-info.sh:
  • Run: bin/release-info.sh | pbcopy

Step Details

Update the marketing version

There are actually two versions:

  • marketing, like v1.2.3
  • internal, like 78.186417373

The public, or marketing, version is the one that aligns with the tagged version. That's the one you need to update.

You'll find it defined in the file Config/version.xcconfig.

Update it there. Follow semver.

NOTE: Till we ship a final, certified build, we're anticipating sticking with v0.y.z, which promises no compatibility. You can just bump the minor version for now, so our first few tags will be v0.0.0, v0.1.0, etc.

Don't forget to commit, but don't push!

git commit -um 'Bump version'

(The internal version is autogenerated at build time. You can convert from its encoded form to a human-readable form by passing the version number to bin/release-info.sh, e.g. bin/release-info.sh 166.186417373.)

Update the generated version

Carthage doesn't do a checkout with git info. So we want to leave the existing git info intact during a Carthage build.

Run from the project root:

SRCROOT=$PWD bin/generate-bundle-version.sh
git update-index --no-skip-worktree Config/generated_current_version.xcconfig
git add Config/generated_current_version.xcconfig && git commit -m 'Update generated version'

Tag the version

Create an annotated tag.

env VERS='v0.Y.0' git tag -am "Release $VERS" "$VERS"

Don't push!

Archive the version

(We do this before pushing the tag, in case the archive build fails. At that point, we can fix the issue, force-uupdate the tag, and none's the wiser.)

Build the project for Release using Carthage:

carthage checkout && carthage build --archive

Push the commit and tag

If the build went fine, you're good to go:

git push --follow-tags

Attach the archive

Add a prebuilt framework to the release to speed up build times with Carthage:

  • Navigate to the release you'd like to add the framework to at https://github.com/X/Y/releases
  • Edit the GitHub release and upload the ZIP file that Carthage created.
  • Paste in the output of the bin/release-info.sh script.

Carthage will then download this prebuilt framework instead of building from source. Provided you're building for the same Swift version as was used to create the archive, you will save a lot of build time.

// Declare autogenerated versions using a config file produced by the "Versioning" aggregate target.
// (Note that one version of that file has been committed to prevent this #include line from erroring out and failing the build before the generator gets to run.)
#include "./generated_current_version.xcconfig"
// Update this and commit the change before tagging a new release.
// It should look like: X.Y.Z - there is NO leading "v", unlike the tag!
//
// See docs in file: RELEASING.md for more info.
MARKETING_VERSION = 0.0.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment