Skip to content

Instantly share code, notes, and snippets.

@jellybeansoup
Last active June 23, 2021 08:44
Show Gist options
  • Star 43 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save jellybeansoup/db7b24fb4c7ed44030f4 to your computer and use it in GitHub Desktop.
Save jellybeansoup/db7b24fb4c7ed44030f4 to your computer and use it in GitHub Desktop.
Script for Incrementing Version Numbers
#!/bin/bash
# Link: <https://gist.github.com/jellybeansoup/db7b24fb4c7ed44030f4>
#
# A command-line script for incrementing build numbers for all known targets in an Xcode project.
#
# This script has two main goals: firstly, to ensure that all the targets in a project have the
# same CFBundleVersion and CFBundleShortVersionString values. This is because mismatched values
# can cause a warning when submitting to the App Store. Secondly, to ensure that the build number
# is incremented appropriately when git has changes.
#
# If not using git, you are a braver soul than I.
##
# The xcodeproj. This is usually found by the script, but you may need to specify its location
# if it's not in the same folder as the script is called from (the project root if called as a
# build phase run script).
#
# This value can also be provided (or overridden) using "--xcodeproj=<path>"
#
#xcodeproj="Project.xcodeproj"
##
# We have to define an Info.plist as the source of truth. This is typically the one for the main
# target. If not set, the script will try to guess the correct file from the list it gathers from
# the xcodeproj file, but this can be overriden by setting the path here.
#
# This value can also be provided (or overridden) using "--plist=<path>"
#
#plist="Project/Info.plist"
##
# By default, the script ensures that the build number is incremented when changes are declared
# based on git's records. Alternatively the number of commits on the current branch can be used
# by toggling the "reflect_commits" variable to true. If not on "master", the current branch name
# will be used to ensure no version collisions across branches, i.e. "497-develop".
#
# This setting can also be enabled using "--reflect-commits"
#
#reflect_commits=true
##
# If you would like to iterate the build number only when a specific branch is checked out
# (i.e. "master"), you can specify the branch name. The current version will still be replicated
# across all Info.plist files (to ensure consistency) if they don't match the source of truth.
#
# This setting can be enabled for multiple branches can be enabled by using comma separated names
# (i.e. "master,develop"). No spacing is permitted.
#
# This setting can also be enabled using "--branch"
#
#enable_for_branch="master"
##
# Released under the BSD License
#
# Copyright © 2017 Daniel Farrelly
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# We use PlistBuddy to handle the Info.plist values. Here we define where it lives.
plistBuddy="/usr/libexec/PlistBuddy"
# Parse input variables and update settings.
for i in "$@"; do
case $i in
-h|--help)
echo "usage: sh version-update.sh [options...]\n"
echo "Options: (when provided via the CLI, these will override options set within the script itself)"
echo "-b, --branch=<name[,name...]> Only allow the script to run on the branch with the given name(s)."
echo " --build=<number> Apply the given value to the build number (CFBundleVersion) for the project."
echo "-i, --ignore-changes Ignore git status when iterating build number (doesn't apply to manual values or --reflect-commits)."
echo "-p, --plist=<path> Use the specified plist file as the source of truth for version details."
echo " --reflect-commits Reflect the number of commits in the current branch when preparing build numbers."
echo " --version=<number> Apply the given value to the marketing version (CFBundleShortVersionString) for the project."
echo "-x, --xcodeproj=<path> Use the specified Xcode project file to gather plist names."
echo "\nFor more detailed information on the use of these variables, see the script source."
exit 1
;;
--reflect-commits)
reflect_commits=true
shift
;;
-x=*|--xcodeproj=*)
xcodeproj="${i#*=}"
shift
;;
-p=*|--plist=*)
plist="${i#*=}"
shift
;;
-b=*|--branch=*)
enable_for_branch="${i#*=}"
shift
;;
--build=*)
specified_build="${i#*=}"
shift
;;
--version=*)
specified_version="${i#*=}"
shift
;;
-i|--ignore-changes)
ignore_git_status=true
shift
;;
*)
;;
esac
done
# Locate the xcodeproj.
# If we've specified a xcodeproj above, we'll simply use that instead.
if [[ -z ${xcodeproj} ]]; then
xcodeproj=$(find . -depth 1 -name "*.xcodeproj" | sed -e 's/^\.\///g')
fi
# Check that the xcodeproj file we've located is valid, and warn if it isn't.
# This could also indicate an issue with the code used to automatically locate the xcodeproj file.
# If you're encountering this and the file exists, ensure that ${xcodeproj} contains the correct
# path, or use the "--xcodeproj" variable to provide an accurate location.
if [[ ! -f "${xcodeproj}/project.pbxproj" ]]; then
echo "${BASH_SOURCE}:${LINENO}: error: Could not locate the xcodeproj file \"${xcodeproj}\"."
exit 1
else
echo "Xcode Project: \"${xcodeproj}\""
fi
# Find unique references to Info.plist files in the project
projectFile="${xcodeproj}/project.pbxproj"
plists=$(grep "^\s*INFOPLIST_FILE.*$" "${projectFile}" | sed -Ee 's/^[[:space:]]+INFOPLIST_FILE[[:space:]*=[[:space:]]*["]?([^"]+)["]?;$/\1/g' | sort | uniq)
# Attempt to guess the plist based on the list we have.
# If we've specified a plist above, we'll simply use that instead.
if [[ -z ${plist} ]]; then
read -r plist <<< "${plists}"
fi
# Check that the plist file we've located is valid, and warn if it isn't.
# This could also indicate an issue with the code used to match plist files in the xcodeproj file.
# If you're encountering this and the file exists, ensure that ${plists} contains _ONLY_ filenames.
if [[ ! -f ${plist} ]]; then
echo "${BASH_SOURCE}:${LINENO}: error: Could not locate the plist file \"${plist}\"."
exit 1
else
echo "Source Info.plist: \"${plist}\""
fi
# Find the current build number in the main Info.plist
mainBundleVersion=$("${plistBuddy}" -c "Print CFBundleVersion" "${plist}")
mainBundleShortVersionString=$("${plistBuddy}" -c "Print CFBundleShortVersionString" "${plist}")
echo "Current project version is ${mainBundleShortVersionString} (${mainBundleVersion})."
# If the user specified a marketing version (via "--version"), we overwrite the version from the source of truth.
if [[ ! -z ${specified_version} ]]; then
mainBundleShortVersionString=${specified_version}
echo "Applying specified marketing version (${specified_version})..."
fi
# Increment the build number if git says things have changed. Note that we also check the main
# Info.plist file, and if it has already been modified, we don't increment the build number.
# Alternatively, if the script has been called using "--reflect-commits", we just update to the
# current number of commits. We can also specify a build number to use with "--build".
git=$(sh /etc/profile; which git)
branchName=$("${git}" rev-parse --abbrev-ref HEAD)
if [[ -z ${enable_for_branch} ]] || [[ ",${enable_for_branch}," == *",${branchName},"* ]]; then
if [[ ! -z ${specified_build} ]]; then
mainBundleVersion=${specified_build}
echo "Applying specified build number (${specified_build})..."
elif [[ ! -z ${reflect_commits} ]] && [[ ${reflect_commits} ]]; then
currentBundleVersion=${mainBundleVersion}
mainBundleVersion=$("${git}" rev-list --count HEAD)
if [[ ${branchName} != "master" ]]; then
mainBundleVersion="${mainBundleVersion}-${branchName}"
fi
if [[ ${currentBundleVersion} != ${mainBundleVersion} ]]; then
echo "Branch \"${branchName}\" has ${mainBundleVersion} commit(s). Updating build number..."
else
echo "Branch \"${branchName}\" has ${mainBundleVersion} commit(s). Version is stable."
fi
elif [[ ! -z ${ignore_git_status} ]] && [[ ${ignore_git_status} ]]; then
echo "Iterating build number (forced)..."
mainBundleVersion=$((${mainBundleVersion} + 1))
else
status=$("${git}" status --porcelain)
if [[ ${#status} == 0 ]]; then
echo "Repository does not have any changes. Version is stable."
elif [[ ${status} == *"M ${plist}"* ]] || [[ ${status} == *"M \"${plist}\""* ]]; then
echo "The source Info.plist has been modified. Version is assumed to be stable. Use --ignore-changes to override."
else
echo "Repository is dirty. Iterating build number..."
mainBundleVersion=$((${mainBundleVersion} + 1))
fi
fi
else
echo "${xcodeproj}:0: warning: Version number updates are disabled for the current git branch (${branchName})."
fi
# Update all of the Info.plist files we discovered
while read -r thisPlist; do
# Find out the current version
thisBundleVersion=$("${plistBuddy}" -c "Print CFBundleVersion" "${thisPlist}")
thisBundleShortVersionString=$("${plistBuddy}" -c "Print CFBundleShortVersionString" "${thisPlist}")
# Update the CFBundleVersion if needed
if [[ ${thisBundleVersion} != ${mainBundleVersion} ]]; then
echo "Updating \"${thisPlist}\" with build ${mainBundleVersion}..."
"${plistBuddy}" -c "Set :CFBundleVersion ${mainBundleVersion}" "${thisPlist}"
fi
# Update the CFBundleShortVersionString if needed
if [[ ${thisBundleShortVersionString} != ${mainBundleShortVersionString} ]]; then
echo "Updating \"${thisPlist}\" with marketing version ${mainBundleShortVersionString}..."
"${plistBuddy}" -c "Set :CFBundleShortVersionString ${mainBundleShortVersionString}" "${thisPlist}"
fi
done <<< "${plists}"
@jellybeansoup
Copy link
Author

i've updated the script again to fix a bug which i'd introduced where --reflect-commits was ignored (unless it wasn't set, i guess?)

also new is the ability to specify a build number or marketing version from the CLI using --build=<number> and --version=<number>, respectively. this allows you to reset the value to a specific number so that it doesn't cause your repo to be dirty when you're using the aforementioned --reflect-commits flag.

@kamalmarhubi
Copy link

@jellybeansoup could you add a license to this script? It's really useful, but I want to know if I can use it in my project! :-)

@rph8
Copy link

rph8 commented Jan 27, 2017

Does not do anything for me. Whenever I run ./update-version.sh or ./update-version.sh --reflect-commits in the xcodeproj directory I only get the output
Xcode Project: "xyz.xcodeproj"
Source Info.plist: "xyz/Info.plist"
Current project version is 0.1.2 (125).

@jellybeansoup
Copy link
Author

@kamalmarhubi i typically use the 2-clause bsd license, but i honestly don't care much as long as you're not distributing the source. for the purposes of inclusion in OSS projects, i've added the license to the file.

@rph8 the --reflect-commits uses git rev-list --count HEAD to determine the number of commits to the current branch. without it the script will determine changes based on git status --porcelain and will NOT run unless there ARE changes and the source info.plist file has NOT been modified. i've updated the script to include an --ignore-changes flag that will ALWAYS iterate the build number (unless --reflect-commits or --build is specified, or if on an ignored branch). use it at your own peril.

@lexum0
Copy link

lexum0 commented Sep 28, 2017

is there a way to update only the main bundle when compiling in Debug, then update all plists when compiling Release or Archive?

I have a project with several targets, and would like to update only the main plist, otherwise every commit shows several files modified for only a small change. The script seems to differentiate a main plist, but I don't understand it fully to modify it.

Thanks for any info

@lexum0
Copy link

lexum0 commented Sep 28, 2017

I've solved it :D

@norlin
Copy link

norlin commented Nov 19, 2017

@jellybeansoup

but i honestly don't care much

Probably you should consider to move to my favorite license – WTFPL. Using it for all my personal projects.

p.s. thanks a lot for the script!

@inekipelov
Copy link

@jellybeansoup Sorry for noob question. This script can be used from Archive Pre-Actions?

@jellybeansoup
Copy link
Author

@inekipelov Theoretically, yes. I've only ever run it outside of Xcode, or as a Run Script Build Phase, so YMMV.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment