I have installed FreeType on my mac using Brew. The code on my mac works fine, but when I try to run the project on other mac I get the below mentioned linking error.
dyld: Library not loaded: /usr/local/opt/freetype/lib/libfreetype.6.dylib
Referenced from: /Users/ashutosh/Library/Developer/Xcode/DerivedData/InstrumentChamp-
etytleaabhxmokadgrjzbgtmwxfl/Build/Products/Debug/instrumentchamp.app/Contents/MacOS/instrumentchamp
Reason: image not found (lldb)
All the library directories and include directories of Freetype are included in project's '$SRCROOT/' directory when I try to run the code on other mac.
The path you see in the linking error for library is where brew had installed freetype in the mac where I created this project.
/usr/local/opt/freetype/lib/libfreetype.6.dylib
I have copied all the lib/ include/ directories that were needed to my project's home folder.
And I have set the library and include paths in Xcode.
What is it that I am missing here? What else do I have to do to make my code portable on any other Mac.
I got the project to run on other mac by installing Brew, but I want to do it without the need to install brew.
PS: I had to install freetype using brew, as I couldn't compile the .dylib for freetype for 32bit processor, a 64bit copy of .dylib was giving me error such as 'wrong architecture!'
The basic idea I was getting at in my comments is that OS X is pretty stupid about where it searches for libraries, it will use the same absolute path used during compilation to resolve them at run-time.
Usually when you want to deploy/distribute your application to a different machine from the one that built it, you will include your libraries with the install package/bundle. But you probably want them to use a path relative to your application at run-time, thus install_name_tool -change allows you to replace the nasty absolute path with something relative.
Hope this makes sense, Apple makes it really easy to use system-wide frameworks on OS X, custom libraries not so much. If you compile using a system-wide framework, /System/Library/Frameworks/...
is universally available on all OS X installs (given the same target release version).
To solve your problem, I would do the following:
install_name_tool -change /usr/local/opt/freetype/lib/libfreetype.6.dylib @executable_path/lib/libfreetype.6.dylib <executable_name_here>
Then it will stop looking for libfreetype.6.dylib in the location it was when you compiled the software, and instead search for it relative to your executable's location at run-time (in this case, in the sub-directory lib/
).
Here is an automated script based on Andon's answer that will replace all @executable_path/../Frameworks/
with @executable_path/
, so that all frameworks can be in the same folder as the executable. This also adapts the frameworks themselves, if they have dependencies on each other.
Beware, this only makes sense for command-line applications. If it's a regular OSX bundles app, one probably shouldn't change the "../Frameworks" convention.
Instructions
Add this as "Run Script" using Shell = /bin/sh
at the end of your Xcode project Build Phases.
Also make sure to have the "Copy Files" step with a "Destination = Products Directory" for all the frameworks.
Script
# change the hardcoded ../Frameworks relative path that Xcode does by rewriting the binaries after the build
# check with:
# otool -L <binary>
# this is the new location
# make sure this is the same Destination the Frameworks are copied to in the "Copy Files" step
# the default "@executable_path/" would be Destination = Products Directory
NEW_FRAMEWORKS_PATH="@executable_path/"
# the one we want to replace
DEFAULT_FRAMEWORKS_PATH="@executable_path/../Frameworks/"
function change_binary {
local libpaths=`otool -L "$1" | grep "$DEFAULT_FRAMEWORKS_PATH" | tr '\t' ' ' | cut -d " " -f2`
local lib
for lib in $libpaths; do
if [ "$2" == "recursive" ]; then
local libbinary=`echo $lib | sed "s,$DEFAULT_FRAMEWORKS_PATH,,"`
change_binary "$libbinary"
fi
local newlib=`echo $lib | sed "s,$DEFAULT_FRAMEWORKS_PATH,$NEW_FRAMEWORKS_PATH,"`;
echo "changing library path in '$1': '$lib' => '$newlib'"
install_name_tool -change "$lib" "$newlib" "$1"
done
}
cd $BUILT_PRODUCTS_DIR
change_binary "$EXECUTABLE_NAME" recursive
Note that changing NEW_FRAMEWORKS_PATH to e.g. a relative subpath does not work, the script would need to become a bit more complex to handle this.
Result Structure
$BUILT_PRODUCTS_DIR/
executable
Some.framework/
Another.framework/
...
And otool -L executable
would look like this:
executable:
@executable_path/Some.framework/Versions/A/Some (...)
@executable_path/Another.framework/Versions/A/Another (...)
...
In the end, this did not end up as easy as I originally thought. It works but there are caveats.
Building on the answers from Andon M. Coleman & Alexander Klimetschek, here is a script function which takes dylib names from Homebrew, copies them into the app bundle's Contents/Frameworks
directory, and sets the executable's corresponding search path using install_name_tool
. I noticed that sometimes the actual .dylib location is different from what the Xcode linker finds when putting together the executable and this handles that as well by querying the exe with otool -L
.
The script below copies the .dylib for the liblo library into the app bundle. Adding new libs should be as simple as adding a new copy lib ###.dlyib
line to the bottom of the script.
# exit on error
set -e
# paths
LOCALLIBS_PATH="$SRCROOT/libs"
HOMEBREW_PATH="/usr/local"
FRAMEWORKS_PATH="$BUILT_PRODUCTS_DIR/$FRAMEWORKS_FOLDER_PATH"
EXE_PATH="$BUILT_PRODUCTS_DIR/$EXECUTABLE_FOLDER_PATH/$EXECUTABLE_NAME"
# code signing identity
IDENTITY="$EXPANDED_CODE_SIGN_IDENTITY_NAME"
if [ "$IDENTITY" == "" ] ; then
IDENTITY="$CODE_SIGN_IDENTITY"
fi
# copy lib into Contents/Frameworks and set lib search path
# $1: library .dylib
# $2: optional path to libs folder, otherwise use Homebrew
copylib() {
LIB=$1
if [ "$2" == "" ] ; then
# use homebrew
LIB_PATH=$(find "$HOMEBREW_PATH" -type f -name $LIB)
else
# use given path
LIB_PATH="$2/$LIB"
fi
echo "$LIB:"
echo " $LIB_PATH -> $FRAMEWORKS_FOLDER_PATH/$LIB"
# copy lib, set permissions, and sign
mkdir -p "$FRAMEWORKS_PATH"
cp "$LIB_PATH" "$FRAMEWORKS_PATH/"
chmod 755 "$FRAMEWORKS_PATH/$LIB"
codesign --verify --force --sign "$IDENTITY" "$FRAMEWORKS_PATH/$LIB"
# set new path within executable
OLD=$(otool -L "$EXE_PATH" | grep $LIB | awk '{print $1}')
NEW="@executable_path/../Frameworks/$LIB"
install_name_tool -change "$OLD" "$NEW" "$EXE_PATH"
# print to confirm new path
otool -L "$EXE_PATH" | grep $LIB
}
##### GO
# use lib from homebrew
copy lib liblo.7.dylib
# or use local lib
#copylib liblo.7.dylib "$LOCALLIBS_PATH/liblo/lib"
Add this as a Run Script build phase in the Xcode project. You can also save it as an external file and call it using sh
within the Run Script phase with:
sh $PROJECT_DIR/path-to/copy-libs.sh
Example build report output:
liblo.7.dylib:
/usr/local/Cellar/liblo/0.29/lib/liblo.7.dylib ->> YOURAPP.app/Contents/Frameworks/liblo.7.dylib
@executable_path/../Frameworks/liblo.7.dylib (compatibility version 11.0.0, current version 11.0.0)
The $(find $HOMEBREW_PATH -type f -name $LIB)
could probably be improved by following the .dylib symlink in /usr/local/lib
instead.
UPDATE: You will probably also need to make sure the new dylibs are signed otherwise the code signing step will fail when building, at least it eventually did for me. Following this SO post, adding --deep
to the Other Code Signing Flags option in the project Build Settings works.
There might be a better way to sign only the files that the script adds, but I didn't feel like digging into running codesign
manually. From what I've read, however, --deep
is not suggested by Apple for anything but temporary fixes, so it's probably not a best practice.
UPDATE 2: Running --deep
didn't actually solve the problem and the app still would not run on other machines. In the end, I needed to code sign the copied .dylib manually which turned out to be relatively easy.
One note: I ran into an ambiguous "Mac Developer" identities error when running codesign which seemed to be confused by two certificates with the same name in the Keychain: ie. the current developer certificate and the previous expired developer certificate. Opening Keychain Access and deleting the expired certificate solved the problem.
UPDATE 3: Quoted path variable usage to fix paths with spaces.
UPDATE 4: This solution works fine for building apps for recent systems, but I ran into a code signing problem when running the app on macOS 10.10. Judging from this SO post, older macOS versions use a different codesign hash algorithm and the app would run fine on a 10.12 system but fail with a code signing error on 10.10. On both systems, codesign verified that everything was signed correctly.
You basically need to build the libraries with the -mmacosx-version-min
set so codesign will be able to tell which algorithms to use to support the min and the recent macOS versions. I tried to pass custom CFLAGS/LDFLAGS to Homebrew while building liblo from source, but it's not possible without editing the brew formula. In the end, I decided to write a build script to build liblo from source myself and I modified the copy script so it can also take a local location. Now everything is finally working.
TLDR: Homebrew libs work great for initial development & testing, but building the libs manually works better for deployment.