Info.plist Preprocessing

Article's main picture
  • #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

  1. We need a project with a configured Apple versioning system (see QA1827 Automating Version and Build Numbers Using agvtool).
  2. ⚠️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:

  1. Create a full path to the file if it doesn't exist: temporary folders are deleted after the Clean action.
  2. Add timestamp after the radix point to the current build number (CURRENT_PROJECT_VERSION) for a non-release build.
  3. 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.

Info.plist
Info.plist

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

Build Settings
Build Settings

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:

  1. Edit scheme.
  2. Expand the Build stage on the left.
  3. Select pre-actions.
  4. Add a New Run Script Action.
  5. Paste your script.

Screenshot if you get lost
Screenshot if you get lost

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

More From engineering

Subscribe to our newsletter