Up until today, I have been using a Build Phase script to update the CFBundleVersion in the Info.plist file using a script like this:
#!/bin/bash
if [[ ! -d .git ]]; then
echo "Error: Git setup not found: ./.git"
exit 1
fi
# The number of commits in the master branch.
rev=$(expr $(git rev-list master --count) - $(git rev-list HEAD..master --count))
echo "Updating build number to $rev:"
for plist in
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
; do
if [ -f "$plist" ]; then
echo " -> $(file "$plist")"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $rev" "$plist"
plutil -lint -s "$plist"
else
echo " ?? $plist"
fi
done
Sadly, with Xcode 15, this stopped working. Whether or not ENABLE_USER_SCRIPT_SANDBOXING was enabled or not, Xcode no longer allowed access to the plist file (or complained about a circular dependency).
So, now I am using a build configuration file to set the build number; the config file needs to be added to the Xcode project, and selected in the project "Configurations". My file contains just:
// proj.xcconfig
CURRENT_PROJECT_VERSION = 0
Also, the Info.plist file needs to be updated to use:
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
My new methodology uses a "Build pre-actions" script (added in the target's scheme editor and using the build settings from the target) to update the .xcconfig file before every build:
#!/bin/bash
cd "$SOURCE_ROOT"
if [[ ! -d .git ]]; then
echo "Error: Git setup not found: ./.git"
exit 1
fi
# The number of commits in the master branch.
rev=$(expr $(git rev-list master --count) - $(git rev-list HEAD..master --count))
echo "Updating build number to $rev:"
sed -i '' "/CURRENT_PROJECT_VERSION/ { s/=.*/= $rev/; }" ./*.xcconfig
But since the .xcconfig file is also a revision-controlled file, I reset the value again in a "Build post-actions" script. Similar to using git restore to revert the changes:
#!/bin/bash
cd "$SOURCE_ROOT"
sed -i '' '/CURRENT_PROJECT_VERSION/ { s/=.*/= 0/; }' ./*.xcconfig
Explanation of the sed code:
- For any file matching
*.xcconfig,
- modify it "in place" (not creating a backup file),
- all lines containing "CURRENT_PROJECT_VERSION",
- substitute text beginnig with "=" to "= $rev" or "= 0".
Update 2024-01-11
Finally, I added a build configuration script to make sure that the $(CURRENT_PROJECT_VERSION) variable contains a non-zero value:
#!/bin/sh
ok=0
if [ ${CURRENT_PROJECT_VERSION:-0} -eq 0 ]; then
echo 'Error: CURRENT_PROJECT_VERSION has not been set.'
ok=1
fi
exit $ok
Update 2025-01-01
The script has evolved further. I am no longer re-setting the values in the .xcconfig file since Xcode is very sensitive to any changes to the file, particularly when working with SwiftUI previews. The new approach is as follows; this is one scheme pre-build script, shown here in sections:
To begin with, some setup and checks that a git repo is present and that Xcode is calling the script:
cd "$PROJECT_DIR"
# The default branch, if not supplied as an argument, is 'master'.
branch=${1:-'master'}
year=$(date +%Y)
if [ ! -d ./.git ]; then
echo "Error: Git setup not found: $(pwd)/.git" >&2
exit 1
fi
if [ -z "${MARKETING_VERSION:-}" ]; then
echo 'Error: MARKETING_VERSION has not been set.' >&2
exit 1
fi
The build number is worked out as the number of commits:
# The number of commits in the branch, less the number of commits
# behind in that branch.
rev=$(expr $(git rev-list $branch --count) - $(git rev-list HEAD..$branch --count))
if [ -z "$rev" ]; then
echo 'Error: Unable to work out commit count.' >&2
exit 1
fi
echo "Updating build number to $rev using branch '$branch':"
To avoid changing the local file, we create a copy in this temporary directory, creating it if missing:
[ -d "$CONFIGURATION_TEMP_DIR" ] || mkdir -p "$CONFIGURATION_TEMP_DIR"
I have moved the input .xcconfig template files in a ./Config directory, which will be modified and a copy is created in the local directory. Also, I have renamed my variables to TREDJE_BUILD_NUMBER and TREDJE_YEAR.
Two important points:
The script creates a copy in a temporary directory and only overwrites the local file if they are different. Keeping the local file untouched in this way avoids repeated re-builds for SwiftUI previews.
While I have checked in a basic version of the .xcconfig file, I never want to check in the modified version. This is achieved by instructing git to ignore the local file change.
This is done here:
for file in ./Config/*.xcconfig ; do
fout=${file#*Config/}
name=${file##*/}
[ ! -f "$file" ] && echo " ?? $file" && continue
[ ! -f "$fout" ] && echo " ?? $fout" && continue
# Put the up-to-date data in a temporary file.
sed "/TREDJE_BUILD_NUMBER/ s/=.*/= $rev/; /TREDJE_YEAR/ s/=.*/= $year/;" "$file" > "$CONFIGURATION_TEMP_DIR/$name"
# Skip changing the file if identical to the temp file.
$(cmp -s "$fout" "$CONFIGURATION_TEMP_DIR/$name") && continue
echo " -> $fout"
mv "$CONFIGURATION_TEMP_DIR/$name" "$fout"
git update-index --assume-unchanged "$fout"
done
Finally, I do a similar change for a settings bundle Plist file:
echo "Updating YEAR to $year and VERSION_STRING to $MARKETING_VERSION ($rev):"
for file in
./Config/iOS/Settings.bundle/Root.plist
; do
fout=${file#*Config/}
name=${file##*/}
[ ! -f "$file" ] && echo " ?? $file" && continue
[ ! -f "$fout" ] && echo " ?? $fout" && continue
sed "s/TREDJE_VERSION_STRING/$MARKETING_VERSION ($rev)/; s/TREDJE_YEAR/$year/;" "$file" > "$CONFIGURATION_TEMP_DIR/$name"
# Skip changing the file if identical to the temp file.
$(cmp -s "$fout" "$CONFIGURATION_TEMP_DIR/$name") && continue
echo " -> $(file "$fout")"
mv "$CONFIGURATION_TEMP_DIR/$name" "$fout"
plutil -lint -s "$fout"
git update-index --assume-unchanged "$fout"
done