Question

I would like to include the application version and internal revision, something like 1.0.1 (r1243), in my application's settings bundle.

The Root.plist file contains a fragment like this...

     <dict>
        <key>Type</key>
        <string>PSTitleValueSpecifier</string>
        <key>Title</key>
        <string>Version</string>
        <key>Key</key>
        <string>version_preference</string>
        <key>DefaultValue</key>
        <string>VersionValue</string>
        <key>Values</key>
        <array>
            <string>VersionValue</string>
        </array>
        <key>Titles</key>
        <array>
            <string>VersionValue</string>
        </array>
    </dict>

and I would like to replace the "VersionValue" string at build time.

I have a script that can extract the version number from my repository, what I need is a way to process (pre-process) the Root.plist file, at build time, and replace the revision number without affecting the source file.

Was it helpful?

Solution 7

I managed to do what I wanted by using the pListcompiler (http://sourceforge.net/projects/plistcompiler) open source porject.

  1. Using this compiler you can write the property file in a .plc file using the following format:

    plist {
        dictionary {
            key "StringsTable" value string "Root"
            key "PreferenceSpecifiers" value array [
                dictionary {
                    key "Type" value string "PSGroupSpecifier"
                    key "Title" value string "AboutSection"
                }
                dictionary {
                    key "Type" value string "PSTitleValueSpecifier"
                    key "Title" value string "Version"
                    key "Key" value string "version"
                    key "DefaultValue" value string "VersionValue"
                    key "Values" value array [
                        string "VersionValue"
                    ]
                    key "Titles" value array [
                        string "r" kRevisionNumber
                    ]
                }
            ]
        }
    }
    
  2. I had a custom run script build phase that was extracting my repository revision to .h file as described by brad-larson here.

  3. The plc file can contain preprocessor directives, like #define, #message, #if, #elif, #include, #warning, #ifdef, #else, #pragma, #error, #ifndef, #endif, xcode environment variables. So I was able to reference the variable kRevisionNumber by adding the following directive

    #include "Revision.h"
    
  4. I also added a custom script build phase to my xcode target to run the plcompiler every time the project is beeing build

    /usr/local/plistcompiler0.6/plcompile -dest Settings.bundle -o Root.plist Settings.plc
    

And that was it!

OTHER TIPS

There is another solution that can be much simpler than either of the previous answers. Apple bundles a command-line tool called PlistBuddy inside most of its installers, and has included it in Leopard at /usr/libexec/PlistBuddy.

Since you want to replace VersionValue, assuming you have the version value extracted into $newVersion, you could use this command:

/usr/libexec/PlistBuddy -c "Set :VersionValue $newVersion" /path/to/Root.plist

No need to fiddle with sed or regular expressions, this approach is quite straightforward. See the man page for detailed instructions. You can use PlistBuddy to add, remove, or modify any entry in a property list. For example, a friend of mine blogged about incrementing build numbers in Xcode using PlistBuddy.

Note: If you supply just the path to the plist, PlistBuddy enters interactive mode, so you can issue multiple commands before deciding to save changes. I definitely recommend doing this before plopping it in your build script.

My lazy man's solution was to update the version number from my application code. You could have a default (or blank) value in the Root.plist and then, somewhere in your startup code:

NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
[[NSUserDefaults standardUserDefaults] setObject:version forKey:@"version_preference"];

The only catch is that your app would have to be run at least once for the updated version to appear in the settings panel.

You could take the idea further and update, for instance, a counter of how many times your app has been launched, or other interesting bits of information.

Based on @Quinn's answer, here the full process and working code I use to do this.

  • Add a settings bundle to your app. Don't rename it.
  • Open Settings.bundle/Root.plist in a text editor

Replace the contents with:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"     "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Title</key>
            <string>About</string>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
        </dict>
        <dict>
            <key>DefaultValue</key>
            <string>DummyVersion</string>
            <key>Key</key>
            <string>version_preference</string>
            <key>Title</key>
            <string>Version</string>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
</dict>
</plist>
  • Create a Run Script build phase, move to be after the Copy Bundle Resources phase. Add this code:

    cd "${BUILT_PRODUCTS_DIR}"
    buildVersion=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_PATH}" )
    /usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $buildVersion" "${WRAPPER_NAME}/Settings.bundle/Root.plist"
    
  • Replace MyAppName with your actual app's name, and the 1 after PreferenceSpecifiers to be the index of your Version entry in the Settings. The above Root.plist example has it at index 1.

Using Ben Clayton's plist https://stackoverflow.com/a/12842530/338986

Add Run script with following snippet after Copy Bundle Resources.

version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PROJECT_DIR/$INFOPLIST_FILE")
build=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$PROJECT_DIR/$INFOPLIST_FILE")
/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $version ($build)" "$CODESIGNING_FOLDER_PATH/Settings.bundle/Root.plist"

Appending CFBundleVersion in addition of CFBundleShortVersionString. It emit version like this:

By writing to $CODESIGNING_FOLDER_PATH/Settings.bundle/Root.plist instead of the one in $SRCROOT have some benefits.

  1. It dosen't modify files in working copy of repository.
  2. You don't need to case path to Settings.bundle in $SRCROOT. The path may vary.

Testing on Xcode 7.3.1

Based on the example here, here's the script I'm using to automatically update the settings bundle version number:

#! /usr/bin/env python
import os
from AppKit import NSMutableDictionary

settings_file_path = 'Settings.bundle/Root.plist' # the relative path from the project folder to your settings bundle
settings_key = 'version_preference' # the key of your settings version

# these are used for testing only
info_path = '/Users/mrwalker/developer/My_App/Info.plist'
settings_path = '/Users/mrwalker/developer/My_App/Settings.bundle/Root.plist'

# these environment variables are set in the XCode build phase
if 'PRODUCT_SETTINGS_PATH' in os.environ.keys():
    info_path = os.environ.get('PRODUCT_SETTINGS_PATH')

if 'PROJECT_DIR' in os.environ.keys():
    settings_path = os.path.join(os.environ.get('PROJECT_DIR'), settings_file_path)

# reading info.plist file
project_plist = NSMutableDictionary.dictionaryWithContentsOfFile_(info_path)
project_bundle_version = project_plist['CFBundleVersion']

# print 'project_bundle_version: '+project_bundle_version

# reading settings plist
settings_plist = NSMutableDictionary.dictionaryWithContentsOfFile_(settings_path)
  for dictionary in settings_plist['PreferenceSpecifiers']:
    if 'Key' in dictionary and dictionary['Key'] == settings_key:
        dictionary['DefaultValue'] = project_bundle_version

# print repr(settings_plist)
settings_plist.writeToFile_atomically_(settings_path, True)

Here's the Root.plist I've got in Settings.bundle:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Title</key>
            <string>About</string>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
        </dict>
        <dict>
            <key>DefaultValue</key>
            <string>1.0.0.0</string>
            <key>Key</key>
            <string>version_preference</string>
            <key>Title</key>
            <string>Version</string>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
</dict>
</plist>

The other answers do not work correctly for one reason: The run script build phase isn't executed until AFTER the Settings Bundle has been packaged. So, if your Info.plist version is 2.0.11 and you update it to 2.0.12, then build/archive your project, the Settings bundle will still say 2.0.11. If you open the Settings bundle Root.plist, you can see that the version number does not get updated until the END of the build process. You can build the project AGAIN to get the Settings bundle updated correctly, or you can add the script to a pre-build phase instead...

  • In XCode, Edit the Scheme for your project target
  • Click the disclosure arrow on the BUILD scheme
  • Then, click on the "Pre-actions" item
  • Click the plus sign and choose "New Run Script Action"
  • Set the shell value to /bin/sh
  • Set "Provide build settings from" to your project target
  • Add your script to the text area. The following script worked for me. You may need to modify the paths to match your project setup:

    versionString=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")

    /usr/libexec/PlistBuddy "$SRCROOT/Settings.bundle/Root.plist" -c "set PreferenceSpecifiers:0:DefaultValue $versionString"

This will correctly run the script BEFORE the Settings bundle is packaged during the build/archive process. If you open the Settings bundle Root.plist and build/archive your project, you will now see the version number is updated at the beginning of the build process and your Settings bundle will display the correct version.

I believe you can do this using a way that's similar to what I describe in this answer (based on this post).

First, you can make VersionValue a variable within Xcode by renaming it to ${VERSIONVALUE}. Create a file named versionvalue.xcconfig and add it to your project. Go to your application target and go to the Build settings for that target. I believe that you need to add VERSIONVALUE as a user-defined build setting. In the lower-right-corner of that window, change the Based On value to "versionvalue".

Finally, go to your target and create a Run Script build phase. Inspect that Run Script phase and paste in your script within the Script text field. For example, my script to tag my BUILD_NUMBER setting with the current Subversion build is as follows:

REV=`/usr/bin/svnversion -nc ${PROJECT_DIR} | /usr/bin/sed -e 's/^[^:]*://;s/[A-Za-z]//'`
echo "BUILD_NUMBER = $REV" > ${PROJECT_DIR}/buildnumber.xcconfig

This should do the trick of replacing the variable when these values change within your project.

My working Example based on @Ben Clayton answer and the comments of @Luis Ascorbe and @Vahid Amiri:

Note: This approach modifies the Settings.bundle/Root.plist file in working copy of repository

  1. Add a settings bundle to your project root. Don't rename it
  2. Open Settings.bundle/Root.plist as SourceCode

    Replace the contents with:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>PreferenceSpecifiers</key>
        <array>
            <dict>
                <key>DefaultValue</key>
                <string></string>
                <key>Key</key>
                <string>version_preference</string>
                <key>Title</key>
                <string>Version</string>
                <key>Type</key>
                <string>PSTitleValueSpecifier</string>
            </dict>
        </array>
        <key>StringsTable</key>
        <string>Root</string>
    </dict>
    </plist>
    
  3. Add the following script to the Build, Pre-actions section of the project (target) scheme

    version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PROJECT_DIR/$INFOPLIST_FILE")
    build=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$PROJECT_DIR/$INFOPLIST_FILE")
    
    /usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:0:DefaultValue $version ($build)" "${SRCROOT}/Settings.bundle/Root.plist"
    
  4. Build and Run the current scheme

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top