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

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

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):

otool -L PATHTOTHEDYNAMICLIBRARY

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

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 -p  "$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 -p  "$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 -p  "$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 -p  "$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 -p  "$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

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.

18 Comments

Tim Murray-Browne

This is really helpful. Thank you.

Tomas

Thanks! you saved my day :)

phil

Great! Thanks so much!!

These instructions worked fine for Xcode 9.4.1 with the exception that I had to change "bundle" to "app" throughout the script. Also, I added -p to the "mkdir" command because it was giving a warning that the Frameworks directory already existed.

Mark Wieder

Phil- thanks for the suggestion. I added the -p flag to the mkdir command. If you're adding a dynamic library to an app then it makes sense to change the 'bundle' identifier. Otherwise, since an app is a subset of the bundle type, you can aggregate libraries into other bundles for use and resuse, as in frameworks.

Igor

Thank you for the script and explanation on how it works.
However, I'm curious - why do you use "install_name_tool" to change @executable_path to @loader_path and why are you changing this on completely bogus file? Because "$PRODUCT_NAME" as the last parameter for this command is not correct. You should change this on the "$DYLIB", which is at the compiled place and not the copied one.

Am I completely wrong?

Thank you.

Mark Wieder

Admittedly this was written several years (and several Xcode iterations) ago, but the commandline arguments haven't changed. What this line does is change to search path for the dynamic library from the default absolute location to a location relative to the Framework folder. And the final argument says to modify the search path of the application itself, and that's the critical part. If you don't modify the application you're building then it will continue to look in the default location, and that's pretty much guaranteed to fail.

Igor

@Mark Wieder,
Thank you for the explanation.
I thought (based on the Google search) that one suppose to change this setting for the library itself.

[quote]
-change old new
Changes the dependent shared library install name old to new in the specified Mach-O binary. More than one of these options can be
specified. If the Mach-O binary does not contain the old install name in a specified -change option the option is ignored.
[/quote]

The quote from the man page seems to support my theory - you change the install directory on the library itself.

Thank you.

Mark Wieder

Igor- the problem that needs solving here is that the app launched within Xcode is able to find the dynamic library using the absolute address because that's where it was built. When you build the app and take it out of Xcode it no longer can use that reference. So the problem is that the app itself needs updating in order to know where to search for the library. It's also possible to change the search path on the library itself, but that wouldn't fix the problem.

Igor

@Mark Wieder,
After doing

[code]
mkdir -p "$TARGET_BUILD_DIR/$TARGET_NAME.app/Contents/Frameworks"
cp -f ~/dbhandler/dbhandler/Build/Products/Debug/liblibdbwindow.dylib "$TARGET_BUILD_DIR/$TARGET_NAME.app/Contents/Frameworks/liblibdbwindow.dylib"
install_name_tool -change @executable_path/liblibdbwindow.dylib @loader_path/../Frameworks/liblibdbwindow.dylib "$TARGET_BUILD_DIR/$TARGET_NAME.app/Contents/MacOS/$PRODUCT_NAME"
[/code]

and issuing:

[code]
open dbhandler.app
[/code]

I'm still getting:

[quote]
/usr/local/lib/liblibdbwindow.dylib: No such file or directory
[/quote]

So, something is not right here.

Igor

It looks like what I need to do is

https://geekjutsu.wordpress.com/2015/10/13/embedding-dylib-libraries-in-your-application-bundle/

You might fix this article..

Thank you.

Mark Wieder

So I'm confused - that article says exactly the same thing. Well, there *is* one difference - in Xcode is your TARGET_NAME somehow not the same as your PRODUCT_NAME?

Mark Wieder

See above: the result of otool should give you the path to the installed library. That should be the first argument to install_name_change. Basically you want to tell the application to ignore the default location of the dynamic library and use its bundled version instead.

Mark Wieder

Hmmm... what's the result of running otool?

Igor

@Mark Weider,

[code]
MyMac:Frameworks igorkorot$ pwd
/Users/igorkorot/dbhandler/dbhandler/Build/Products/Debug/dbhandler.app/Contents/Frameworks
MyMac:Frameworks igorkorot$ otool -L liblibdbwindow.dylib
liblibdbwindow.dylib:
/usr/local/lib/liblibdbwindow.dylib (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
/System/Library/Frameworks/Carbon.framework/Versions/A/Carbon (compatibility version 2.0.0, current version 157.0.0)
/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 20.0.0)
/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox (compatibility version 1.0.0, current version 492.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_stc-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_xrc-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_html-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_qa-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_adv-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_core-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_baseu_xml-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_baseu_net-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_baseu-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/local/lib/liblibpropertypages.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/local/lib/liblibfieldswindow.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/local/lib/liblibshapeframework.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 120.0.0)
MyMac:Frameworks igorkorot$
[/code]

I

And this one:

[code]
MyMac:MacOS igorkorot$ otool -L dbhandler
dbhandler:
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
/System/Library/Frameworks/Carbon.framework/Versions/A/Carbon (compatibility version 2.0.0, current version 157.0.0)
/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 20.0.0)
/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox (compatibility version 1.0.0, current version 492.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_xrc-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_html-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_qa-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_adv-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_osx_cocoau_core-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_baseu_xml-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_baseu_net-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
/Users/igorkorot/wxWidgets/buildC11/lib/libwx_baseu-3.1.3.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
@executable_path/../Frameworks/liblibdbwindow.dylib (compatibility version 1.0.0, current version 1.0.0)
@executable_path/../Frameworks/liblibdialogs.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 120.0.0)
MyMac:MacOS igorkorot$ pwd
/Users/igorkorot/dbhandler/dbhandler/Build/Products/Debug/dbhandler.app/Contents/MacOS
MyMac:MacOS igorkorot$

Mark Wieder

Given the output of the first call to otool above, the library you're trying to bring into the app is in /usr/local/lib, as you mentioned above.
In the second call I see the library in the app/Contents/Frameworks folder and If Xcode's macros are configured correctly then you'd want to invoke install_name_change as follows (note that since this is an application you can probably substitute @executable_path for @loader_path):

[code]
install_name_tool -change /usr/local/lib/liblibdbwindow.dylib @loader_path/../Frameworks/liblibdbwindow.dylib "$TARGET_BUILD_DIR/$TARGET_NAME.app/Contents/MacOS/$PRODUCT_NAME"
[/code]

Mark Wieder

Mark Wieder

Hmmm... so it's working in Xcode but not in the app itself? And you copied the library into the Frameworks folder of the app source in Xcode, and then told the app to look in its own Frameworks folder and rebuilt? And it still isn't working? That's puzzling.

Add your comment

E-Mail me when someone replies to this comment