23 March 2009

Automating the Setup of Xcode Executables

Problem

Xcode does not store Executable settings in the project file. The settings end up in the user.pbxuser. These files do not typically get checked in to version control system, making sharing the setup of these executable impossible. This causes problems if you have environment settings, or command line arguments that need to be called in your executable in a consistent way, across all developer machines.

The situation that motivates this post is the need to set the DYLD_FALLBACK_LIBRARY_PATH in a unit test executable to help it find a third-party dynamic library. Simple command line tools do not get the same benefits of application bundles, which have their dependent frameworks and libraries packaged neatly away. Command-line tools are simply executable code without packaged resources or private frameworks. Because there is no package, when the executable can only link to libraries in known locations such as /usr/lib/ or /System/Library/Frameworks.

At times this can cause problems. When unit testing code, you might create a simple executable that links in required frameworks and runs tests on them and produces code coverage. The problem is that your application will be able to link to a variety of dynamic libraries and frameworks and embed them its bundle. The unit test executables, on the other hand, do not have a bundle. How then, can your executable find these dynamic libraries and frameworks at runtime?

Note: A solution to this particular problem is to copy all of the frameworks and libraries into the $BUILT_PRODUCTS_DIR, this can be done simply with a build phase in your test target, but doesn't serve to demonstrate the automation of other environment variables or command line arguments.

AppleScript to the Rescue

Fortunately for us, Xcode is an extremely scriptable application. Here is a script that modifies the currently building target and sets up some environment variables on the linked executable.

-- Function: setEnvironmentVariable

-- Sets an environment variable in the executable, which is linked to the current target. If the

-- environment variable exists, the value is overwritten. If the environment variable doesn't exist

-- the variable is created and set.

--

-- Args:

--    variableName - name of the environment variable to set

--    variableValue - value of the environment variable being set

on setEnvironmentVariable(variableName, variableValue)

tell application "Xcode"

tell active project document

set executableName to name of executable of active target as string

tell executable executableName

-- Check to see if the fallback path already exists

set hasVariable to false as boolean

repeat with environmentVariable in environment variables

if name of environmentVariable is equal to variableName then

-- Overwrite any value

set value of environmentVariable to variableValue

set active of environmentVariable to yes

set hasVariable to true as boolean

exit repeat

end if

end repeat

-- Since the fallback path doesn't exist yet, create it

if not hasVariable then

make new environment variable with properties ¬

{name:variableName, value:variableValue, active:yes}

end if

end tell -- executable

end tell -- active project document

end tell -- Xcode

end setEnvironmentVariable


tell application "Xcode"

tell active project document

-- First, find the path to the project directory

set projectPath to path as string

-- Create a library path to the third-party and in-house libraries

set dyldLibraryPath to projectPath & "../relative/path/to/third-party/lib" & ":" & ¬

projectPath & "../relative/path/to/in-house/lib"

-- Create a framework path to the third-party and in-house frameworks

set dyldFrameworkPath to projectPath & "../relative/path/to/third-party/frameworks" & ":" & ¬

projectPath & "../relative/path/to/in-house/frameworks"

-- Next, add path to the DYLD fallback paths

my setEnvironmentVariable("DYLD_FALLBACK_FRAMEWORK_PATH", dyldFrameworkPath)

my setEnvironmentVariable("DYLD_FALLBACK_LIBRARY_PATH", dyldLibraryPath)

end tell -- active project document

end tell -- Xcode

Adding the Script to your Target

To add the above script to a target, first, save the script as an applescript. Then add a "Run Script" build phase with the one line

osascript path/to/script.applescript

Obviously replacing the path and file name to that of your own script.

No comments: