Skip to content

Instantly share code, notes, and snippets.

@jellybeansoup
Last active June 23, 2021 08:44
Show Gist options
  • 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}"
@razalur
Copy link

razalur commented Feb 24, 2016

@steveatinfincia Thank you!

@DavidGagne
Copy link

I'm trying to use this and have followed instructions exactly, but am getting a "Permission denied" failure when I try to build. Any ideas?

@stephanmantler
Copy link

stephanmantler commented Sep 23, 2016

Ran into the same problem as @razalur and @steveatinfincia; I think the problem with the original is that filenames aren't always delimited by " so the expression doesn't have anything to anchor against. I replaced the pipe through two (or more) sed expressions with one that does the entire job by using a back reference:

plists=$(grep "^\s*INFOPLIST_FILE.*$" "${projectFile}" | sed -Ee 's/^[[:space:]]+INFOPLIST_FILE[[:space:]*=[[:space:]]*["]?([^"]+)["]?;$/\1/g' | sort | uniq)

The fairly strict expression (it must match the entire line) will hopefully help catch strange errors if the syntax changes again.

Anyway, thanks @jellybeansoup for the original script (and the podcasts – am a regular listener!)

@jellybeansoup
Copy link
Author

jellybeansoup commented Dec 6, 2016

thanks @razalur, @steveatinfincia and @stephanmantler for pointing out the problem with the code for parsing plists from the xcodeproj (sorry it's taken so long for me to get around to actually dealing with it). i've updated my version of the script with @stephanmantler's code, and dropped in a couple of checks to ensure that the script doesn't try to power through with the wrong files, should it ever come to that. if you're running this script as a build phase run script (like i am), you should have the additional benefit of the messages popping up as proper xcode errors.

i've also several new abilities to the script. firstly, there's now a basic help output when calling sh update-version.sh --help. secondly, you can now enable updating of the version number only for specific git branches. finally, you can now provide the xcodeproj and plist values via the command line, which allows you to utilise the xcode environment variables when calling the script from a build phase run script, like so:

sh update-version.sh --build=master --xcodeproj=$PROJECT_FILE --plist=$INFOPLIST_FILE

I've attempted to document these changes within the script (as they are all able to be toggled as hardcoded values, or with the cli variables). if you have any questions, i'll be happy to answer them (but expect a delay if they're asked here, use my contact form for a more timely response.

@DavidGagne i'm not sure what could be going on here. can you please provide the complete output you get when running the script?

@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