1

Years ago, when compiling with GCC, the following defines in a #include .h file could be pre-processed for use in info.plist:

#define MAJORVERSION 2
#define MINORVERSION 6 
#define MAINTVERSION 4

<key>CFBundleShortVersionString</key> <string>MAJORVERSION.MINORVERSION.MAINTVERSION</string>

...which would turn into "2.6.4". That worked because GCC supported the "-traditional" flag. (see Tech Note TN2175 Info.plist files in Xcode Using the C Preprocessor, under "Eliminating whitespace between tokens in the macro expansion process")

However, fast-forward to 2016 and Clang 7.0.2 (Xcode 7.2.1) apparently does not support either "-traditional" or "-traditional-cpp" (or support it properly), yielding this string:

"2 . 6 . 4"

(see Bug 12035 - Preprocessor inserts spaces in macro expansions, comment 4)

Because there are so many different variations (CFBundleShortVersionString, CFBundleVersion, CFBundleGetInfoString), it would be nice to work around this clang problem, and define these once, and concatenate / stringify the pieces together. What is the commonly-accepted pattern for doing this now? (I'm presently building on MacOS but the same pattern would work for IOS)

SMGreenfield
  • 1,680
  • 19
  • 35
  • 1
    Apart from the typos in your sample code it works as it should - adjacent strings are concatenated and no spaces are added (tested in Xcode 7.2). If you are seeing spaces you probably need to provide more context before anyone can help you figure it out. – CRD Mar 15 '16 at 08:28
  • @CRD -- Thanks -- I've re-written the example to accurately reflect the real problem, not how I was hacking together a potential solution. – SMGreenfield Mar 15 '16 at 09:41
  • 1
    I always store the version number in an external file and have a pre-build script (written in python) to update the plist.info files and generate any `version.h` files to be included in the project. It's simple and solid and even cross platform (i.e. you can modify it to write to Windows resource files). It also increments the build number if it detects a change to any source file, so it manages itself mostly. – trojanfoe Mar 15 '16 at 09:44
  • @trojanfoe - was that to work around this bug with the -traditional flag? Can you share an example of your Python script? – SMGreenfield Mar 15 '16 at 09:48
  • No it wasn't to work around this issue as I never even considered using something like this to manage version numbers. I have posted my code below as an answer, but this version does not generation `version.h` files (there have been many iterations of this script). – trojanfoe Mar 15 '16 at 10:00

2 Answers2

1

Here is the Python script I use to increment my build number, whenever a source code change is detected, and update one or more Info.plist files within the project.

It was created to solve the issue raised in this question I asked a while back.

You need to create buildnum.ver file in the source tree that looks like this:

version 1.0
build 1

(you will need to manually increment version when certain project milestones are reached, but buildnum is incremented automatically).

NOTE the location of the .ver file must be in the root of the source tree (see SourceDir, below) as this script will look for modified files in this directory. If any are found, the build number is incremented. Modified means source files changes after the .ver file was last updated.

Then create a new Xcode target to run an external build tool and run something like:

tools/bump_buildnum.py SourceDir/buildnum.ver SourceDir/Info.plist

(make it run in ${PROJECT_DIR})

and then make all the actual Xcode targets dependent upon this target, so it runs before any of them are built.

#!/usr/bin/env python
#
# Bump build number in Info.plist files if a source file have changed.
#
# usage: bump_buildnum.py buildnum.ver Info.plist [ ... Info.plist ]
#
# andy@trojanfoe.com, 2014.
#

import sys, os, subprocess, re

def read_verfile(name):
    version = None
    build = None
    verfile = open(name, "r")
    for line in verfile:
        match = re.match(r"^version\s+(\S+)", line)
        if match:
            version = match.group(1).rstrip()
        match = re.match(r"^build\s+(\S+)", line)
        if match:
            build = int(match.group(1).rstrip())
    verfile.close()
    return (version, build)

def write_verfile(name, version, build):
    verfile = open(name, "w")
    verfile.write("version {0}\n".format(version))
    verfile.write("build {0}\n".format(build))
    verfile.close()
    return True

def set_plist_version(plistname, version, build):
    if not os.path.exists(plistname):
        print("{0} does not exist".format(plistname))
        return False

    plistbuddy = '/usr/libexec/Plistbuddy'
    if not os.path.exists(plistbuddy):
        print("{0} does not exist".format(plistbuddy))
        return False

    cmdline = [plistbuddy,
        "-c", "Set CFBundleShortVersionString {0}".format(version),
        "-c", "Set CFBundleVersion {0}".format(build),
        plistname]
    if subprocess.call(cmdline) != 0:
        print("Failed to update {0}".format(plistname))
        return False

    print("Updated {0} with v{1} ({2})".format(plistname, version, build))
    return True

def should_bump(vername, dirname):
    verstat = os.stat(vername)
    allnames = []
    for dirname, dirnames, filenames in os.walk(dirname):
        for filename in filenames:
            allnames.append(os.path.join(dirname, filename))

    for filename in allnames:
        filestat = os.stat(filename)
        if filestat.st_mtime > verstat.st_mtime:
            print("{0} is newer than {1}".format(filename, vername))
            return True

    return False

def upver(vername):
    (version, build) = read_verfile(vername)
    if version == None or build == None:
        print("Failed to read version/build from {0}".format(vername))
        return False

    # Bump the version number if any files in the same directory as the version file
    # have changed, including sub-directories.
    srcdir = os.path.dirname(vername)
    bump = should_bump(vername, srcdir)

    if bump:
        build += 1
        print("Incremented to build {0}".format(build))
        write_verfile(vername, version, build)
        print("Written {0}".format(vername))
    else:
        print("Staying at build {0}".format(build))

    return (version, build)

if __name__ == "__main__":
    if os.environ.has_key('ACTION') and os.environ['ACTION'] == 'clean':
        print("{0}: Not running while cleaning".format(sys.argv[0]))
        sys.exit(0)

    if len(sys.argv) < 3:
        print("Usage: {0} buildnum.ver Info.plist [... Info.plist]".format(sys.argv[0]))
        sys.exit(1)
    vername = sys.argv[1]

    (version, build) = upver(vername)
    if version == None or build == None:
        sys.exit(2)

    for i in range(2, len(sys.argv)):
        plistname = sys.argv[i]
        set_plist_version(plistname, version, build)        

    sys.exit(0)
Community
  • 1
  • 1
trojanfoe
  • 120,358
  • 21
  • 212
  • 242
  • WOW! Elegant AND educational! You describe the possibility of writing version.h -- in my case, I refer to these build and version numbers in various .cpp files. Could it also work by parsing the version components from a version.h file, or is it just easier to rewrite a version.h? – SMGreenfield Mar 15 '16 at 10:18
  • Also -- doesn't Apple's version number scheme limit the build number (major.minor.maint.build) to the range 0-255, or is that an outdated limitation? I love the automatic bumping of the build number, but I would expect that number to get pretty high pretty fast. – SMGreenfield Mar 15 '16 at 10:24
  • Well I used to have versions of this script that generated either `.c`, `.cpp`, `.m` and associated `.h` files, but if I am writing a Cocoa app I don't bother with that and simply read it from the `Info.plist` file at runtime and store it globally. Thanks to `git` [here](https://gist.github.com/trojanfoe/3e094086830fd426b30c) is the original version, but beware it will work slightly differently to the one in my answer and will possibly contain bugs. – trojanfoe Mar 15 '16 at 10:24
  • @SMGreenfield I am not aware of limitations in the version number. I always use `major.minor` for version and a simple integer for build number and have never had issues. It is interesting how high the number gets but efforts are made to make it meaningful as it won't increment each build, just when source files have changed, each build. – trojanfoe Mar 15 '16 at 10:25
  • thanks again, impressive (as are a lot of your posts!). Will review your code more closely when I'm awake tomorrow. Still curious if others have run into the clang defect. – SMGreenfield Mar 15 '16 at 10:31
  • One thing I've just noticed with my original script is that it attempts to get the current git SHA1 and put that in the version information as well. That is flawed really as the SHA1 will change once you commit the tracked files so it will be always be *one-out*. Therefore I would recommend taking that stuff out and simplify as has been my trend more recently. – trojanfoe Mar 16 '16 at 12:03
0

First, I would like to clarify what each key is meant to do:

  • CFBundleShortVersionString

    A string describing the released version of an app, using semantic versioning. This string will be displayed in the App Store description.

  • CFBundleVersion

    A string specifing the build version (released or unreleased). It is a string, but Apple recommends to use numbers instead.

  • CFBundleGetInfoString

    Seems to be deprecated, as it is no longer listed in the Information Property List Key Reference.

During development, CFBundleShortVersionString isn't changed that often, and I normally set CFBundleShortVersionString manually in Xcode. The only string I change regularly is CFBundleVersion, because you can't submit a new build to iTunes Connect/TestFlight, if the CFBundleVersion wasn't changed.

To change the value, I use a Rake task with PlistBuddy to write a time stamp (year, month, day, hour, and minute) to CFBundleVersion:

desc "Bump bundle version"
task :bump_bundle_version do
  bundle_version = Time.now.strftime "%Y%m%d%H%M"
  sh %Q{/usr/libexec/PlistBuddy -c "Set CFBundleVersion #{bundle_version}" "DemoApp/DemoApp-Info.plist"}
end

You can use PlistBuddy, if you need to automate CFBundleShortVersionString as well.

mkalmes
  • 144
  • 1
  • 10
  • As far as I can tell, CFBundleGetInfoString is absolutely still used -- because I set it to contain my company name and copyright information, and that is exactly what shows up in the Version field of the Get Info window. – SMGreenfield Mar 15 '16 at 15:43
  • 1
    Deprecation doesn't mean that you can't use it. It's just not recommended anymore :) Since `CFBundleGetInfoString` is no longer documented, I would look into alternatives (eg. `NSHumanReadableCopyright `). – mkalmes Mar 16 '16 at 15:38