Linking an OSX external bundle with a .dylib library

If you are creating an external bundle on OSX that uses another dynamic library you will find some special issues. In particular, you may find that the external works from within XCode but doesn't work from a rev stack outside the XCode environment.

In my case, I had a third-party dynamic library with a well-defined api. I wrote the code to wrap the api calls and everything worked fine from the "Build and Go" mode of XCode: my code compiled and linked properly, then XCode launched Revolution with my test stack and all my functions worked properly. But when I launched my test stack by double-clicking or opening from the IDE, my external library didn't load.

The problem

OSX dynamic libraries (file extension ".dylib") have an "install name" that tells the operating system where the library expects to be found. If it's not in that location in your system the library won't load. And what that means for us creating an OSX external bundle is that our external library won't load: you'll still see it if you display the externals of the stack because that's just a text property of the path to the bundle, but you won't see it if you display the externalpackages of the stack because it didn't load properly.

In order to get our external library to find the dynamic library and load properly we have to create an extra Build Step in XCode after the compilation and linking process has completed. This will modify the bundle we created by creating a Frameworks directory in it, placing the dynamic library in the Frameworks directory, and modifying the install name appropriately.

To make things simpler, it's easiest to have the dynamic library you're working with (or a copy of it) in the same folder as the XCode project. That way we can use the SRCROOT environment variable later on to make the XCode project portable without having to rewrite the build script.

Step 1: Drag the library into your XCode project

If you don't already have a Frameworks folder in your project, Add one.
Then drag the dynamic library you want to link with into the Frameworks folder.

Step 2: Create the external and test it in XCode

This is an essential first step. If the external is running from the "Build and Go" menu of XCode, launching a Revolution test stack, open the message box by pressing command-M. Look at the list of globals: you should see both DYLD_LIBRARY_PATH and DYLD_FRAMEWORK_PATH. If you try this by running a stack outside the XCode environment and look at the globals you won't see these variables. XCode sets these up and exports them before running your stack in test mode. You can't do this in your stack, so you need another way to access the dynamic library. We do this by creating a new post-compilation Build Step.

Step 3: Run otool to find the install_name

Open a terminal window and type the following (NOTE: it's easiest to type "otool -L " and then drag your dynamic library from the Finder into the terminal window, then press return):


You should see a display something like the above.

The important part is right at the top: the part that says "@executable_path/libGoIO_DLL.dylib". The dynamic library might have an absolute path coded in place of the "@executable_path" part.

Step 4: Create a new Build Step

In XCode right- or control-click on your Target.

From the popup menu select Add | New Build Phase | New Run Script Build Phase

Type the following build script into the Script field:

NOTE 1: the text in red should be replaced by the appropriate information for your library:

in other words, if your dynamic library is in the same folder as the XCode project file then you should put its filename in place of NameOfLibrary, if the name of the dynamic library is "Code.dylib" then you should put "Code.dylib" in place of "NameOfLibrary" (see example 1), and you should put the result of the previous call to otool in the place of "@executable_path/$DYLIB" (see example 3).

If your dynamic library is not in the same folder as the XCode project, then replace the entire "$SRCROOT/$DYLIB" string with the absolute path. In other words, if your library is at "/Users/Fred/Desktop/iCode.dylib" then you should put that in place of $SRCROOT/$DYLIB. See example 2.

NOTE 2: watch the line wrap on the last line: you should have four lines of code.

export DYLIB=NameOfLibrary
mkdir "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
cp -f "$SRCROOT/$DYLIB" "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
install_name_tool -change @executable_path/$DYLIB @loader_path/../Frameworks/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/MacOS/$PRODUCT_NAME"

Example 1: if the name of the dynamic library is "Code.dylib" and it's in the same folder as the XCode project file, you would have

export DYLIB=Code.dylib
mkdir "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
cp -f "$SRCROOT/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
install_name_tool -change @executable_path/$DYLIB @loader_path/../Frameworks/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/MacOS/$PRODUCT_NAME"

Example 2: If that same dynamic library is in "/Users/Fred/src/" then you'd have

export DYLIB=Code.dylib
mkdir "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
cp -f "/Users/Fred/src/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
install_name_tool -change @executable_path/$DYLIB @loader_path/../Frameworks/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/MacOS/$PRODUCT_NAME"

Example 3: if otool in step 3 above returned "./executable_path/Code.dylib" you'd have

export DYLIB=Code.dylib
mkdir "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
cp -f "/Users/Fred/src/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"
install_name_tool -change ./executable_path/$DYLIB @loader_path/../Frameworks/$DYLIB "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/MacOS/$PRODUCT_NAME"

What this does step-by-step

First we declare an environment variable to make life easier.
export DYLIB=NameOfLibrary

Next we make the Frameworks directory in the external bundle.
We are using two XCode environment variables here to find the bundle.
mkdir "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"

Copy the dynamic library file into the Frameworks directory we just created.
We are using the XCode environment variable SRCROOT because we placed the dynamic library in the same folder as the XCode project file. If the library is somewhere else, you can use an absolute path to it.
cp -f $SRCROOT/NameOfLibrary "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/Frameworks"

And finally change the "intall_name" of the library so the bundle knows where to look for it.
install_name_tool -change @executable_path/NAME.dylib @loader_path/../Frameworks/NAME.dylib "$TARGET_BUILD_DIR/$TARGET_NAME.bundle/Contents/MacOS/$PRODUCT_NAME"

A real-world example

Here's what the Build Step looks like for my bundle. Note that the last line wrapped - there are actually four lines of text in the script.

Compile your external bundle

If there are no errors in your Build Script the next time you build your external library you'll see the Frameworks folder inside the bundle (control-click the bundle and select "Show Package Contents") and the dynamic library inside the Frameworks folder.

And you should be able to run your stack from the IDE and have the externals working properly.