Info.plist Preprocessing
- #iOS
- #macOS
• 6 min read
Hello there, my fellow readers. Another day == another challenge.
Problem statement
I couldn’t shake the following problem off my mind:
On macOS, how to automate auto-increasing the build number every time you build your app? Also, based on what we’ve learned from the previous part, we must avoid a significant increase (i.e., +50) in the pull request. Indeed:
For macOS apps, build numbers must monotonically increase even across different versions.
Why? Some samples for build number updates are required to fix the problem with a debug build:
- to prevent User Notification Center from using an old icon;
- to update an installed privileged helper and compare them by the build number;
- to fix the issue with updating an icon in the Finder sidebar after adding that item programmatically.
Goal
My goal was to have a unique and increasing value for the CFBundleVersion
key in the Info.plist file of the application or plugin each time I build the target for debugging or testing. But at the same time, to have an integer number there for release builds.
Options
Unfortunately, using PlistBuddy for patching the Info.plist file from a script in the Build Phase is not an option because Info.plist is now processed after all Build Phases are complete, meaning your value in Info.plist file will be overridden.
Was I just missing something and just trying to invent a bicycle? 👀🤷♀️
ENV var is not an option, too. There’s no way to update the environment variable with dynamic values, like a timestamp, from the isolated scripts.
My last resort was Info.plist file preprocessing. It works like this: we declare a prefix header file for Info.plist, configure build settings, and put our constant names from the prefix header file into the Info.plist file. Sounds easy. Let's do it together.
Task
I wanted my debug builds to have a floating point number as a legit value for the CFBundleVersion
key of the Info.plist file. The fractional part should be the number of seconds passed from the wonderful beginning of the 2020 year (01 Jan 2020 00:00:00 GMT).
For release builds, our CFBundleVersion
should have a regular integer number.
Prerequisites
- We need a project with a configured Apple versioning system (see QA1827 Automating Version and Build Numbers Using agvtool).
- ⚠️The prefix file should exist before the build process starts for the build to succeed.
1. Script file
Let's start with a script file that would create a prefix header file defining the STP_CURRENT_PROJECT_VERSION
constant.
The script should operate with the Info.plist Preprocessor Prefix File build setting (INFOPLIST_PREFIX_HEADER
).
What the script should do:
- Create a full path to the file if it doesn't exist: temporary folders are deleted after the Clean action.
- Add timestamp after the radix point to the current build number (CURRENT_PROJECT_VERSION) for a non-release build.
- Create a prefix header file.
💻 Script create_InfoPlist_prefix_header_script.sh
#!/bin/sh
# Fail if ENV var is empty.
if [[ -z "${INFOPLIST_PREFIX_HEADER}" ]]; then
echo "error: ENV var INFOPLIST_PREFIX_HEADER is not defined."
exit 1
fi
# Exit if file exists.
if [[ -f "${INFOPLIST_PREFIX_HEADER}" ]]; then
exit 0
fi
# Create folder if needed. Cache folders are deleted after Clean action.
PREFIX_HEADER_FOLDER=$(dirname "${INFOPLIST_PREFIX_HEADER}")
if [[ ! -d "$INFOPLIST_PREFIX_HEADER" ]]; then
mkdir -p "${PREFIX_HEADER_FOLDER}"
fi
PROJECT_VERSION_SUFFIX=""
if [[ "$CONFIGURATION" != "Release" ]] && [[ "$CONFIGURATION" != "Deployment" ]]; then
# Get seconds since 2020 year.
PROJECT_VERSION_SUFFIX=".$(($(date +%s) - 1577836800))"
fi
# Create a new Info.plist prefix header file.
echo "#define STP_CURRENT_PROJECT_VERSION ${CURRENT_PROJECT_VERSION}${PROJECT_VERSION_SUFFIX}" > "${INFOPLIST_PREFIX_HEADER}"
2. Info.plist file
Paste the declared constant name STP_CURRENT_PROJECT_VERSION
into the value of the CFBundleVersion
key in the Info.plist file. No braces & dollar signs are needed.
3. Build Settings
We need to enable Info.plist preprocessing. And for that, we first need to set the value of the Preprocess Info.plist Filesetting to YES
.
We also need to figure out the path & name of the prefix header file. From the first step, we know that Info.plist Preprocessor Prefix File is our build setting. I've stopped on $(CACHE_ROOT)/InfoPlistPrefix$(CONFIGURATION).h
as a prefix header filename. You can choose your destination.
And to avoid unexpected behavior extra challenges (c), according to TN2175 Preprocessing Info.plist files in Xcode Using the C Preprocessor, we need to pass -traditional
to the Info.plist Other Preprocessor Flags build setting.
xcconfig
// Info.plist preprocessing
INFOPLIST_PREPROCESS = YES
INFOPLIST_PREFIX_HEADER = $(CACHE_ROOT)/InfoPlistPrefix$(CONFIGURATION).h
INFOPLIST_OTHER_PREPROCESSOR_FLAGS = -traditional
4. Pre-action scripts
Bad news. The prefix header file should exist before our code compile.
Good news. We still have pre-action scripts in the Build stage of our main target scheme.
So we need to delete the previous prefix header file before Build to increase the build number and create a new one.
Of course, there is a way to avoid endless deleting & updating of the prefix header file. Let's add a pre-action for the build stage:
- Edit scheme.
- Expand the Build stage on the left.
- Select pre-actions.
- Add a New Run Script Action.
- Paste your script.
Script sample
if [[ -f "${INFOPLIST_PREFIX_HEADER}" ]]; then
rm "${INFOPLIST_PREFIX_HEADER}"
fi
"${SOURCE_ROOT}/Scripts/create_InfoPlist_prefix_header_script.sh"
Stepping on grabli a rake 😅
Regarding other application targets in your main app (launch agents, plugins, helpers), don’t remove the existing prefix header file or create a new one unless you already have one. This will help you maintain the same build number across app components. Let me reiterate: you should only create a new prefix header file if it doesn't exist. Also, you will need this file if you want to build your helpers separately.
And as a bonus (not exactly), you will need to set the Info.plist Preprocessor Prefix File value for your test targets to escape failing tests executed from the command line 🤷♀️.
That's all.
🙏 Thanks to Twitch for the iOS Versioning article, Ilya Puchka for Info.plist preprocessing article, and Paul Taykalo for using the timestamp in the CFBundleVersion
.
Resources
Documentation archive
Technical Q&A QA1827: Automating Version and Build Numbers Using agvtool
Technical Note TN2175: Preprocessing Info.plist files in Xcode Using the C Preprocessor